mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-12 18:59:26 +00:00
Compare commits
1 Commits
hide-stop
...
auth-passw
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bd316a36a0 |
@@ -9,6 +9,7 @@ import {
|
||||
mdiExclamationThick,
|
||||
mdiFlask,
|
||||
mdiHomeAssistant,
|
||||
mdiInformation,
|
||||
mdiKey,
|
||||
mdiNetwork,
|
||||
mdiPound,
|
||||
@@ -52,7 +53,6 @@ import { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/hassio-card-content";
|
||||
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
|
||||
const STAGE_ICON = {
|
||||
stable: mdiCheckCircle,
|
||||
@@ -386,94 +386,67 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
${this.addon.version
|
||||
? html`
|
||||
<div class="addon-options">
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
Start on boot
|
||||
</span>
|
||||
<span slot="description">
|
||||
Make the add-on start during a system boot
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._startOnBootToggled}
|
||||
.checked=${this.addon.boot === "auto"}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
|
||||
${this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
Watchdog
|
||||
</span>
|
||||
<span slot="description">
|
||||
This will start the add-on if it crashes
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._watchdogToggled}
|
||||
.checked=${this.addon.watchdog}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.auto_update || this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
Auto update
|
||||
</span>
|
||||
<span slot="description">
|
||||
Auto update the add-on when there is a new version
|
||||
available
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._autoUpdateToggled}
|
||||
.checked=${this.addon.auto_update}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.ingress
|
||||
? html`
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
Show in sidebar
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this._computeCannotIngressSidebar
|
||||
? "This option requires Home Assistant 0.92 or later."
|
||||
: "Add this add-on to your sidebar"}
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._panelToggled}
|
||||
.checked=${this.addon.ingress_panel}
|
||||
.disabled=${this._computeCannotIngressSidebar}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: ""}
|
||||
${this._computeUsesProtectedOptions
|
||||
? html`
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
Protection mode
|
||||
</span>
|
||||
<span slot="description">
|
||||
Blocks elevated system access from the add-on
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._protectionToggled}
|
||||
.checked=${this.addon.protected}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: ""}
|
||||
<div class="state">
|
||||
<div>Start on boot</div>
|
||||
<ha-switch
|
||||
@change=${this._startOnBootToggled}
|
||||
.checked=${this.addon.boot === "auto"}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
${this.addon.auto_update || this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<div class="state">
|
||||
<div>Auto update</div>
|
||||
<ha-switch
|
||||
@change=${this._autoUpdateToggled}
|
||||
.checked=${this.addon.auto_update}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.ingress
|
||||
? html`
|
||||
<div class="state">
|
||||
<div>Show in sidebar</div>
|
||||
<ha-switch
|
||||
@change=${this._panelToggled}
|
||||
.checked=${this.addon.ingress_panel}
|
||||
.disabled=${this._computeCannotIngressSidebar}
|
||||
haptic
|
||||
></ha-switch>
|
||||
${this._computeCannotIngressSidebar
|
||||
? html`
|
||||
<span>
|
||||
This option requires Home Assistant 0.92 or
|
||||
later.
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._computeUsesProtectedOptions
|
||||
? html`
|
||||
<div class="state">
|
||||
<div>
|
||||
Protection mode
|
||||
<span>
|
||||
<ha-svg-icon path=${mdiInformation}></ha-svg-icon>
|
||||
<paper-tooltip>
|
||||
Grant the add-on elevated system access.
|
||||
</paper-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<ha-switch
|
||||
@change=${this._protectionToggled}
|
||||
.checked=${this.addon.protected}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
||||
@@ -579,6 +552,137 @@ class HassioAddonInfo extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-card.warning {
|
||||
background-color: var(--error-color);
|
||||
color: white;
|
||||
}
|
||||
ha-card.warning .card-header {
|
||||
color: white;
|
||||
}
|
||||
ha-card.warning .card-content {
|
||||
color: white;
|
||||
}
|
||||
ha-card.warning mwc-button {
|
||||
--mdc-theme-primary: white !important;
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
.light-color {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.addon-header {
|
||||
padding-left: 8px;
|
||||
font-size: 24px;
|
||||
color: var(--ha-card-header-color, --primary-text-color);
|
||||
}
|
||||
.addon-version {
|
||||
float: right;
|
||||
font-size: 15px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.errors {
|
||||
color: var(--error-color);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.description {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
img.logo {
|
||||
max-height: 60px;
|
||||
margin: 16px 0;
|
||||
display: block;
|
||||
}
|
||||
.state {
|
||||
display: flex;
|
||||
margin: 33px 0;
|
||||
}
|
||||
.state div {
|
||||
width: 180px;
|
||||
display: inline-block;
|
||||
}
|
||||
.state ha-svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-switch {
|
||||
display: flex;
|
||||
}
|
||||
ha-svg-icon.running {
|
||||
color: var(--paper-green-400);
|
||||
}
|
||||
ha-svg-icon.stopped {
|
||||
color: var(--google-red-300);
|
||||
}
|
||||
ha-call-api-button {
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
protection-enable mwc-button {
|
||||
--mdc-theme-primary: white;
|
||||
}
|
||||
.description a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.red {
|
||||
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
|
||||
}
|
||||
.blue {
|
||||
--ha-label-badge-color: var(--label-badge-blue, #039be5);
|
||||
}
|
||||
.green {
|
||||
--ha-label-badge-color: var(--label-badge-green, #0da035);
|
||||
}
|
||||
.yellow {
|
||||
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
|
||||
}
|
||||
.security {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flow-root;
|
||||
}
|
||||
.security h3 {
|
||||
margin-bottom: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.security ha-label-badge {
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
--ha-label-badge-padding: 8px 0 0 0;
|
||||
}
|
||||
.changelog {
|
||||
display: contents;
|
||||
}
|
||||
.changelog-link {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-markdown {
|
||||
padding: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private get _computeHassioApi(): boolean {
|
||||
return (
|
||||
this.addon.hassio_api &&
|
||||
@@ -667,24 +771,6 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _watchdogToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
watchdog: !this.addon.watchdog,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "option",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon option, ${err.body?.message || err}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async _autoUpdateToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
@@ -801,146 +887,6 @@ class HassioAddonInfo extends LitElement {
|
||||
this._error = `Failed to uninstall addon, ${err.body?.message || err}`;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-card.warning {
|
||||
background-color: var(--error-color);
|
||||
color: white;
|
||||
}
|
||||
ha-card.warning .card-header {
|
||||
color: white;
|
||||
}
|
||||
ha-card.warning .card-content {
|
||||
color: white;
|
||||
}
|
||||
ha-card.warning mwc-button {
|
||||
--mdc-theme-primary: white !important;
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
.light-color {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.addon-header {
|
||||
padding-left: 8px;
|
||||
font-size: 24px;
|
||||
color: var(--ha-card-header-color, --primary-text-color);
|
||||
}
|
||||
.addon-version {
|
||||
float: right;
|
||||
font-size: 15px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.errors {
|
||||
color: var(--error-color);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.description {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
img.logo {
|
||||
max-height: 60px;
|
||||
margin: 16px 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-switch {
|
||||
display: flex;
|
||||
}
|
||||
ha-svg-icon.running {
|
||||
color: var(--paper-green-400);
|
||||
}
|
||||
ha-svg-icon.stopped {
|
||||
color: var(--google-red-300);
|
||||
}
|
||||
ha-call-api-button {
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
protection-enable mwc-button {
|
||||
--mdc-theme-primary: white;
|
||||
}
|
||||
.description a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.red {
|
||||
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
|
||||
}
|
||||
.blue {
|
||||
--ha-label-badge-color: var(--label-badge-blue, #039be5);
|
||||
}
|
||||
.green {
|
||||
--ha-label-badge-color: var(--label-badge-green, #0da035);
|
||||
}
|
||||
.yellow {
|
||||
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
|
||||
}
|
||||
.security {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flow-root;
|
||||
}
|
||||
.security h3 {
|
||||
margin-bottom: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.security ha-label-badge {
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
--ha-label-badge-padding: 8px 0 0 0;
|
||||
}
|
||||
.changelog {
|
||||
display: contents;
|
||||
}
|
||||
.changelog-link {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-markdown {
|
||||
padding: 16px;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row > span[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-settings-row[three-line] {
|
||||
height: 74px;
|
||||
}
|
||||
|
||||
.addon-options {
|
||||
max-width: 50%;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.addon-options {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
|
@@ -10,7 +10,7 @@ import {
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/buttons/ha-call-api-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
|
||||
@@ -21,11 +21,6 @@ import {
|
||||
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 {
|
||||
@@ -131,43 +126,31 @@ export class HassioUpdate extends LitElement {
|
||||
<a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
|
||||
<mwc-button>Release notes</mwc-button>
|
||||
</a>
|
||||
<ha-progress-button
|
||||
.apiPath=${apiPath}
|
||||
.name=${name}
|
||||
.version=${lastVersion}
|
||||
@click=${this._confirmUpdate}
|
||||
<ha-call-api-button
|
||||
.hass=${this.hass}
|
||||
.path=${apiPath}
|
||||
@hass-api-called=${this._apiCalled}
|
||||
>
|
||||
Update
|
||||
</ha-progress-button>
|
||||
</ha-call-api-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _confirmUpdate(ev): Promise<void> {
|
||||
const item = ev.target;
|
||||
item.progress = true;
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: `Update ${item.name}`,
|
||||
text: `Are you sure you want to upgrade ${item.name} to version ${item.version}?`,
|
||||
confirmText: "update",
|
||||
dismissText: "cancel",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
item.progress = false;
|
||||
private _apiCalled(ev): void {
|
||||
if (ev.detail.success) {
|
||||
this._error = "";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Update failed",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
});
|
||||
|
||||
const response = ev.detail.response;
|
||||
|
||||
if (typeof response.body === "object") {
|
||||
this._error = response.body.message || "Unknown error";
|
||||
} else {
|
||||
this._error = response.body;
|
||||
}
|
||||
item.progress = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
|
@@ -1,328 +0,0 @@
|
||||
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 { mdiClose } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
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 "../../../../src/components/ha-related-items";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
|
||||
@customElement("dialog-hassio-network")
|
||||
export class DialogHassioNetwork extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _prosessing = false;
|
||||
|
||||
@internalProperty() private _params?: HassioNetworkDialogParams;
|
||||
|
||||
@internalProperty() private _network!: {
|
||||
interface: string;
|
||||
data: NetworkInterface;
|
||||
}[];
|
||||
|
||||
@internalProperty() private _curTabIndex = 0;
|
||||
|
||||
@internalProperty() private _device?: {
|
||||
interface: string;
|
||||
data: NetworkInterface;
|
||||
};
|
||||
|
||||
@internalProperty() private _dirty = false;
|
||||
|
||||
public async showDialog(params: HassioNetworkDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._dirty = false;
|
||||
this._curTabIndex = 0;
|
||||
this._network = Object.keys(params.network?.interfaces)
|
||||
.map((device) => ({
|
||||
interface: device,
|
||||
data: params.network.interfaces[device],
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
return a.data.primary > b.data.primary ? -1 : 1;
|
||||
});
|
||||
this._device = this._network[this._curTabIndex];
|
||||
this._device.data.nameservers = String(this._device.data.nameservers);
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._params = undefined;
|
||||
this._prosessing = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params || !this._network) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog open .heading=${true} hideActions @closed=${this.closeDialog}>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title">
|
||||
Network settings
|
||||
</span>
|
||||
<mwc-icon-button slot="actionItems" dialogAction="cancel">
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</ha-header-bar>
|
||||
${this._network.length > 1
|
||||
? html` <mwc-tab-bar
|
||||
.activeIndex=${this._curTabIndex}
|
||||
@MDCTabBar:activated=${this._handleTabActivated}
|
||||
>${this._network.map(
|
||||
(device) =>
|
||||
html`<mwc-tab
|
||||
.id=${device.interface}
|
||||
.label=${device.interface}
|
||||
>
|
||||
</mwc-tab>`
|
||||
)}
|
||||
</mwc-tab-bar>`
|
||||
: ""}
|
||||
</div>
|
||||
${cache(this._renderTab())}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTab() {
|
||||
return html` <div class="form container">
|
||||
<ha-formfield label="DHCP">
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="dhcp"
|
||||
name="method"
|
||||
?checked=${this._device!.data.method === "dhcp"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Static">
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="static"
|
||||
name="method"
|
||||
?checked=${this._device!.data.method === "static"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
${this._device!.data.method !== "dhcp"
|
||||
? html` <paper-input
|
||||
class="flex-auto"
|
||||
id="ip_address"
|
||||
label="IP address/Netmask"
|
||||
.value="${this._device!.data.ip_address}"
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="gateway"
|
||||
label="Gateway address"
|
||||
.value="${this._device!.data.gateway}"
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="nameservers"
|
||||
label="DNS servers"
|
||||
.value="${this._device!.data.nameservers as string}"
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
></paper-input>
|
||||
NB!: If you are changing IP or gateway addresses, you might lose
|
||||
the connection.`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<mwc-button label="close" @click=${this.closeDialog}> </mwc-button>
|
||||
<mwc-button @click=${this._updateNetwork} ?disabled=${!this._dirty}>
|
||||
${this._prosessing
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: "Update"}
|
||||
</mwc-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _updateNetwork() {
|
||||
this._prosessing = true;
|
||||
let options: Partial<NetworkInterface> = {
|
||||
method: this._device!.data.method,
|
||||
};
|
||||
if (options.method !== "dhcp") {
|
||||
options = {
|
||||
...options,
|
||||
address: this._device!.data.ip_address,
|
||||
gateway: this._device!.data.gateway,
|
||||
dns: String(this._device!.data.nameservers).split(","),
|
||||
};
|
||||
}
|
||||
try {
|
||||
await updateNetworkInterface(this.hass, this._device!.interface, options);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to change network settings",
|
||||
text:
|
||||
typeof err === "object" ? err.body.message || "Unkown error" : err,
|
||||
});
|
||||
this._prosessing = false;
|
||||
return;
|
||||
}
|
||||
this._params?.loadData();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
|
||||
if (this._dirty) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
text:
|
||||
"You have unsaved changes, these will get lost if you change tabs, do you want to continue?",
|
||||
confirmText: "yes",
|
||||
dismissText: "no",
|
||||
});
|
||||
if (!confirm) {
|
||||
this.requestUpdate("_device");
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._curTabIndex = ev.detail.index;
|
||||
this._device = this._network[ev.detail.index];
|
||||
this._device.data.nameservers = String(this._device.data.nameservers);
|
||||
}
|
||||
|
||||
private _handleRadioValueChanged(ev: CustomEvent): void {
|
||||
const value = (ev.target as HaRadio).value as "dhcp" | "static";
|
||||
|
||||
if (!value || !this._device || this._device!.data.method === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dirty = true;
|
||||
|
||||
this._device!.data.method = value;
|
||||
this.requestUpdate("_device");
|
||||
}
|
||||
|
||||
private _handleInputValueChanged(ev: CustomEvent): void {
|
||||
const value: string | null | undefined = (ev.target as PaperInputElement)
|
||||
.value;
|
||||
const id = (ev.target as PaperInputElement).id;
|
||||
|
||||
if (!value || !this._device || this._device.data[id] === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dirty = true;
|
||||
|
||||
this._device.data[id] = value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
mwc-tab-bar {
|
||||
border-bottom: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--dialog-content-position: static;
|
||||
--dialog-content-padding: 0;
|
||||
--dialog-z-index: 6;
|
||||
}
|
||||
|
||||
@media all and (min-width: 451px) and (min-height: 501px) {
|
||||
.container {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: block;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-header-bar {
|
||||
--mdc-theme-primary: var(--app-header-background-color);
|
||||
--mdc-theme-on-primary: var(--app-header-text-color, white);
|
||||
}
|
||||
}
|
||||
|
||||
mwc-button.warning {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
|
||||
:host([rtl]) app-toolbar {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
.container {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
.form {
|
||||
margin-bottom: 53px;
|
||||
}
|
||||
.buttons {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 8px);
|
||||
background-color: var(--mdc-theme-surface, #fff);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-network": DialogHassioNetwork;
|
||||
}
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { NetworkInfo } from "../../../../src/data/hassio/network";
|
||||
import "./dialog-hassio-network";
|
||||
|
||||
export interface HassioNetworkDialogParams {
|
||||
network: NetworkInfo;
|
||||
loadData: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const showNetworkDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioNetworkDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-network",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-network" */ "./dialog-hassio-network"
|
||||
),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -7,9 +7,9 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
fetchHassioSnapshotInfo,
|
||||
HassioSnapshotDetail,
|
||||
} from "../../../../src/data/hassio/snapshot";
|
||||
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { PolymerChangedEvent } from "../../../../src/polymer-types";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
@@ -267,12 +266,8 @@ class HassioSnapshotDialog extends LitElement {
|
||||
this._snapshotPassword = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _partialRestoreClicked() {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: "Are you sure you want partially to restore this snapshot?",
|
||||
}))
|
||||
) {
|
||||
private _partialRestoreClicked() {
|
||||
if (!confirm("Are you sure you want to restore this snapshot?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -317,13 +312,8 @@ class HassioSnapshotDialog extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private async _fullRestoreClicked() {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title:
|
||||
"Are you sure you want to wipe your system and restore this snapshot?",
|
||||
}))
|
||||
) {
|
||||
private _fullRestoreClicked() {
|
||||
if (!confirm("Are you sure you want to restore this snapshot?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -348,12 +338,8 @@ class HassioSnapshotDialog extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private async _deleteClicked() {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: "Are you sure you want to delete this snapshot?",
|
||||
}))
|
||||
) {
|
||||
private _deleteClicked() {
|
||||
if (!confirm("Are you sure you want to delete this snapshot?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -106,9 +106,7 @@ export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) {
|
||||
};
|
||||
}
|
||||
} else {
|
||||
themeName =
|
||||
((this.hass.selectedTheme as unknown) as string) ||
|
||||
this.hass.themes.default_theme;
|
||||
themeName = (this.hass.selectedTheme as unknown) as string;
|
||||
}
|
||||
|
||||
applyThemesOnElement(
|
||||
|
@@ -1,23 +1,18 @@
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { safeDump } from "js-yaml";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
import "../../../src/components/buttons/ha-call-api-button";
|
||||
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
|
||||
import {
|
||||
changeHostOptions,
|
||||
configSyncOS,
|
||||
fetchHassioHostInfo,
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo as HassioHostInfoType,
|
||||
@@ -25,26 +20,16 @@ import {
|
||||
shutdownHost,
|
||||
updateOS,
|
||||
} from "../../../src/data/hassio/host";
|
||||
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
|
||||
import {
|
||||
fetchNetworkInfo,
|
||||
NetworkInfo,
|
||||
} from "../../../src/data/hassio/network";
|
||||
import { HassioInfo } from "../../../src/data/hassio/supervisor";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
||||
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
|
||||
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-host-info")
|
||||
class HassioHostInfo extends LitElement {
|
||||
@@ -56,130 +41,86 @@ class HassioHostInfo extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
@internalProperty() public _networkInfo?: NetworkInfo;
|
||||
@internalProperty() private _errors?: string;
|
||||
|
||||
public render(): TemplateResult | void {
|
||||
const primaryIpAddress = this.hostInfo.features.includes("network")
|
||||
? this._primaryIpAddress(this._networkInfo!)
|
||||
: "";
|
||||
return html`
|
||||
<ha-card header="Host System">
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<h2>Host system</h2>
|
||||
<table class="info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Hostname</td>
|
||||
<td>${this.hostInfo.hostname}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>System</td>
|
||||
<td>${this.hostInfo.operating_system}</td>
|
||||
</tr>
|
||||
${!this.hostInfo.features.includes("hassos")
|
||||
? html`<tr>
|
||||
<td>Docker version</td>
|
||||
<td>${this.hassioInfo.docker}</td>
|
||||
</tr>`
|
||||
: ""}
|
||||
${this.hostInfo.deployment
|
||||
? html`
|
||||
<tr>
|
||||
<td>Deployment</td>
|
||||
<td>${this.hostInfo.deployment}</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
</tbody>
|
||||
</table>
|
||||
<mwc-button raised @click=${this._showHardware} class="info">
|
||||
Hardware
|
||||
</mwc-button>
|
||||
${this.hostInfo.features.includes("hostname")
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Hostname
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hostInfo.hostname}
|
||||
</span>
|
||||
? html`
|
||||
<mwc-button
|
||||
title="Change the hostname"
|
||||
label="Change"
|
||||
raised
|
||||
@click=${this._changeHostnameClicked}
|
||||
class="info"
|
||||
>
|
||||
Change hostname
|
||||
</mwc-button>
|
||||
</ha-settings-row>`
|
||||
`
|
||||
: ""}
|
||||
${this.hostInfo.features.includes("network")
|
||||
? html` <ha-settings-row>
|
||||
<span slot="heading">
|
||||
IP address
|
||||
</span>
|
||||
<span slot="description">
|
||||
${primaryIpAddress}
|
||||
</span>
|
||||
<mwc-button
|
||||
title="Change the network"
|
||||
label="Change"
|
||||
@click=${this._changeNetworkClicked}
|
||||
>
|
||||
</mwc-button>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Operating system
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hostInfo.operating_system}
|
||||
</span>
|
||||
${this.hostInfo.version !== this.hostInfo.version_latest &&
|
||||
this.hostInfo.features.includes("hassos")
|
||||
? html`
|
||||
<mwc-button
|
||||
title="Update the host OS"
|
||||
label="Update"
|
||||
@click=${this._osUpdate}
|
||||
>
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
${!this.hostInfo.features.includes("hassos")
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Docker version
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hassioInfo.docker}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
${this.hostInfo.deployment
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Deployment
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hostInfo.deployment}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
${this._errors
|
||||
? html` <div class="errors">Error: ${this._errors}</div> `
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
${this.hostInfo.features.includes("reboot")
|
||||
? html`
|
||||
<mwc-button
|
||||
title="Reboot the host OS"
|
||||
label="Reboot"
|
||||
class="warning"
|
||||
@click=${this._hostReboot}
|
||||
<mwc-button class="warning" @click=${this._rebootHost}
|
||||
>Reboot</mwc-button
|
||||
>
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
${this.hostInfo.features.includes("shutdown")
|
||||
? html`
|
||||
<mwc-button
|
||||
title="Shutdown the host OS"
|
||||
label="Shutdown"
|
||||
class="warning"
|
||||
@click=${this._hostShutdown}
|
||||
<mwc-button class="warning" @click=${this._shutdownHost}
|
||||
>Shutdown</mwc-button
|
||||
>
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
@action=${this._handleMenuAction}
|
||||
>
|
||||
<mwc-icon-button slot="trigger">
|
||||
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-list-item title="Show a list of hardware">
|
||||
Hardware
|
||||
</mwc-list-item>
|
||||
${this.hostInfo.features.includes("hassos")
|
||||
? html`<mwc-list-item
|
||||
${this.hostInfo.features.includes("hassos")
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
path="hassio/os/config/sync"
|
||||
title="Load HassOS configs or updates from USB"
|
||||
>Import from USB</ha-call-api-button
|
||||
>
|
||||
Import from USB
|
||||
</mwc-list-item>`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
`
|
||||
: ""}
|
||||
${this.hostInfo.version !== this.hostInfo.version_latest
|
||||
? html` <mwc-button @click=${this._updateOS}>Update</mwc-button> `
|
||||
: ""}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -192,96 +133,72 @@ class HassioHostInfo extends LitElement {
|
||||
css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
.card-actions {
|
||||
height: 48px;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row[three-line] {
|
||||
height: 74px;
|
||||
.card-content {
|
||||
color: var(--primary-text-color);
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 47px);
|
||||
}
|
||||
ha-settings-row > span[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
.info {
|
||||
width: 100%;
|
||||
}
|
||||
.info td:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
.errors {
|
||||
color: var(--error-color);
|
||||
margin-top: 16px;
|
||||
}
|
||||
mwc-button.info {
|
||||
max-width: calc(50% - 12px);
|
||||
}
|
||||
table.info {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
|
||||
ha-button-menu {
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-menu-min-width: 200px;
|
||||
}
|
||||
@media (min-width: 563px) {
|
||||
paper-listbox {
|
||||
max-height: 150px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
min-height: 35px;
|
||||
}
|
||||
mwc-list-item ha-svg-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._loadData();
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
}
|
||||
|
||||
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
|
||||
if (!network_info) {
|
||||
return "";
|
||||
private _apiCalled(ev): void {
|
||||
if (ev.detail.success) {
|
||||
this._errors = undefined;
|
||||
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;
|
||||
}
|
||||
const response = ev.detail.response;
|
||||
|
||||
this._errors =
|
||||
typeof response.body === "object"
|
||||
? response.body.message || "Unknown error"
|
||||
: response.body;
|
||||
}
|
||||
|
||||
private async _showHardware(): Promise<void> {
|
||||
try {
|
||||
const content = await fetchHassioHardwareInfo(this.hass);
|
||||
const content = this._objectToMarkdown(
|
||||
await fetchHassioHardwareInfo(this.hass)
|
||||
);
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: "Hardware",
|
||||
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
|
||||
content,
|
||||
});
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to get Hardware list",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: "Hardware",
|
||||
content: "Error getting hardware info",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _hostReboot(): Promise<void> {
|
||||
private async _rebootHost(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Reboot",
|
||||
text: "Are you sure you want to reboot the host?",
|
||||
@@ -298,13 +215,12 @@ class HassioHostInfo extends LitElement {
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to reboot",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
text: err.body.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _hostShutdown(): Promise<void> {
|
||||
private async _shutdownHost(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Shutdown",
|
||||
text: "Are you sure you want to shutdown the host?",
|
||||
@@ -321,13 +237,12 @@ class HassioHostInfo extends LitElement {
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to shutdown",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
text: err.body.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _osUpdate(): Promise<void> {
|
||||
private async _updateOS(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Update",
|
||||
text: "Are you sure you want to update the OS?",
|
||||
@@ -344,17 +259,30 @@ class HassioHostInfo extends LitElement {
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to update",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
text: err.body.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _changeNetworkClicked(): Promise<void> {
|
||||
showNetworkDialog(this, {
|
||||
network: this._networkInfo!,
|
||||
loadData: () => this._loadData(),
|
||||
private _objectToMarkdown(obj, indent = ""): string {
|
||||
let data = "";
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (typeof obj[key] !== "object") {
|
||||
data += `${indent}- ${key}: ${obj[key]}\n`;
|
||||
} else {
|
||||
data += `${indent}- ${key}:\n`;
|
||||
if (Array.isArray(obj[key])) {
|
||||
if (obj[key].length) {
|
||||
data +=
|
||||
`${indent} - ` + obj[key].join(`\n${indent} - `) + "\n";
|
||||
}
|
||||
} else {
|
||||
data += this._objectToMarkdown(obj[key], ` ${indent}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private async _changeHostnameClicked(): Promise<void> {
|
||||
@@ -373,29 +301,11 @@ class HassioHostInfo extends LitElement {
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Setting hostname failed",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
text: err.body.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@@ -6,28 +6,26 @@ import {
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-call-api-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import {
|
||||
HassioSupervisorInfo as HassioSupervisorInfoType,
|
||||
reloadSupervisor,
|
||||
setSupervisorOption,
|
||||
SupervisorOptions,
|
||||
updateSupervisor,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-switch";
|
||||
import {
|
||||
showConfirmationDialog,
|
||||
showAlertDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-supervisor-info")
|
||||
class HassioSupervisorInfo extends LitElement {
|
||||
@@ -35,110 +33,90 @@ class HassioSupervisorInfo extends LitElement {
|
||||
|
||||
@property() public supervisorInfo!: HassioSupervisorInfoType;
|
||||
|
||||
@property() public hostInfo!: HassioHostInfoType;
|
||||
@internalProperty() private _errors?: string;
|
||||
|
||||
public render(): TemplateResult | void {
|
||||
return html`
|
||||
<ha-card header="Supervisor">
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Version
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisorInfo.version}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Newest version
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisorInfo.version_latest}
|
||||
</span>
|
||||
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
|
||||
? html`
|
||||
<mwc-button
|
||||
title="Update the supervisor"
|
||||
label="Update"
|
||||
@click=${this._supervisorUpdate}
|
||||
>
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Channel
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisorInfo.channel}
|
||||
</span>
|
||||
${this.supervisorInfo.channel === "beta"
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._toggleBeta}
|
||||
label="Leave beta channel"
|
||||
title="Get stable updates for Home Assistant, supervisor and host"
|
||||
>
|
||||
</mwc-button>
|
||||
`
|
||||
: this.supervisorInfo.channel === "stable"
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._toggleBeta}
|
||||
label="Join beta channel"
|
||||
title="Get beta updates for Home Assistant (RCs), supervisor and host"
|
||||
>
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
|
||||
${this.supervisorInfo?.supported
|
||||
? html` <ha-settings-row three-line>
|
||||
<span slot="heading">
|
||||
Share diagnostics
|
||||
</span>
|
||||
<div slot="description" class="diagnostics-description">
|
||||
Share crash reports and diagnostic information.
|
||||
<button
|
||||
class="link"
|
||||
title="Show more information about this"
|
||||
@click=${this._diagnosticsInformationDialog}
|
||||
>
|
||||
Learn more
|
||||
</button>
|
||||
</div>
|
||||
<ha-switch
|
||||
haptic
|
||||
.checked=${this.supervisorInfo.diagnostics}
|
||||
@change=${this._toggleDiagnostics}
|
||||
></ha-switch>
|
||||
</ha-settings-row>`
|
||||
: html`<div class="error">
|
||||
You are running an unsupported installation.
|
||||
<a
|
||||
href="https://github.com/home-assistant/architecture/blob/master/adr/${this.hostInfo.features.includes(
|
||||
"hassos"
|
||||
)
|
||||
? "0015-home-assistant-os.md"
|
||||
: "0014-home-assistant-supervised.md"}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Learn more about how you can make your system compliant"
|
||||
<h2>Supervisor</h2>
|
||||
<table class="info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td>${this.supervisorInfo.version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Latest version</td>
|
||||
<td>${this.supervisorInfo.version_latest}</td>
|
||||
</tr>
|
||||
${this.supervisorInfo.channel !== "stable"
|
||||
? html`
|
||||
<tr>
|
||||
<td>Channel</td>
|
||||
<td>${this.supervisorInfo.channel}</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="options">
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Share Diagnostics
|
||||
</span>
|
||||
<span slot="description">
|
||||
Share crash reports and diagnostic information.
|
||||
<button
|
||||
class="link"
|
||||
@click=${this._diagnosticsInformationDialog}
|
||||
>
|
||||
Learn More
|
||||
</a>
|
||||
</div>`}
|
||||
Learn more
|
||||
</button>
|
||||
</span>
|
||||
<ha-switch
|
||||
.checked=${this.supervisorInfo.diagnostics}
|
||||
@change=${this._toggleDiagnostics}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
${this._errors
|
||||
? html` <div class="errors">Error: ${this._errors}</div> `
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button
|
||||
@click=${this._supervisorReload}
|
||||
title="Reload parts of the supervisor."
|
||||
label="Reload"
|
||||
<ha-call-api-button .hass=${this.hass} path="hassio/supervisor/reload"
|
||||
>Reload</ha-call-api-button
|
||||
>
|
||||
</mwc-button>
|
||||
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
.hass=${this.hass}
|
||||
path="hassio/supervisor/update"
|
||||
>Update</ha-call-api-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
${this.supervisorInfo.channel === "beta"
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
.hass=${this.hass}
|
||||
path="hassio/supervisor/options"
|
||||
.data=${{ channel: "stable" }}
|
||||
>Leave beta channel</ha-call-api-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
${this.supervisorInfo.channel === "stable"
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._joinBeta}
|
||||
class="warning"
|
||||
title="Get beta updates for Home Assistant (RCs), supervisor and host"
|
||||
>Join beta channel</mwc-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -151,103 +129,92 @@ class HassioSupervisorInfo extends LitElement {
|
||||
css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.card-actions {
|
||||
height: 48px;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.card-content {
|
||||
color: var(--primary-text-color);
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 47px);
|
||||
}
|
||||
button.link {
|
||||
color: var(--primary-color);
|
||||
.info,
|
||||
.options {
|
||||
width: 100%;
|
||||
}
|
||||
.info td:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
.errors {
|
||||
color: var(--error-color);
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row[three-line] {
|
||||
height: 74px;
|
||||
}
|
||||
ha-settings-row > span[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
button.link {
|
||||
color: var(--primary-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",
|
||||
});
|
||||
protected firstUpdated(): void {
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
}
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
private _apiCalled(ev): void {
|
||||
if (ev.detail.success) {
|
||||
this._errors = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const response = ev.detail.response;
|
||||
|
||||
this._errors =
|
||||
typeof response.body === "object"
|
||||
? response.body.message || "Unknown error"
|
||||
: response.body;
|
||||
}
|
||||
|
||||
private async _joinBeta() {
|
||||
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",
|
||||
};
|
||||
const data: SupervisorOptions = { channel: "beta" };
|
||||
await setSupervisorOption(this.hass, data);
|
||||
await reloadSupervisor(this.hass);
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "option",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to set supervisor option",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
});
|
||||
this._errors = `Error joining beta channel, ${err.body?.message || 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> {
|
||||
private async _diagnosticsInformationDialog() {
|
||||
await showAlertDialog(this, {
|
||||
title: "Help Improve Home Assistant",
|
||||
text: html`Would you want to automatically share crash reports and
|
||||
@@ -257,23 +224,27 @@ class HassioSupervisorInfo extends LitElement {
|
||||
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
|
||||
The data does not include any private/sensetive information and you can
|
||||
disable this in settings at any time you want.`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _toggleDiagnostics(): Promise<void> {
|
||||
private async _toggleDiagnostics() {
|
||||
try {
|
||||
const data: SupervisorOptions = {
|
||||
diagnostics: !this.supervisorInfo?.diagnostics,
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "option",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to set supervisor option",
|
||||
text:
|
||||
typeof err === "object" ? err.body.message || "Unkown error" : err,
|
||||
});
|
||||
this._errors = `Error changing supervisor setting, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,20 +7,18 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
import "../../../src/components/ha-card";
|
||||
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,7 +102,7 @@ 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>
|
||||
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -116,7 +114,6 @@ class HassioSupervisorLog extends LitElement {
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
pre {
|
||||
@@ -130,6 +127,9 @@ class HassioSupervisorLog extends LitElement {
|
||||
color: var(--error-color);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-content {
|
||||
padding-top: 0px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@@ -142,6 +142,7 @@ class HassioSupervisorLog extends LitElement {
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._error = undefined;
|
||||
this._content = undefined;
|
||||
|
||||
try {
|
||||
this._content = await fetchHassioLogs(
|
||||
@@ -150,10 +151,14 @@ class HassioSupervisorLog extends LitElement {
|
||||
);
|
||||
} catch (err) {
|
||||
this._error = `Failed to get supervisor logs, ${
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async _refresh(): Promise<void> {
|
||||
await this._loadData();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -52,10 +52,10 @@ class HassioSystem extends LitElement {
|
||||
>
|
||||
<span slot="header">System</span>
|
||||
<div class="content">
|
||||
<h1>Information</h1>
|
||||
<div class="card-group">
|
||||
<hassio-supervisor-info
|
||||
.hass=${this.hass}
|
||||
.hostInfo=${this.hostInfo}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
></hassio-supervisor-info>
|
||||
<hassio-host-info
|
||||
@@ -65,6 +65,7 @@ class HassioSystem extends LitElement {
|
||||
.hassOsInfo=${this.hassOsInfo}
|
||||
></hassio-host-info>
|
||||
</div>
|
||||
<h1>System log</h1>
|
||||
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
|
20
package.json
20
package.json
@@ -23,11 +23,8 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-pluralrules": "^1.5.8",
|
||||
"@fullcalendar/common": "5.1.0",
|
||||
"@fullcalendar/core": "5.1.0",
|
||||
"@fullcalendar/daygrid": "5.1.0",
|
||||
"@fullcalendar/interaction": "5.1.0",
|
||||
"@fullcalendar/list": "5.1.0",
|
||||
"@fullcalendar/core": "^5.0.0-beta.2",
|
||||
"@fullcalendar/daygrid": "^5.0.0-beta.2",
|
||||
"@material/chips": "=8.0.0-canary.096a7a066.0",
|
||||
"@material/circular-progress": "=8.0.0-canary.a78ceb112.0",
|
||||
"@material/mwc-button": "^0.18.0",
|
||||
@@ -44,8 +41,8 @@
|
||||
"@material/mwc-tab": "^0.18.0",
|
||||
"@material/mwc-tab-bar": "^0.18.0",
|
||||
"@material/top-app-bar": "=8.0.0-canary.096a7a066.0",
|
||||
"@mdi/js": "5.5.55",
|
||||
"@mdi/svg": "5.5.55",
|
||||
"@mdi/js": "5.4.55",
|
||||
"@mdi/svg": "5.4.55",
|
||||
"@polymer/app-layout": "^3.0.2",
|
||||
"@polymer/app-route": "^3.0.2",
|
||||
"@polymer/app-storage": "^3.0.2",
|
||||
@@ -88,7 +85,6 @@
|
||||
"codemirror": "^5.49.0",
|
||||
"comlink": "^4.3.0",
|
||||
"cpx": "^1.5.0",
|
||||
"cropperjs": "^1.5.7",
|
||||
"deep-clone-simple": "^1.1.1",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"es6-object-assign": "^1.1.0",
|
||||
@@ -149,7 +145,7 @@
|
||||
"@types/leaflet-draw": "^1.0.1",
|
||||
"@types/marked": "^1.1.0",
|
||||
"@types/memoize-one": "4.1.0",
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/resize-observer-browser": "^0.1.3",
|
||||
"@types/webspeechapi": "^0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^2.28.0",
|
||||
@@ -160,7 +156,7 @@
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb-typescript": "^7.2.1",
|
||||
"eslint-config-prettier": "^6.10.1",
|
||||
"eslint-import-resolver-webpack": "^0.12.2",
|
||||
"eslint-import-resolver-webpack": "^0.12.1",
|
||||
"eslint-plugin-disable": "^2.0.1",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-lit": "^1.2.0",
|
||||
@@ -183,7 +179,7 @@
|
||||
"magic-string": "^0.25.7",
|
||||
"map-stream": "^0.0.7",
|
||||
"merge-stream": "^1.0.1",
|
||||
"mocha": "^7.2.0",
|
||||
"mocha": "^6.0.2",
|
||||
"object-hash": "^2.0.3",
|
||||
"open": "^7.0.4",
|
||||
"prettier": "^2.0.4",
|
||||
@@ -200,7 +196,7 @@
|
||||
"systemjs": "^6.3.2",
|
||||
"terser-webpack-plugin": "^3.0.6",
|
||||
"ts-lit-plugin": "^1.2.0",
|
||||
"ts-mocha": "^7.0.0",
|
||||
"ts-mocha": "^6.0.0",
|
||||
"typescript": "^3.8.3",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20200824.0",
|
||||
version="20200807.1",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -1,9 +0,0 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
/** Return if a service is loaded. */
|
||||
export const isServiceLoaded = (
|
||||
hass: HomeAssistant,
|
||||
domain: string,
|
||||
service: string
|
||||
): boolean =>
|
||||
hass && domain in hass.services && service in hass.services[domain];
|
@@ -21,11 +21,6 @@ export default function relativeTime(
|
||||
const tense = delta >= 0 ? "past" : "future";
|
||||
delta = Math.abs(delta);
|
||||
let roundedDelta = Math.round(delta);
|
||||
|
||||
if (roundedDelta === 0) {
|
||||
return localize("ui.components.relative_time.just_now");
|
||||
}
|
||||
|
||||
let unit = "week";
|
||||
|
||||
for (let i = 0; i < tests.length; i++) {
|
||||
|
@@ -5,16 +5,12 @@ import { domainIcon } from "./domain_icon";
|
||||
import { batteryIcon } from "./battery_icon";
|
||||
|
||||
const fixedDeviceClassIcons = {
|
||||
current: "hass:current-ac",
|
||||
energy: "hass:flash",
|
||||
humidity: "hass:water-percent",
|
||||
illuminance: "hass:brightness-5",
|
||||
temperature: "hass:thermometer",
|
||||
pressure: "hass:gauge",
|
||||
power: "hass:flash",
|
||||
power_factor: "hass:angle-acute",
|
||||
signal_strength: "hass:wifi",
|
||||
voltage: "hass:sine-wave",
|
||||
};
|
||||
|
||||
export const sensorIcon = (state: HassEntity) => {
|
||||
|
@@ -3,21 +3,19 @@ import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
eventOptions,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
eventOptions,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { scroll } from "lit-virtualizer";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../common/search/search-input";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
@@ -26,6 +24,8 @@ import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-icon";
|
||||
import { filterData, sortData } from "./sort-filter";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -70,7 +70,6 @@ export interface DataTableColumnData extends DataTableSortColumnData {
|
||||
maxWidth?: string;
|
||||
grows?: boolean;
|
||||
forceLTR?: boolean;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export interface DataTableRowData {
|
||||
@@ -215,15 +214,13 @@ export class HaDataTable extends LitElement {
|
||||
class="mdc-data-table__table ${classMap({
|
||||
"auto-height": this.autoHeight,
|
||||
})}"
|
||||
role="table"
|
||||
aria-rowcount=${this._filteredData.length}
|
||||
style=${styleMap({
|
||||
height: this.autoHeight
|
||||
? `${(this._filteredData.length || 1) * 53 + 57}px`
|
||||
: `calc(100% - ${this._header?.clientHeight}px)`,
|
||||
})}
|
||||
>
|
||||
<div class="mdc-data-table__header-row" role="row">
|
||||
<div class="mdc-data-table__header-row">
|
||||
${this.selectable
|
||||
? html`
|
||||
<div
|
||||
@@ -243,10 +240,8 @@ export class HaDataTable extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(this.columns).map(([key, column]) => {
|
||||
if (column.hidden) {
|
||||
return "";
|
||||
}
|
||||
${Object.entries(this.columns).map((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
const sorted = key === this._sortColumn;
|
||||
const classes = {
|
||||
"mdc-data-table__header-cell--numeric": Boolean(
|
||||
@@ -293,8 +288,8 @@ export class HaDataTable extends LitElement {
|
||||
${!this._filteredData.length
|
||||
? html`
|
||||
<div class="mdc-data-table__content">
|
||||
<div class="mdc-data-table__row" role="row">
|
||||
<div class="mdc-data-table__cell grows center" role="cell">
|
||||
<div class="mdc-data-table__row">
|
||||
<div class="mdc-data-table__cell grows center">
|
||||
${this.noDataText || "No data"}
|
||||
</div>
|
||||
</div>
|
||||
@@ -309,14 +304,12 @@ export class HaDataTable extends LitElement {
|
||||
items: !this.hasFab
|
||||
? this._filteredData
|
||||
: [...this._filteredData, ...[{ empty: true }]],
|
||||
renderItem: (row: DataTableRowData, index) => {
|
||||
renderItem: (row: DataTableRowData) => {
|
||||
if (row.empty) {
|
||||
return html` <div class="mdc-data-table__row"></div> `;
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
aria-rowindex=${index}
|
||||
role="row"
|
||||
.rowId="${row[this.id]}"
|
||||
@click=${this._handleRowClick}
|
||||
class="mdc-data-table__row ${classMap({
|
||||
@@ -335,7 +328,6 @@ export class HaDataTable extends LitElement {
|
||||
? html`
|
||||
<div
|
||||
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
|
||||
role="cell"
|
||||
>
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@@ -349,45 +341,40 @@ export class HaDataTable extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(this.columns).map(
|
||||
([key, column]) => {
|
||||
if (column.hidden) {
|
||||
return "";
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
role="cell"
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"mdc-data-table__cell--numeric": Boolean(
|
||||
column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__cell--icon": Boolean(
|
||||
column.type === "icon"
|
||||
),
|
||||
"mdc-data-table__cell--icon-button": Boolean(
|
||||
column.type === "icon-button"
|
||||
),
|
||||
grows: Boolean(column.grows),
|
||||
forceLTR: Boolean(column.forceLTR),
|
||||
})}"
|
||||
style=${column.width
|
||||
? styleMap({
|
||||
[column.grows
|
||||
? "minWidth"
|
||||
: "width"]: column.width,
|
||||
maxWidth: column.maxWidth
|
||||
? column.maxWidth
|
||||
: "",
|
||||
})
|
||||
: ""}
|
||||
>
|
||||
${column.template
|
||||
? column.template(row[key], row)
|
||||
: row[key]}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${Object.entries(this.columns).map((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
return html`
|
||||
<div
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"mdc-data-table__cell--numeric": Boolean(
|
||||
column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__cell--icon": Boolean(
|
||||
column.type === "icon"
|
||||
),
|
||||
"mdc-data-table__cell--icon-button": Boolean(
|
||||
column.type === "icon-button"
|
||||
),
|
||||
grows: Boolean(column.grows),
|
||||
forceLTR: Boolean(column.forceLTR),
|
||||
})}"
|
||||
style=${column.width
|
||||
? styleMap({
|
||||
[column.grows
|
||||
? "minWidth"
|
||||
: "width"]: column.width,
|
||||
maxWidth: column.maxWidth
|
||||
? column.maxWidth
|
||||
: "",
|
||||
})
|
||||
: ""}
|
||||
>
|
||||
${column.template
|
||||
? column.template(row[key], row)
|
||||
: row[key]}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
@@ -1,11 +1,11 @@
|
||||
// To use comlink under ES5
|
||||
import { expose } from "comlink";
|
||||
import "proxy-polyfill";
|
||||
import { expose } from "comlink";
|
||||
import type {
|
||||
DataTableRowData,
|
||||
DataTableSortColumnData,
|
||||
SortableColumnContainer,
|
||||
DataTableRowData,
|
||||
SortingDirection,
|
||||
SortableColumnContainer,
|
||||
} from "./ha-data-table";
|
||||
|
||||
const filterData = (
|
||||
@@ -19,7 +19,7 @@ const filterData = (
|
||||
const [key, column] = columnEntry;
|
||||
if (column.filterable) {
|
||||
if (
|
||||
String(column.filterKey ? row[key][column.filterKey] : row[key])
|
||||
(column.filterKey ? row[key][column.filterKey] : row[key])
|
||||
.toUpperCase()
|
||||
.includes(filter)
|
||||
) {
|
||||
|
@@ -1,12 +1,12 @@
|
||||
/* eslint-plugin-disable lit */
|
||||
import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior";
|
||||
import "../ha-icon-button";
|
||||
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
|
||||
import { timeOut } from "@polymer/polymer/lib/utils/async";
|
||||
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { formatTime } from "../../common/datetime/format_time";
|
||||
import "../ha-icon-button";
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
/* global Chart moment Color */
|
||||
@@ -355,7 +355,7 @@ class HaChartBase extends mixinBehaviors(
|
||||
return value;
|
||||
}
|
||||
const date = new Date(values[index].value);
|
||||
return formatTime(date, this.hass.language);
|
||||
return formatTime(date);
|
||||
}
|
||||
|
||||
drawChart() {
|
||||
@@ -420,7 +420,7 @@ class HaChartBase extends mixinBehaviors(
|
||||
},
|
||||
};
|
||||
options = Chart.helpers.merge(options, this.data.options);
|
||||
options.scales.xAxes[0].ticks.callback = this._formatTickValue.bind(this);
|
||||
options.scales.xAxes[0].ticks.callback = this._formatTickValue;
|
||||
if (this.data.type === "timeline") {
|
||||
this.set("isTimeline", true);
|
||||
if (this.data.colors !== undefined) {
|
||||
|
@@ -1,67 +0,0 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
LitElement,
|
||||
property,
|
||||
svg,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
import {
|
||||
getValueInPercentage,
|
||||
normalize,
|
||||
roundWithOneDecimal,
|
||||
} from "../util/calculate";
|
||||
|
||||
@customElement("ha-bar")
|
||||
export class HaBar extends LitElement {
|
||||
@property({ type: Number }) public min = 0;
|
||||
|
||||
@property({ type: Number }) public max = 100;
|
||||
|
||||
@property({ type: Number }) public value!: number;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const valuePrecentage = roundWithOneDecimal(
|
||||
getValueInPercentage(
|
||||
normalize(this.value, this.min, this.max),
|
||||
this.min,
|
||||
this.max
|
||||
)
|
||||
);
|
||||
|
||||
return svg`
|
||||
<svg>
|
||||
<g>
|
||||
<rect></rect>
|
||||
<rect width="${valuePrecentage}%"></rect>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
rect:first-child {
|
||||
width: 100%;
|
||||
fill: var(--ha-bar-background-color, var(--secondary-background-color));
|
||||
}
|
||||
rect:last-child {
|
||||
fill: var(--ha-bar-primary-color, var(--primary-color));
|
||||
rx: var(--ha-bar-border-radius, 4px);
|
||||
}
|
||||
svg {
|
||||
border-radius: var(--ha-bar-border-radius, 4px);
|
||||
height: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-bar": HaBar;
|
||||
}
|
||||
}
|
@@ -1,20 +1,21 @@
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
property,
|
||||
LitElement,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
|
||||
import "./ha-icon-button";
|
||||
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { ToggleButton } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-button-toggle-group")
|
||||
export class HaButtonToggleGroup extends LitElement {
|
||||
@property({ attribute: false }) public buttons!: ToggleButton[];
|
||||
@property() public buttons!: ToggleButton[];
|
||||
|
||||
@property() public active?: string;
|
||||
|
||||
@@ -22,23 +23,21 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
return html`
|
||||
<div>
|
||||
${this.buttons.map(
|
||||
(button) => html`
|
||||
<mwc-icon-button
|
||||
.label=${button.label}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
(button) => html` <ha-icon-button
|
||||
.label=${button.label}
|
||||
.icon=${button.icon}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
</ha-icon-button>`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick(ev): void {
|
||||
this.active = ev.currentTarget.value;
|
||||
this.active = ev.target.value;
|
||||
fireEvent(this, "value-changed", { value: this.active });
|
||||
}
|
||||
|
||||
@@ -49,13 +48,12 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
--mdc-icon-button-size: var(--button-toggle-size, 36px);
|
||||
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
|
||||
}
|
||||
mwc-icon-button {
|
||||
ha-icon-button {
|
||||
border: 1px solid var(--primary-color);
|
||||
border-right-width: 0px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
mwc-icon-button::before {
|
||||
ha-icon-button::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
@@ -67,26 +65,22 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
content: "";
|
||||
transition: opacity 15ms linear, background-color 15ms linear;
|
||||
}
|
||||
mwc-icon-button[active]::before {
|
||||
ha-icon-button[active]::before {
|
||||
opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
|
||||
}
|
||||
mwc-icon-button:first-child {
|
||||
ha-icon-button:first-child {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
mwc-icon-button:last-child {
|
||||
ha-icon-button:last-child {
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
mwc-icon-button:only-child {
|
||||
border-radius: 4px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-button-toggle-group": HaButtonToggleGroup;
|
||||
"ha-button-toggle-button": HaButtonToggleGroup;
|
||||
}
|
||||
}
|
||||
|
@@ -12,8 +12,6 @@ import {
|
||||
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,
|
||||
@@ -39,8 +37,6 @@ class HaCameraStream extends LitElement {
|
||||
|
||||
private _hlsPolyfillInstance?: Hls;
|
||||
|
||||
private _useExoPlayer = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._attached = true;
|
||||
@@ -129,33 +125,22 @@ class HaCameraStream extends LitElement {
|
||||
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 Hls = ((await import(
|
||||
/* webpackChunkName: "hls.js" */ "hls.js"
|
||||
)) as any).default as HLSModule;
|
||||
let hlsSupported = Hls.isSupported();
|
||||
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) {
|
||||
hlsSupported =
|
||||
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
|
||||
}
|
||||
|
||||
if (!hlsSupported) {
|
||||
this._forceMJPEG = this.stateObj!.entity_id;
|
||||
return;
|
||||
}
|
||||
if (!hlsSupported) {
|
||||
this._forceMJPEG = this.stateObj!.entity_id;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -164,10 +149,8 @@ class HaCameraStream extends LitElement {
|
||||
this.stateObj!.entity_id
|
||||
);
|
||||
|
||||
if (this._useExoPlayer) {
|
||||
this._renderHLSExoPlayer(url);
|
||||
} else if (hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, hls, url);
|
||||
if (Hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, Hls, url);
|
||||
} else {
|
||||
this._renderHLSNative(videoEl, url);
|
||||
}
|
||||
@@ -180,29 +163,6 @@ class HaCameraStream extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
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) =>
|
||||
@@ -234,15 +194,11 @@ class HaCameraStream extends LitElement {
|
||||
fireEvent(this, "iron-resize");
|
||||
}
|
||||
|
||||
private _destroyPolyfill() {
|
||||
private _destroyPolyfill(): void {
|
||||
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 {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { Editor } from "codemirror";
|
||||
import {
|
||||
customElement,
|
||||
internalProperty,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
UpdatingElement,
|
||||
} from "lit-element";
|
||||
@@ -101,6 +101,11 @@ export class HaCodeEditor extends UpdatingElement {
|
||||
.CodeMirror-scroll {
|
||||
max-height: var(--code-mirror-max-height, --code-mirror-height);
|
||||
}
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
|
||||
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
|
||||
transition: 0.2s ease border-right;
|
||||
}
|
||||
:host(.error-state) .CodeMirror-gutters {
|
||||
border-color: var(--error-state-color, red);
|
||||
}
|
||||
@@ -108,7 +113,7 @@ export class HaCodeEditor extends UpdatingElement {
|
||||
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
|
||||
}
|
||||
.CodeMirror-linenumber {
|
||||
color: var(--paper-dialog-color, var(--secondary-text-color));
|
||||
color: var(--paper-dialog-color, var(--primary-text-color));
|
||||
}
|
||||
.rtl .CodeMirror-vscrollbar {
|
||||
right: auto;
|
||||
@@ -117,100 +122,6 @@ export class HaCodeEditor extends UpdatingElement {
|
||||
.rtl-gutter {
|
||||
width: 20px;
|
||||
}
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
|
||||
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
|
||||
transition: 0.2s ease border-right;
|
||||
}
|
||||
.cm-s-default.CodeMirror {
|
||||
background-color: var(--code-editor-background-color, var(--card-background-color));
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.cm-s-default .CodeMirror-cursor {
|
||||
border-left: 1px solid var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.cm-s-default div.CodeMirror-selected, .cm-s-default.CodeMirror-focused div.CodeMirror-selected {
|
||||
background: rgba(var(--rgb-primary-color), 0.2);
|
||||
}
|
||||
|
||||
.cm-s-default .CodeMirror-line::selection,
|
||||
.cm-s-default .CodeMirror-line>span::selection,
|
||||
.cm-s-default .CodeMirror-line>span>span::selection {
|
||||
background: rgba(var(--rgb-primary-color), 0.2);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-keyword {
|
||||
color: var(--codemirror-keyword, #6262FF);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-operator {
|
||||
color: var(--codemirror-operator, #cda869);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-variable-2 {
|
||||
color: var(--codemirror-variable-2, #690);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-builtin {
|
||||
color: var(--codemirror-builtin, #9B7536);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-atom {
|
||||
color: var(--codemirror-atom, #F90);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-number {
|
||||
color: var(--codemirror-number, #ca7841);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-def {
|
||||
color: var(--codemirror-def, #8DA6CE);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-string {
|
||||
color: var(--codemirror-string, #07a);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-string-2 {
|
||||
color: var(--codemirror-string-2, #bd6b18);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-comment {
|
||||
color: var(--codemirror-comment, #777);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-variable {
|
||||
color: var(--codemirror-variable, #07a);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-tag {
|
||||
color: var(--codemirror-tag, #997643);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-meta {
|
||||
color: var(--codemirror-meta, #000);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-attribute {
|
||||
color: var(--codemirror-attribute, #d6bb6d);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-property {
|
||||
color: var(--codemirror-property, #905);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-qualifier {
|
||||
color: var(--codemirror-qualifier, #690);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-variable-3 {
|
||||
color: var(--codemirror-variable-3, #07a);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-type {
|
||||
color: var(--codemirror-type, #07a);
|
||||
}
|
||||
</style>`;
|
||||
|
||||
this.codemirror = codeMirror(shadowRoot, {
|
||||
|
@@ -37,6 +37,24 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
if (this.schema.name.includes("password")) {
|
||||
const stepInput = document.createElement("input");
|
||||
stepInput.setAttribute("type", "password");
|
||||
stepInput.setAttribute("name", "password");
|
||||
stepInput.setAttribute("autocomplete", "on");
|
||||
stepInput.onkeyup = (ev) => this._externalValueChanged(ev, this);
|
||||
document.documentElement.appendChild(stepInput);
|
||||
} else if (this.schema.name.includes("username")) {
|
||||
const stepInput = document.createElement("input");
|
||||
stepInput.setAttribute("type", "text");
|
||||
stepInput.setAttribute("name", "username");
|
||||
stepInput.setAttribute("autocomplete", "on");
|
||||
stepInput.onkeyup = (ev) => this._externalValueChanged(ev, this);
|
||||
document.documentElement.appendChild(stepInput);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this.schema.name.includes("password")
|
||||
? html`
|
||||
@@ -81,11 +99,21 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
if (this.data === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
private _externalValueChanged(ev: Event, el): void {
|
||||
const value = (ev.target as PaperInputElement).value;
|
||||
if (this.data === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.shadowRoot!.querySelector("paper-input").value = value;
|
||||
}
|
||||
|
||||
private get _stringType(): string {
|
||||
if (this.schema.format) {
|
||||
if (["email", "url"].includes(this.schema.format)) {
|
||||
|
@@ -11,13 +11,23 @@ import { styleMap } from "lit-html/directives/style-map";
|
||||
import { afterNextRender } from "../common/util/render-status";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
import { getValueInPercentage, normalize } from "../util/calculate";
|
||||
|
||||
const getAngle = (value: number, min: number, max: number) => {
|
||||
const percentage = getValueInPercentage(normalize(value, min, max), min, max);
|
||||
return (percentage * 180) / 100;
|
||||
};
|
||||
|
||||
const normalize = (value: number, min: number, max: number) => {
|
||||
if (value > max) return max;
|
||||
if (value < min) return min;
|
||||
return value;
|
||||
};
|
||||
|
||||
const getValueInPercentage = (value: number, min: number, max: number) => {
|
||||
const newMax = max - min;
|
||||
const newVal = value - min;
|
||||
return (100 * newVal) / newMax;
|
||||
};
|
||||
|
||||
// Workaround for https://github.com/home-assistant/frontend/issues/6467
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
|
||||
|
@@ -106,7 +106,6 @@ const mdiRenameMapping = {
|
||||
pot: "pot-steam",
|
||||
ruby: "language-ruby",
|
||||
sailing: "sail-boat",
|
||||
scooter: "human-scooter",
|
||||
settings: "cog",
|
||||
"settings-box": "cog-box",
|
||||
"settings-outline": "cog-outline",
|
||||
|
@@ -1,226 +0,0 @@
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { mdiClose, mdiImagePlus } from "@mdi/js";
|
||||
import "@polymer/iron-input/iron-input";
|
||||
import "@polymer/paper-input/paper-input-container";
|
||||
import {
|
||||
css,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { createImage, generateImageThumbnailUrl } from "../data/image";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-circular-progress";
|
||||
import "./ha-svg-icon";
|
||||
import {
|
||||
showImageCropperDialog,
|
||||
CropOptions,
|
||||
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
|
||||
@customElement("ha-picture-upload")
|
||||
export class HaPictureUpload extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property() public value: string | null = null;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) public crop = false;
|
||||
|
||||
@property({ attribute: false }) public cropOptions?: CropOptions;
|
||||
|
||||
@property({ type: Number }) public size = 512;
|
||||
|
||||
@internalProperty() private _error = "";
|
||||
|
||||
@internalProperty() private _uploading = false;
|
||||
|
||||
@internalProperty() private _drag = false;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("_drag")) {
|
||||
(this.shadowRoot!.querySelector(
|
||||
"paper-input-container"
|
||||
) as any)._setFocused(this._drag);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${this._uploading
|
||||
? html`<ha-circular-progress
|
||||
alt="Uploading"
|
||||
size="large"
|
||||
active
|
||||
></ha-circular-progress>`
|
||||
: html`
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
||||
<label for="input">
|
||||
<paper-input-container
|
||||
.alwaysFloatLabel=${Boolean(this.value)}
|
||||
@drop=${this._handleDrop}
|
||||
@dragenter=${this._handleDragStart}
|
||||
@dragover=${this._handleDragStart}
|
||||
@dragleave=${this._handleDragEnd}
|
||||
@dragend=${this._handleDragEnd}
|
||||
class=${classMap({
|
||||
dragged: this._drag,
|
||||
})}
|
||||
>
|
||||
<label for="input" slot="label">
|
||||
${this.label ||
|
||||
this.hass.localize("ui.components.picture-upload.label")}
|
||||
</label>
|
||||
<iron-input slot="input">
|
||||
<input
|
||||
id="input"
|
||||
type="file"
|
||||
class="file"
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
@change=${this._handleFilePicked}
|
||||
/>
|
||||
${this.value ? html`<img .src=${this.value} />` : ""}
|
||||
</iron-input>
|
||||
${this.value
|
||||
? html`<mwc-icon-button
|
||||
slot="suffix"
|
||||
@click=${this._clearPicture}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>`
|
||||
: html`<mwc-icon-button slot="suffix">
|
||||
<ha-svg-icon .path=${mdiImagePlus}></ha-svg-icon>
|
||||
</mwc-icon-button>`}
|
||||
</paper-input-container>
|
||||
</label>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleDrop(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (ev.dataTransfer?.files) {
|
||||
if (this.crop) {
|
||||
this._cropFile(ev.dataTransfer.files[0]);
|
||||
} else {
|
||||
this._uploadFile(ev.dataTransfer.files[0]);
|
||||
}
|
||||
}
|
||||
this._drag = false;
|
||||
}
|
||||
|
||||
private _handleDragStart(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._drag = true;
|
||||
}
|
||||
|
||||
private _handleDragEnd(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._drag = false;
|
||||
}
|
||||
|
||||
private async _handleFilePicked(ev) {
|
||||
if (this.crop) {
|
||||
this._cropFile(ev.target.files[0]);
|
||||
} else {
|
||||
this._uploadFile(ev.target.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private async _cropFile(file: File) {
|
||||
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.components.picture-upload.unsupported_format"
|
||||
);
|
||||
return;
|
||||
}
|
||||
showImageCropperDialog(this, {
|
||||
file,
|
||||
options: this.cropOptions || {
|
||||
round: false,
|
||||
aspectRatio: NaN,
|
||||
},
|
||||
croppedCallback: (croppedFile) => {
|
||||
this._uploadFile(croppedFile);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _uploadFile(file: File) {
|
||||
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.components.picture-upload.unsupported_format"
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._uploading = true;
|
||||
this._error = "";
|
||||
try {
|
||||
const media = await createImage(this.hass, file);
|
||||
this.value = generateImageThumbnailUrl(media.id, this.size);
|
||||
fireEvent(this, "change");
|
||||
} catch (err) {
|
||||
this._error = err.toString();
|
||||
} finally {
|
||||
this._uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _clearPicture(ev: Event) {
|
||||
ev.preventDefault();
|
||||
this.value = null;
|
||||
this._error = "";
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
paper-input-container {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
margin: 0 -8px;
|
||||
}
|
||||
paper-input-container.dragged:before {
|
||||
position: var(--layout-fit_-_position);
|
||||
top: var(--layout-fit_-_top);
|
||||
right: var(--layout-fit_-_right);
|
||||
bottom: var(--layout-fit_-_bottom);
|
||||
left: var(--layout-fit_-_left);
|
||||
background: currentColor;
|
||||
content: "";
|
||||
opacity: var(--dark-divider-opacity);
|
||||
pointer-events: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
img {
|
||||
max-width: 125px;
|
||||
max-height: 125px;
|
||||
}
|
||||
input.file {
|
||||
display: none;
|
||||
}
|
||||
mwc-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-picture-upload": HaPictureUpload;
|
||||
}
|
||||
}
|
@@ -25,7 +25,7 @@ export class HaSettingsRow extends LitElement {
|
||||
</style>
|
||||
<paper-item-body
|
||||
?two-line=${!this.threeLine}
|
||||
?three-line=${this.threeLine}
|
||||
?three-line=${!this.threeLine}
|
||||
>
|
||||
<slot name="heading"></slot>
|
||||
<div secondary><slot name="description"></slot></div>
|
||||
|
@@ -21,8 +21,8 @@ import {
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
LeafletModuleType,
|
||||
replaceTileLayer,
|
||||
setupLeafletMap,
|
||||
replaceTileLayer,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { defaultRadiusColor } from "../../data/zone";
|
||||
@@ -40,8 +40,6 @@ class LocationEditor extends LitElement {
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@property({ type: Boolean }) public darkMode?: boolean;
|
||||
|
||||
public fitZoom = 16;
|
||||
|
||||
private _iconEl?: DivIcon;
|
||||
@@ -131,7 +129,7 @@ class LocationEditor extends LitElement {
|
||||
private async _initMap(): Promise<void> {
|
||||
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||
this._mapEl,
|
||||
this.darkMode ?? this.hass.themes?.darkMode,
|
||||
this.hass.themes?.darkMode,
|
||||
Boolean(this.radius)
|
||||
);
|
||||
this._leafletMap.addEventListener(
|
||||
|
@@ -1,115 +0,0 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
} from "../../data/media-player";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { createCloseHeading } from "../ha-dialog";
|
||||
import "./ha-media-player-browse";
|
||||
import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
|
||||
|
||||
@customElement("dialog-media-player-browse")
|
||||
class DialogMediaPlayerBrowse extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _entityId!: string;
|
||||
|
||||
@internalProperty() private _mediaContentId?: string;
|
||||
|
||||
@internalProperty() private _mediaContentType?: string;
|
||||
|
||||
@internalProperty() private _action?: MediaPlayerBrowseAction;
|
||||
|
||||
@internalProperty() private _params?: MediaPlayerBrowseDialogParams;
|
||||
|
||||
public async showDialog(
|
||||
params: MediaPlayerBrowseDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._entityId = this._params.entityId;
|
||||
this._mediaContentId = this._params.mediaContentId;
|
||||
this._mediaContentType = this._params.mediaContentType;
|
||||
this._action = this._params.action || "play";
|
||||
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
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.media-player-browser")
|
||||
)}
|
||||
@closed=${this._closeDialog}
|
||||
>
|
||||
<ha-media-player-browse
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.action=${this._action!}
|
||||
.mediaContentId=${this._mediaContentId}
|
||||
.mediaContentType=${this._mediaContentType}
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 8;
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
}
|
||||
ha-media-player-browse {
|
||||
width: 700px;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-media-player-browse": DialogMediaPlayerBrowse;
|
||||
}
|
||||
}
|
@@ -1,591 +0,0 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-fab/mwc-fab";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiArrowLeft, mdiFolder, mdiPlay, mdiPlus } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { browseMediaPlayer, MediaPickedEvent } 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";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-picker";
|
||||
import "../ha-button-menu";
|
||||
import "../ha-card";
|
||||
import "../ha-circular-progress";
|
||||
import "../ha-paper-dropdown-menu";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"media-picked": MediaPickedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-media-player-browse")
|
||||
export class HaMediaPlayerBrowse extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public entityId!: string;
|
||||
|
||||
@property() public mediaContentId?: string;
|
||||
|
||||
@property() public mediaContentType?: string;
|
||||
|
||||
@property() public action: "pick" | "play" = "play";
|
||||
|
||||
@property({ type: Boolean, attribute: "narrow", reflect: true })
|
||||
private _narrow = false;
|
||||
|
||||
@internalProperty() private _loading = false;
|
||||
|
||||
@internalProperty() private _mediaPlayerItems: MediaPlayerItem[] = [];
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.updateComplete.then(() => this._attachObserver());
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
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[
|
||||
this._mediaPlayerItems.length - 1
|
||||
];
|
||||
const previousItem =
|
||||
this._mediaPlayerItems.length > 1
|
||||
? this._mediaPlayerItems[this._mediaPlayerItems.length - 2]
|
||||
: undefined;
|
||||
|
||||
const hasExpandableChildren:
|
||||
| MediaPlayerItem
|
||||
| undefined = this._hasExpandableChildren(mostRecentItem.children);
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
${mostRecentItem.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="img"
|
||||
style="background-image: url(${mostRecentItem.thumbnail})"
|
||||
>
|
||||
${this._narrow && mostRecentItem?.can_play
|
||||
? html`
|
||||
<mwc-fab
|
||||
mini
|
||||
.item=${mostRecentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</mwc-fab>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
<div class="header-info">
|
||||
<div class="breadcrumb-overflow">
|
||||
<div class="breadcrumb">
|
||||
${previousItem
|
||||
? html`
|
||||
<div
|
||||
class="previous-title"
|
||||
.previous=${true}
|
||||
.item=${previousItem}
|
||||
@click=${this._navigate}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
|
||||
${previousItem.title}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<h1 class="title">${mostRecentItem.title}</h1>
|
||||
<h2 class="subtitle">
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.content-type.${mostRecentItem.media_content_type}`
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
${mostRecentItem?.can_play &&
|
||||
(!this._narrow || (this._narrow && !mostRecentItem.thumbnail))
|
||||
? html`
|
||||
<div class="actions">
|
||||
<mwc-button
|
||||
raised
|
||||
.item=${mostRecentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
${mostRecentItem.children?.length
|
||||
? hasExpandableChildren
|
||||
? html`
|
||||
<div class="children">
|
||||
${mostRecentItem.children?.length
|
||||
? html`
|
||||
${mostRecentItem.children.map(
|
||||
(child) => html`
|
||||
<div
|
||||
class="child"
|
||||
.item=${child}
|
||||
@click=${this._navigate}
|
||||
>
|
||||
<div class="ha-card-parent">
|
||||
<ha-card
|
||||
outlined
|
||||
style="background-image: url(${child.thumbnail})"
|
||||
>
|
||||
${child.can_expand && !child.thumbnail
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${mdiFolder}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
${child.can_play
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
class="play"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="title">${child.title}</div>
|
||||
<div class="type">
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.content-type.${child.media_content_type}`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<mwc-list>
|
||||
${mostRecentItem.children.map(
|
||||
(child) => html`<mwc-list-item
|
||||
@click=${this._actionClicked}
|
||||
.item=${child}
|
||||
graphic="icon"
|
||||
>
|
||||
<span>${child.title}</span>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||
></ha-svg-icon
|
||||
></mwc-list-item>
|
||||
<li divider role="separator"></li>`
|
||||
)}
|
||||
</mwc-list>
|
||||
`
|
||||
: this.hass.localize("ui.components.media-browser.no_items")}
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._measureCard();
|
||||
this._attachObserver();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (
|
||||
!changedProps.has("entityId") &&
|
||||
!changedProps.has("mediaContentId") &&
|
||||
!changedProps.has("mediaContentType") &&
|
||||
!changedProps.has("action")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._fetchData(this.mediaContentId, this.mediaContentType).then(
|
||||
(itemData) => {
|
||||
this._mediaPlayerItems = [itemData];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _actionClicked(ev: MouseEvent): void {
|
||||
ev.stopPropagation();
|
||||
const item = (ev.currentTarget as any).item;
|
||||
|
||||
this._runAction(item);
|
||||
}
|
||||
|
||||
private _runAction(item: MediaPlayerItem): void {
|
||||
fireEvent(this, "media-picked", {
|
||||
media_content_id: item.media_content_id,
|
||||
media_content_type: item.media_content_type,
|
||||
});
|
||||
}
|
||||
|
||||
private async _navigate(ev: MouseEvent): Promise<void> {
|
||||
const target = ev.currentTarget as any;
|
||||
let item: MediaPlayerItem | undefined;
|
||||
|
||||
if (target.previous) {
|
||||
this._mediaPlayerItems!.pop();
|
||||
item = this._mediaPlayerItems!.pop();
|
||||
}
|
||||
|
||||
item = target.item;
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemData = await this._fetchData(
|
||||
item.media_content_id,
|
||||
item.media_content_type
|
||||
);
|
||||
this._mediaPlayerItems = [...this._mediaPlayerItems, itemData];
|
||||
}
|
||||
|
||||
private async _fetchData(
|
||||
mediaContentId?: string,
|
||||
mediaContentType?: string
|
||||
): Promise<MediaPlayerItem> {
|
||||
const itemData = await browseMediaPlayer(
|
||||
this.hass,
|
||||
this.entityId,
|
||||
!mediaContentId ? undefined : mediaContentId,
|
||||
mediaContentType
|
||||
);
|
||||
|
||||
return itemData;
|
||||
}
|
||||
|
||||
private _measureCard(): void {
|
||||
this._narrow = this.offsetWidth < 500;
|
||||
}
|
||||
|
||||
private async _attachObserver(): Promise<void> {
|
||||
if (!this._resizeObserver) {
|
||||
await installResizeObserver();
|
||||
this._resizeObserver = new ResizeObserver(
|
||||
debounce(() => this._measureCard(), 250, false)
|
||||
);
|
||||
}
|
||||
|
||||
this._resizeObserver.observe(this);
|
||||
}
|
||||
|
||||
private _hasExpandableChildren = memoizeOne((children) =>
|
||||
children.find((item: MediaPlayerItem) => item.can_expand)
|
||||
);
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.breadcrumb-overflow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-grow: 1;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-content .img {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
margin-right: 16px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-info .actions {
|
||||
padding-top: 24px;
|
||||
--mdc-theme-primary: var(--primary-color);
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.breadcrumb .title {
|
||||
font-size: 48px;
|
||||
line-height: 1.2;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.breadcrumb .previous-title {
|
||||
font-size: 14px;
|
||||
padding-bottom: 8px;
|
||||
color: var(--secondary-text-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
|
||||
.breadcrumb .subtitle {
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.divider {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.divider::before {
|
||||
height: 1px;
|
||||
display: block;
|
||||
background-color: var(--divider-color);
|
||||
content: " ";
|
||||
}
|
||||
|
||||
/* ============= CHILDREN ============= */
|
||||
|
||||
mwc-list {
|
||||
--mdc-list-vertical-padding: 0;
|
||||
--mdc-theme-text-icon-on-background: var(--secondary-text-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
mwc-list li:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
mwc-list li[divider] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
|
||||
.children {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
auto-fit,
|
||||
minmax(var(--media-browse-item-size, 175px), 0.33fr)
|
||||
);
|
||||
grid-gap: 16px;
|
||||
margin: 8px 0px;
|
||||
}
|
||||
|
||||
.child {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ha-card-parent {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.child .folder,
|
||||
.child .play {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.child .folder {
|
||||
color: var(--secondary-text-color);
|
||||
top: calc(50% - (var(--mdc-icon-size) / 2));
|
||||
left: calc(50% - (var(--mdc-icon-size) / 2));
|
||||
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
||||
}
|
||||
|
||||
.child .play {
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
transition: all 0.5s;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.child .play:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
ha-card:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.child .title {
|
||||
font-size: 16px;
|
||||
padding-top: 8px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.child .type {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
/* ============= Narrow ============= */
|
||||
|
||||
:host([narrow]) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:host([narrow]) mwc-list {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
:host([narrow]) .breadcrumb .title {
|
||||
font-size: 38px;
|
||||
}
|
||||
|
||||
:host([narrow]) .breadcrumb-overflow {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
:host([narrow]) .header-content {
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
:host([narrow]) .header-content .img {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
padding-bottom: 100%;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([narrow]) .header-content .img mwc-fab {
|
||||
position: absolute;
|
||||
--mdc-theme-secondary: var(--primary-color);
|
||||
bottom: -20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
:host([narrow]) .header-info,
|
||||
:host([narrow]) .media-source,
|
||||
:host([narrow]) .children {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:host([narrow]) .children {
|
||||
grid-template-columns: 1fr 1fr !important;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-media-player-browse": HaMediaPlayerBrowse;
|
||||
}
|
||||
}
|
@@ -1,27 +0,0 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
} from "../../data/media-player";
|
||||
|
||||
export interface MediaPlayerBrowseDialogParams {
|
||||
action: MediaPlayerBrowseAction;
|
||||
entityId: string;
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void;
|
||||
mediaContentId?: string;
|
||||
mediaContentType?: string;
|
||||
}
|
||||
|
||||
export const showMediaBrowserDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: MediaPlayerBrowseDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-media-player-browse",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-media-player-browse" */ "./dialog-media-player-browse"
|
||||
),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -19,7 +19,6 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
|
||||
</style>
|
||||
<ha-chart-base
|
||||
id="chart"
|
||||
hass="[[hass]]"
|
||||
data="[[chartData]]"
|
||||
identifier="[[identifier]]"
|
||||
rendered="{{rendered}}"
|
||||
@@ -29,9 +28,6 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
chartData: Object,
|
||||
data: Object,
|
||||
names: Object,
|
||||
|
@@ -25,7 +25,6 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
|
||||
}
|
||||
</style>
|
||||
<ha-chart-base
|
||||
hass="[[hass]]"
|
||||
data="[[chartData]]"
|
||||
rendered="{{rendered}}"
|
||||
rtl="{{rtl}}"
|
||||
|
@@ -44,14 +44,3 @@ export const createAuthForUser = async (
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
export const adminChangePassword = async (
|
||||
hass: HomeAssistant,
|
||||
userId: string,
|
||||
password: string
|
||||
) =>
|
||||
hass.callWS<void>({
|
||||
type: "config/auth_provider/homeassistant/admin_change_password",
|
||||
user_id: userId,
|
||||
password,
|
||||
});
|
||||
|
@@ -3,7 +3,7 @@ import {
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { HomeAssistant, Context } from "../types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import { Action } from "./script";
|
||||
|
||||
@@ -90,12 +90,6 @@ export interface ZoneTrigger {
|
||||
event: "enter" | "leave";
|
||||
}
|
||||
|
||||
export interface TagTrigger {
|
||||
platform: "tag";
|
||||
tag_id: string;
|
||||
device_id?: string;
|
||||
}
|
||||
|
||||
export interface TimeTrigger {
|
||||
platform: "time";
|
||||
at: string;
|
||||
@@ -122,7 +116,6 @@ export type Trigger =
|
||||
| TimePatternTrigger
|
||||
| WebhookTrigger
|
||||
| ZoneTrigger
|
||||
| TagTrigger
|
||||
| TimeTrigger
|
||||
| TemplateTrigger
|
||||
| EventTrigger
|
||||
@@ -206,31 +199,3 @@ export const getAutomationEditorInitData = () => {
|
||||
inititialAutomationEditorData = undefined;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const subscribeTrigger = (
|
||||
hass: HomeAssistant,
|
||||
onChange: (result: {
|
||||
variables: {
|
||||
trigger: {};
|
||||
};
|
||||
context: Context;
|
||||
}) => void,
|
||||
trigger: Trigger | Trigger[],
|
||||
variables?: {}
|
||||
) =>
|
||||
hass.connection.subscribeMessage(onChange, {
|
||||
type: "subscribe_trigger",
|
||||
trigger,
|
||||
variables,
|
||||
});
|
||||
|
||||
export const testCondition = (
|
||||
hass: HomeAssistant,
|
||||
condition: Condition | Condition[],
|
||||
variables?: {}
|
||||
) =>
|
||||
hass.callWS<{ result: boolean }>({
|
||||
type: "test_condition",
|
||||
condition,
|
||||
variables,
|
||||
});
|
||||
|
@@ -8,7 +8,6 @@ export interface ConfigEntry {
|
||||
state: string;
|
||||
connection_class: string;
|
||||
supports_options: boolean;
|
||||
supports_unload: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigEntryMutableParams {
|
||||
@@ -38,11 +37,6 @@ export const deleteConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
|
||||
require_restart: boolean;
|
||||
}>("DELETE", `config/config_entries/entry/${configEntryId}`);
|
||||
|
||||
export const reloadConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
|
||||
hass.callApi<{
|
||||
require_restart: boolean;
|
||||
}>("POST", `config/config_entries/entry/${configEntryId}/reload`);
|
||||
|
||||
export const getConfigEntrySystemOptions = (
|
||||
hass: HomeAssistant,
|
||||
configEntryId: string
|
||||
|
@@ -72,7 +72,6 @@ export interface HassioAddonDetails extends HassioAddonInfo {
|
||||
ingress_panel: boolean;
|
||||
ingress_entry: null | string;
|
||||
ingress_url: null | string;
|
||||
watchdog: null | boolean;
|
||||
}
|
||||
|
||||
export interface HassioAddonsInfo {
|
||||
@@ -100,7 +99,6 @@ export interface HassioAddonSetOptionParams {
|
||||
auto_update?: boolean;
|
||||
ingress_panel?: boolean;
|
||||
network?: object | null;
|
||||
watchdog?: boolean;
|
||||
}
|
||||
|
||||
export const reloadHassioAddons = async (hass: HomeAssistant) => {
|
||||
|
@@ -40,10 +40,6 @@ export const updateOS = async (hass: HomeAssistant) => {
|
||||
return hass.callApi<HassioResponse<void>>("POST", "hassio/os/update");
|
||||
};
|
||||
|
||||
export const configSyncOS = async (hass: HomeAssistant) => {
|
||||
return hass.callApi<HassioResponse<void>>("POST", "hassio/os/config/sync");
|
||||
};
|
||||
|
||||
export const changeHostOptions = async (hass: HomeAssistant, options: any) => {
|
||||
return hass.callApi<HassioResponse<void>>(
|
||||
"POST",
|
||||
|
@@ -1,43 +0,0 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||
|
||||
export interface NetworkInterface {
|
||||
gateway: string;
|
||||
id: string;
|
||||
ip_address: string;
|
||||
address?: string;
|
||||
method: "static" | "dhcp";
|
||||
nameservers: string[] | string;
|
||||
dns?: string[];
|
||||
primary: boolean;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface NetworkInterfaces {
|
||||
[key: string]: NetworkInterface;
|
||||
}
|
||||
|
||||
export interface NetworkInfo {
|
||||
interfaces: NetworkInterfaces;
|
||||
}
|
||||
|
||||
export const fetchNetworkInfo = async (hass: HomeAssistant) => {
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<NetworkInfo>>(
|
||||
"GET",
|
||||
"hassio/network/info"
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const updateNetworkInterface = async (
|
||||
hass: HomeAssistant,
|
||||
network_interface: string,
|
||||
options: Partial<NetworkInterface>
|
||||
) => {
|
||||
await hass.callApi<HassioResponse<NetworkInfo>>(
|
||||
"POST",
|
||||
`hassio/network/interface/${network_interface}/update`,
|
||||
options
|
||||
);
|
||||
};
|
@@ -35,14 +35,6 @@ export interface SupervisorOptions {
|
||||
addons_repositories?: string[];
|
||||
}
|
||||
|
||||
export const reloadSupervisor = async (hass: HomeAssistant) => {
|
||||
await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/reload`);
|
||||
};
|
||||
|
||||
export const updateSupervisor = async (hass: HomeAssistant) => {
|
||||
await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/update`);
|
||||
};
|
||||
|
||||
export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => {
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HassioHomeAssistantInfo>>(
|
||||
@@ -79,11 +71,7 @@ export const createHassioSession = async (hass: HomeAssistant) => {
|
||||
"POST",
|
||||
"hassio/ingress/session"
|
||||
);
|
||||
document.cookie = `ingress_session=${
|
||||
response.data.session
|
||||
};path=/api/hassio_ingress/;SameSite=Strict${
|
||||
location.protocol === "https:" ? ";Secure" : ""
|
||||
}`;
|
||||
document.cookie = `ingress_session=${response.data.session};path=/api/hassio_ingress/`;
|
||||
};
|
||||
|
||||
export const setSupervisorOption = async (
|
||||
|
@@ -1,54 +0,0 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
interface Image {
|
||||
filesize: number;
|
||||
name: string;
|
||||
uploaded_at: string; // isoformat date
|
||||
content_type: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ImageMutableParams {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const generateImageThumbnailUrl = (mediaId: string, size: number) =>
|
||||
`/api/image/serve/${mediaId}/${size}x${size}`;
|
||||
|
||||
export const fetchImages = (hass: HomeAssistant) =>
|
||||
hass.callWS<Image[]>({ type: "image/list" });
|
||||
|
||||
export const createImage = async (
|
||||
hass: HomeAssistant,
|
||||
file: File
|
||||
): Promise<Image> => {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const resp = await hass.fetchWithAuth("/api/image/upload", {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
});
|
||||
if (resp.status === 413) {
|
||||
throw new Error("Uploaded image is too large");
|
||||
} else if (resp.status !== 200) {
|
||||
throw new Error("Unknown error");
|
||||
}
|
||||
return await resp.json();
|
||||
};
|
||||
|
||||
export const updateImage = (
|
||||
hass: HomeAssistant,
|
||||
id: string,
|
||||
updates: Partial<ImageMutableParams>
|
||||
) =>
|
||||
hass.callWS<Image>({
|
||||
type: "image/update",
|
||||
media_id: id,
|
||||
...updates,
|
||||
});
|
||||
|
||||
export const deleteImage = (hass: HomeAssistant, id: string) =>
|
||||
hass.callWS({
|
||||
type: "image/delete",
|
||||
media_id: id,
|
||||
});
|
@@ -7,12 +7,6 @@ export interface LogbookEntry {
|
||||
entity_id?: string;
|
||||
domain: string;
|
||||
context_user_id?: string;
|
||||
context_event_type?: string;
|
||||
context_domain?: string;
|
||||
context_service?: string;
|
||||
context_entity_id?: string;
|
||||
context_entity_id_name?: string;
|
||||
context_name?: string;
|
||||
}
|
||||
|
||||
const DATA_CACHE: {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
export const SUPPORT_PAUSE = 1;
|
||||
export const SUPPORT_SEEK = 2;
|
||||
@@ -15,49 +14,13 @@ export const SUPPORT_SELECT_SOURCE = 2048;
|
||||
export const SUPPORT_STOP = 4096;
|
||||
export const SUPPORTS_PLAY = 16384;
|
||||
export const SUPPORT_SELECT_SOUND_MODE = 65536;
|
||||
export const SUPPORT_BROWSE_MEDIA = 131072;
|
||||
export const CONTRAST_RATIO = 4.5;
|
||||
|
||||
export type MediaPlayerBrowseAction = "pick" | "play";
|
||||
|
||||
export interface MediaPickedEvent {
|
||||
media_content_id: string;
|
||||
media_content_type: string;
|
||||
}
|
||||
|
||||
export interface MediaPlayerThumbnail {
|
||||
content_type: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ControlButton {
|
||||
icon: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
export interface MediaPlayerItem {
|
||||
title: string;
|
||||
media_content_type: string;
|
||||
media_content_id: string;
|
||||
can_play: boolean;
|
||||
can_expand: boolean;
|
||||
thumbnail?: string;
|
||||
children?: MediaPlayerItem[];
|
||||
}
|
||||
|
||||
export const browseMediaPlayer = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
mediaContentId?: string,
|
||||
mediaContentType?: string
|
||||
): Promise<MediaPlayerItem> =>
|
||||
hass.callWS<MediaPlayerItem>({
|
||||
type: "media_player/browse_media",
|
||||
entity_id: entityId,
|
||||
media_content_id: mediaContentId,
|
||||
media_content_type: mediaContentType,
|
||||
});
|
||||
|
||||
export const getCurrentProgress = (stateObj: HassEntity): number => {
|
||||
let progress = stateObj.attributes.media_position;
|
||||
|
||||
|
164
src/data/ozw.ts
164
src/data/ozw.ts
@@ -1,10 +1,4 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { DeviceRegistryEntry } from "./device_registry";
|
||||
|
||||
export interface OZWNodeIdentifiers {
|
||||
ozw_instance: number;
|
||||
node_id: number;
|
||||
}
|
||||
|
||||
export interface OZWDevice {
|
||||
node_id: number;
|
||||
@@ -13,169 +7,15 @@ export interface OZWDevice {
|
||||
is_failed: boolean;
|
||||
is_zwave_plus: boolean;
|
||||
ozw_instance: number;
|
||||
event: string;
|
||||
}
|
||||
|
||||
export interface OZWDeviceMetaDataResponse {
|
||||
node_id: number;
|
||||
ozw_instance: number;
|
||||
metadata: OZWDeviceMetaData;
|
||||
}
|
||||
|
||||
export interface OZWDeviceMetaData {
|
||||
OZWInfoURL: string;
|
||||
ZWAProductURL: string;
|
||||
ProductPic: string;
|
||||
Description: string;
|
||||
ProductManualURL: string;
|
||||
ProductPageURL: string;
|
||||
InclusionHelp: string;
|
||||
ExclusionHelp: string;
|
||||
ResetHelp: string;
|
||||
WakeupHelp: string;
|
||||
ProductSupportURL: string;
|
||||
Frequency: string;
|
||||
Name: string;
|
||||
ProductPicBase64: string;
|
||||
}
|
||||
|
||||
export interface OZWInstance {
|
||||
ozw_instance: number;
|
||||
OZWDaemon_Version: string;
|
||||
OpenZWave_Version: string;
|
||||
QTOpenZWave_Version: string;
|
||||
Status: string;
|
||||
getControllerPath: string;
|
||||
homeID: string;
|
||||
}
|
||||
|
||||
export interface OZWNetworkStatistics {
|
||||
ozw_instance: number;
|
||||
node_count: number;
|
||||
readCnt: number;
|
||||
writeCnt: number;
|
||||
ACKCnt: number;
|
||||
CANCnt: number;
|
||||
NAKCnt: number;
|
||||
dropped: number;
|
||||
retries: number;
|
||||
}
|
||||
|
||||
export const nodeQueryStages = [
|
||||
"ProtocolInfo",
|
||||
"Probe",
|
||||
"WakeUp",
|
||||
"ManufacturerSpecific1",
|
||||
"NodeInfo",
|
||||
"NodePlusInfo",
|
||||
"ManufacturerSpecific2",
|
||||
"Versions",
|
||||
"Instances",
|
||||
"Static",
|
||||
"CacheLoad",
|
||||
"Associations",
|
||||
"Neighbors",
|
||||
"Session",
|
||||
"Dynamic",
|
||||
"Configuration",
|
||||
"Complete",
|
||||
];
|
||||
|
||||
export const networkOnlineStatuses = [
|
||||
"driverAllNodesQueried",
|
||||
"driverAllNodesQueriedSomeDead",
|
||||
"driverAwakeNodesQueried",
|
||||
];
|
||||
export const networkStartingStatuses = [
|
||||
"starting",
|
||||
"started",
|
||||
"Ready",
|
||||
"driverReady",
|
||||
];
|
||||
export const networkOfflineStatuses = [
|
||||
"Offline",
|
||||
"stopped",
|
||||
"driverFailed",
|
||||
"driverReset",
|
||||
"driverRemoved",
|
||||
"driverAllNodesOnFire",
|
||||
];
|
||||
|
||||
export const getIdentifiersFromDevice = function (
|
||||
device: DeviceRegistryEntry
|
||||
): OZWNodeIdentifiers | undefined {
|
||||
if (!device) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ozwIdentifier = device.identifiers.find(
|
||||
(identifier) => identifier[0] === "ozw"
|
||||
);
|
||||
if (!ozwIdentifier) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const identifiers = ozwIdentifier[1].split(".");
|
||||
return {
|
||||
node_id: parseInt(identifiers[1]),
|
||||
ozw_instance: parseInt(identifiers[0]),
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchOZWInstances = (
|
||||
hass: HomeAssistant
|
||||
): Promise<OZWInstance[]> =>
|
||||
hass.callWS({
|
||||
type: "ozw/get_instances",
|
||||
});
|
||||
|
||||
export const fetchOZWNetworkStatus = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number
|
||||
): Promise<OZWInstance> =>
|
||||
hass.callWS({
|
||||
type: "ozw/network_status",
|
||||
ozw_instance: ozw_instance,
|
||||
});
|
||||
|
||||
export const fetchOZWNetworkStatistics = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number
|
||||
): Promise<OZWNetworkStatistics> =>
|
||||
hass.callWS({
|
||||
type: "ozw/network_statistics",
|
||||
ozw_instance: ozw_instance,
|
||||
});
|
||||
|
||||
export const fetchOZWNodeStatus = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
ozw_instance: string,
|
||||
node_id: string
|
||||
): Promise<OZWDevice> =>
|
||||
hass.callWS({
|
||||
type: "ozw/node_status",
|
||||
ozw_instance: ozw_instance,
|
||||
node_id: node_id,
|
||||
});
|
||||
|
||||
export const fetchOZWNodeMetadata = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
): Promise<OZWDeviceMetaDataResponse> =>
|
||||
hass.callWS({
|
||||
type: "ozw/node_metadata",
|
||||
ozw_instance: ozw_instance,
|
||||
node_id: node_id,
|
||||
});
|
||||
|
||||
export const refreshNodeInfo = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
): Promise<OZWDevice> =>
|
||||
hass.callWS({
|
||||
type: "ozw/refresh_node_info",
|
||||
ozw_instance: ozw_instance,
|
||||
node_id: node_id,
|
||||
});
|
||||
|
@@ -17,9 +17,7 @@ export const setDefaultPanel = (
|
||||
};
|
||||
|
||||
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo =>
|
||||
hass.panels[hass.defaultPanel]
|
||||
? hass.panels[hass.defaultPanel]
|
||||
: hass.panels[DEFAULT_PANEL];
|
||||
hass.panels[hass.defaultPanel];
|
||||
|
||||
export const getPanelTitle = (hass: HomeAssistant): string | undefined => {
|
||||
if (!hass.panels) {
|
||||
|
@@ -5,14 +5,12 @@ export interface Person {
|
||||
name: string;
|
||||
user_id?: string;
|
||||
device_trackers?: string[];
|
||||
picture?: string;
|
||||
}
|
||||
|
||||
export interface PersonMutableParams {
|
||||
name: string;
|
||||
user_id: string | null;
|
||||
device_trackers: string[];
|
||||
picture: string | null;
|
||||
}
|
||||
|
||||
export const fetchPersons = (hass: HomeAssistant) =>
|
||||
|
@@ -1,57 +0,0 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { HassEventBase } from "home-assistant-js-websocket";
|
||||
|
||||
export const EVENT_TAG_SCANNED = "tag_scanned";
|
||||
|
||||
export interface TagScannedEvent extends HassEventBase {
|
||||
event_type: "tag_scanned";
|
||||
data: {
|
||||
tag_id: string;
|
||||
device_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
last_scanned?: string;
|
||||
}
|
||||
|
||||
export interface UpdateTagParams {
|
||||
name?: Tag["name"];
|
||||
description?: Tag["description"];
|
||||
}
|
||||
|
||||
export const fetchTags = async (hass: HomeAssistant) =>
|
||||
hass.callWS<Tag[]>({
|
||||
type: "tag/list",
|
||||
});
|
||||
|
||||
export const createTag = async (
|
||||
hass: HomeAssistant,
|
||||
params: UpdateTagParams,
|
||||
tagId?: string
|
||||
) =>
|
||||
hass.callWS<Tag>({
|
||||
type: "tag/create",
|
||||
tag_id: tagId,
|
||||
...params,
|
||||
});
|
||||
|
||||
export const updateTag = async (
|
||||
hass: HomeAssistant,
|
||||
tagId: string,
|
||||
params: UpdateTagParams
|
||||
) =>
|
||||
hass.callWS<Tag>({
|
||||
...params,
|
||||
type: "tag/update",
|
||||
tag_id: tagId,
|
||||
});
|
||||
|
||||
export const deleteTag = async (hass: HomeAssistant, tagId: string) =>
|
||||
hass.callWS<void>({
|
||||
type: "tag/delete",
|
||||
tag_id: tagId,
|
||||
});
|
@@ -2,7 +2,6 @@ import { SVGTemplateResult, svg, html, TemplateResult, css } from "lit-element";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
|
||||
import type { HomeAssistant, WeatherEntity } from "../types";
|
||||
import { roundWithOneDecimal } from "../util/calculate";
|
||||
|
||||
export const weatherSVGs = new Set<string>([
|
||||
"clear-night",
|
||||
@@ -136,7 +135,7 @@ export const getSecondaryWeatherAttribute = (
|
||||
return `
|
||||
${hass!.localize(
|
||||
`ui.card.weather.attributes.${attribute}`
|
||||
)} ${roundWithOneDecimal(value)} ${getWeatherUnit(hass!, attribute)}
|
||||
)} ${value} ${getWeatherUnit(hass!, attribute)}
|
||||
`;
|
||||
};
|
||||
|
||||
|
@@ -1,136 +0,0 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import Cropper from "cropperjs";
|
||||
// @ts-ignore
|
||||
import cropperCss from "cropperjs/dist/cropper.css";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from "lit-element";
|
||||
import "../../components/ha-dialog";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { HaImageCropperDialogParams } from "./show-image-cropper-dialog";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
@customElement("image-cropper-dialog")
|
||||
export class HaImagecropperDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _params?: HaImageCropperDialogParams;
|
||||
|
||||
@internalProperty() private _open = false;
|
||||
|
||||
@query("img") private _image!: HTMLImageElement;
|
||||
|
||||
private _cropper?: Cropper;
|
||||
|
||||
public showDialog(params: HaImageCropperDialogParams): void {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
this._params = undefined;
|
||||
this._cropper?.destroy();
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("_params") || !this._params) {
|
||||
return;
|
||||
}
|
||||
if (!this._cropper) {
|
||||
this._image.src = URL.createObjectURL(this._params.file);
|
||||
this._cropper = new Cropper(this._image, {
|
||||
aspectRatio: this._params.options.aspectRatio,
|
||||
viewMode: 1,
|
||||
dragMode: "move",
|
||||
minCropBoxWidth: 50,
|
||||
ready: () => {
|
||||
URL.revokeObjectURL(this._image!.src);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this._cropper.replace(URL.createObjectURL(this._params.file));
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<ha-dialog
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.open=${this._open}
|
||||
>
|
||||
<div
|
||||
class="container ${classMap({
|
||||
round: Boolean(this._params?.options.round),
|
||||
})}"
|
||||
>
|
||||
<img />
|
||||
</div>
|
||||
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
<mwc-button slot="primaryAction" @click=${this._cropImage}>
|
||||
${this.hass.localize("ui.dialogs.image_cropper.crop")}
|
||||
</mwc-button>
|
||||
</ha-dialog>`;
|
||||
}
|
||||
|
||||
private _cropImage() {
|
||||
this._cropper!.getCroppedCanvas().toBlob(
|
||||
(blob) => {
|
||||
if (!blob) {
|
||||
return;
|
||||
}
|
||||
const file = new File([blob], this._params!.file.name, {
|
||||
type: this._params!.options.type || this._params!.file.type,
|
||||
});
|
||||
this._params!.croppedCallback(file);
|
||||
this.closeDialog();
|
||||
},
|
||||
this._params!.options.type || this._params!.file.type,
|
||||
this._params!.options.quality
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
${unsafeCSS(cropperCss)}
|
||||
.container {
|
||||
max-width: 640px;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
.container.round .cropper-view-box,
|
||||
.container.round .cropper-face {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.cropper-line,
|
||||
.cropper-point,
|
||||
.cropper-point.point-se::before {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"image-cropper-dialog": HaImagecropperDialog;
|
||||
}
|
||||
}
|
@@ -1,30 +0,0 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export interface CropOptions {
|
||||
round: boolean;
|
||||
type?: "image/jpeg" | "image/png";
|
||||
quality?: number;
|
||||
aspectRatio: number;
|
||||
}
|
||||
|
||||
export interface HaImageCropperDialogParams {
|
||||
file: File;
|
||||
options: CropOptions;
|
||||
croppedCallback: (file: File) => void;
|
||||
}
|
||||
|
||||
const loadImageCropperDialog = () =>
|
||||
import(
|
||||
/* webpackChunkName: "image-cropper-dialog" */ "./image-cropper-dialog"
|
||||
);
|
||||
|
||||
export const showImageCropperDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HaImageCropperDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "image-cropper-dialog",
|
||||
dialogImport: loadImageCropperDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
361
src/dialogs/more-info/controls/more-info-light.js
Normal file
361
src/dialogs/more-info/controls/more-info-light.js
Normal file
@@ -0,0 +1,361 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { featureClassNames } from "../../../common/entity/feature_class_names";
|
||||
import "../../../components/ha-attributes";
|
||||
import "../../../components/ha-color-picker";
|
||||
import "../../../components/ha-labeled-slider";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import { EventsMixin } from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import "../../../components/ha-icon-button";
|
||||
|
||||
const FEATURE_CLASS_NAMES = {
|
||||
1: "has-brightness",
|
||||
2: "has-color_temp",
|
||||
4: "has-effect_list",
|
||||
16: "has-color",
|
||||
128: "has-white_value",
|
||||
};
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex"></style>
|
||||
<style>
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.effect_list,
|
||||
.brightness,
|
||||
.color_temp,
|
||||
.white_value {
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.5s ease-in;
|
||||
}
|
||||
|
||||
.color_temp {
|
||||
--ha-slider-background: -webkit-linear-gradient(
|
||||
right,
|
||||
rgb(255, 160, 0) 0%,
|
||||
white 50%,
|
||||
rgb(166, 209, 255) 100%
|
||||
);
|
||||
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
|
||||
--paper-slider-knob-start-border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.segmentationContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ha-color-picker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.5s ease-in;
|
||||
}
|
||||
|
||||
.segmentationButton {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 5%;
|
||||
transform: translate(0%, 0%);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.has-color.is-on .segmentationButton {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.has-effect_list.is-on .effect_list,
|
||||
.has-brightness .brightness,
|
||||
.has-color_temp.is-on .color_temp,
|
||||
.has-white_value.is-on .white_value {
|
||||
max-height: 84px;
|
||||
}
|
||||
|
||||
.has-brightness .has-color_temp.is-on,
|
||||
.has-white_value.is-on {
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
||||
.has-brightness .brightness,
|
||||
.has-color_temp.is-on .color_temp,
|
||||
.has-white_value.is-on .white_value {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.has-color.is-on ha-color-picker {
|
||||
max-height: 500px;
|
||||
overflow: visible;
|
||||
--ha-color-picker-wheel-borderwidth: 5;
|
||||
--ha-color-picker-wheel-bordercolor: white;
|
||||
--ha-color-picker-wheel-shadow: none;
|
||||
--ha-color-picker-marker-borderwidth: 2;
|
||||
--ha-color-picker-marker-bordercolor: white;
|
||||
}
|
||||
|
||||
.control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.is-unavailable .control {
|
||||
max-height: 0px;
|
||||
}
|
||||
|
||||
ha-attributes {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-paper-dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class$="[[computeClassNames(stateObj)]]">
|
||||
<div class="control brightness">
|
||||
<ha-labeled-slider
|
||||
caption="[[localize('ui.card.light.brightness')]]"
|
||||
icon="hass:brightness-5"
|
||||
min="1"
|
||||
max="255"
|
||||
value="{{brightnessSliderValue}}"
|
||||
on-change="brightnessSliderChanged"
|
||||
></ha-labeled-slider>
|
||||
</div>
|
||||
|
||||
<div class="control color_temp">
|
||||
<ha-labeled-slider
|
||||
caption="[[localize('ui.card.light.color_temperature')]]"
|
||||
icon="hass:thermometer"
|
||||
min="[[stateObj.attributes.min_mireds]]"
|
||||
max="[[stateObj.attributes.max_mireds]]"
|
||||
value="{{ctSliderValue}}"
|
||||
on-change="ctSliderChanged"
|
||||
></ha-labeled-slider>
|
||||
</div>
|
||||
|
||||
<div class="control white_value">
|
||||
<ha-labeled-slider
|
||||
caption="[[localize('ui.card.light.white_value')]]"
|
||||
icon="hass:file-word-box"
|
||||
max="255"
|
||||
value="{{wvSliderValue}}"
|
||||
on-change="wvSliderChanged"
|
||||
></ha-labeled-slider>
|
||||
</div>
|
||||
<div class="segmentationContainer">
|
||||
<ha-color-picker
|
||||
class="control color"
|
||||
on-colorselected="colorPicked"
|
||||
desired-hs-color="{{colorPickerColor}}"
|
||||
throttle="500"
|
||||
hue-segments="{{hueSegments}}"
|
||||
saturation-segments="{{saturationSegments}}"
|
||||
>
|
||||
</ha-color-picker>
|
||||
<ha-icon-button
|
||||
icon="mdi:palette"
|
||||
on-click="segmentClick"
|
||||
class="segmentationButton"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
|
||||
<div class="control effect_list">
|
||||
<ha-paper-dropdown-menu
|
||||
label-float=""
|
||||
dynamic-align=""
|
||||
label="[[localize('ui.card.light.effect')]]"
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
selected="[[stateObj.attributes.effect]]"
|
||||
on-selected-changed="effectChanged"
|
||||
attr-for-selected="item-name"
|
||||
>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[stateObj.attributes.effect_list]]"
|
||||
>
|
||||
<paper-item item-name$="[[item]]">[[item]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</div>
|
||||
|
||||
<ha-attributes
|
||||
state-obj="[[stateObj]]"
|
||||
extra-filters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,xy_color,min_mireds,max_mireds,entity_id"
|
||||
></ha-attributes>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: "stateObjChanged",
|
||||
},
|
||||
|
||||
brightnessSliderValue: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
|
||||
ctSliderValue: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
|
||||
wvSliderValue: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
|
||||
hueSegments: {
|
||||
type: Number,
|
||||
value: 24,
|
||||
},
|
||||
|
||||
saturationSegments: {
|
||||
type: Number,
|
||||
value: 8,
|
||||
},
|
||||
|
||||
colorPickerColor: {
|
||||
type: Object,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
stateObjChanged(newVal, oldVal) {
|
||||
const props = {
|
||||
brightnessSliderValue: 0,
|
||||
};
|
||||
|
||||
if (newVal && newVal.state === "on") {
|
||||
props.brightnessSliderValue = newVal.attributes.brightness;
|
||||
props.ctSliderValue = newVal.attributes.color_temp;
|
||||
props.wvSliderValue = newVal.attributes.white_value;
|
||||
if (newVal.attributes.hs_color) {
|
||||
props.colorPickerColor = {
|
||||
h: newVal.attributes.hs_color[0],
|
||||
s: newVal.attributes.hs_color[1] / 100,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.setProperties(props);
|
||||
|
||||
if (oldVal) {
|
||||
setTimeout(() => {
|
||||
this.fire("iron-resize");
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
computeClassNames(stateObj) {
|
||||
const classes = [
|
||||
"content",
|
||||
featureClassNames(stateObj, FEATURE_CLASS_NAMES),
|
||||
];
|
||||
if (stateObj && stateObj.state === "on") {
|
||||
classes.push("is-on");
|
||||
}
|
||||
if (stateObj && stateObj.state === "unavailable") {
|
||||
classes.push("is-unavailable");
|
||||
}
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
effectChanged(ev) {
|
||||
const oldVal = this.stateObj.attributes.effect;
|
||||
const newVal = ev.detail.value;
|
||||
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
effect: newVal,
|
||||
});
|
||||
}
|
||||
|
||||
brightnessSliderChanged(ev) {
|
||||
const bri = parseInt(ev.target.value, 10);
|
||||
|
||||
if (isNaN(bri)) return;
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
brightness: bri,
|
||||
});
|
||||
}
|
||||
|
||||
ctSliderChanged(ev) {
|
||||
const ct = parseInt(ev.target.value, 10);
|
||||
|
||||
if (isNaN(ct)) return;
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
color_temp: ct,
|
||||
});
|
||||
}
|
||||
|
||||
wvSliderChanged(ev) {
|
||||
const wv = parseInt(ev.target.value, 10);
|
||||
|
||||
if (isNaN(wv)) return;
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
white_value: wv,
|
||||
});
|
||||
}
|
||||
|
||||
segmentClick() {
|
||||
if (this.hueSegments === 24 && this.saturationSegments === 8) {
|
||||
this.setProperties({ hueSegments: 0, saturationSegments: 0 });
|
||||
} else {
|
||||
this.setProperties({ hueSegments: 24, saturationSegments: 8 });
|
||||
}
|
||||
}
|
||||
|
||||
serviceChangeColor(hass, entityId, color) {
|
||||
hass.callService("light", "turn_on", {
|
||||
entity_id: entityId,
|
||||
hs_color: [color.h, color.s * 100],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new color has been picked.
|
||||
* should be throttled with the 'throttle=' attribute of the color picker
|
||||
*/
|
||||
colorPicked(ev) {
|
||||
this.serviceChangeColor(this.hass, this.stateObj.entity_id, ev.detail.hs);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("more-info-light", MoreInfoLight);
|
@@ -1,305 +0,0 @@
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-attributes";
|
||||
import "../../../components/ha-color-picker";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-labeled-slider";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import {
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR,
|
||||
SUPPORT_COLOR_TEMP,
|
||||
SUPPORT_EFFECT,
|
||||
SUPPORT_WHITE_VALUE,
|
||||
} from "../../../data/light";
|
||||
import type { HomeAssistant, LightEntity } from "../../../types";
|
||||
|
||||
interface HueSatColor {
|
||||
h: number;
|
||||
s: number;
|
||||
}
|
||||
|
||||
@customElement("more-info-light")
|
||||
class MoreInfoLight extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: LightEntity;
|
||||
|
||||
@internalProperty() private _brightnessSliderValue = 0;
|
||||
|
||||
@internalProperty() private _ctSliderValue = 0;
|
||||
|
||||
@internalProperty() private _wvSliderValue = 0;
|
||||
|
||||
@internalProperty() private _hueSegments = 24;
|
||||
|
||||
@internalProperty() private _saturationSegments = 8;
|
||||
|
||||
@internalProperty() private _colorPickerColor?: HueSatColor;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"is-on": this.stateObj.state === "on",
|
||||
})}"
|
||||
>
|
||||
${this.stateObj.state === "on"
|
||||
? html`
|
||||
${supportsFeature(this.stateObj!, SUPPORT_BRIGHTNESS)
|
||||
? html`
|
||||
<ha-labeled-slider
|
||||
caption=${this.hass.localize("ui.card.light.brightness")}
|
||||
icon="hass:brightness-5"
|
||||
min="1"
|
||||
max="255"
|
||||
value=${this._brightnessSliderValue}
|
||||
@change=${this._brightnessSliderChanged}
|
||||
></ha-labeled-slider>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(this.stateObj, SUPPORT_COLOR_TEMP)
|
||||
? html`
|
||||
<ha-labeled-slider
|
||||
class="color_temp"
|
||||
caption=${this.hass.localize(
|
||||
"ui.card.light.color_temperature"
|
||||
)}
|
||||
icon="hass:thermometer"
|
||||
.min=${this.stateObj.attributes.min_mireds}
|
||||
.max=${this.stateObj.attributes.max_mireds}
|
||||
.value=${this._ctSliderValue}
|
||||
@change=${this._ctSliderChanged}
|
||||
></ha-labeled-slider>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(this.stateObj, SUPPORT_WHITE_VALUE)
|
||||
? html`
|
||||
<ha-labeled-slider
|
||||
caption=${this.hass.localize("ui.card.light.white_value")}
|
||||
icon="hass:file-word-box"
|
||||
max="255"
|
||||
.value=${this._wvSliderValue}
|
||||
@change=${this._wvSliderChanged}
|
||||
></ha-labeled-slider>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(this.stateObj, SUPPORT_COLOR)
|
||||
? html`
|
||||
<div class="segmentationContainer">
|
||||
<ha-color-picker
|
||||
class="color"
|
||||
@colorselected=${this._colorPicked}
|
||||
.desiredHsColor=${this._colorPickerColor}
|
||||
throttle="500"
|
||||
.hueSegments=${this._hueSegments}
|
||||
.saturationSegments=${this._saturationSegments}
|
||||
>
|
||||
</ha-color-picker>
|
||||
<ha-icon-button
|
||||
icon="hass:palette"
|
||||
@click=${this._segmentClick}
|
||||
class="segmentationButton"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(this.stateObj, SUPPORT_EFFECT) &&
|
||||
this.stateObj!.attributes.effect_list?.length
|
||||
? html`
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this.hass.localize("ui.card.light.effect")}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${this.stateObj.attributes.effect || ""}
|
||||
@iron-select=${this._effectChanged}
|
||||
attr-for-selected="item-name"
|
||||
>${this.stateObj.attributes.effect_list.map(
|
||||
(effect: string) => html`
|
||||
<paper-item itemName=${effect}
|
||||
>${effect}</paper-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
<ha-attributes
|
||||
.stateObj=${this.stateObj}
|
||||
extra-filters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,xy_color,min_mireds,max_mireds,entity_id"
|
||||
></ha-attributes>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
const stateObj = this.stateObj! as LightEntity;
|
||||
if (changedProps.has("stateObj") && stateObj.state === "on") {
|
||||
this._brightnessSliderValue = stateObj.attributes.brightness;
|
||||
this._ctSliderValue = stateObj.attributes.color_temp;
|
||||
this._wvSliderValue = stateObj.attributes.white_value;
|
||||
|
||||
if (stateObj.attributes.hs_color) {
|
||||
this._colorPickerColor = {
|
||||
h: stateObj.attributes.hs_color[0],
|
||||
s: stateObj.attributes.hs_color[1] / 100,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _effectChanged(ev: CustomEvent) {
|
||||
const newVal = ev.detail.value;
|
||||
|
||||
if (!newVal || this.stateObj!.attributes.effect === newVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
effect: newVal,
|
||||
});
|
||||
}
|
||||
|
||||
private _brightnessSliderChanged(ev: CustomEvent) {
|
||||
const bri = parseInt((ev.target as any).value, 10);
|
||||
|
||||
if (isNaN(bri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
brightness: bri,
|
||||
});
|
||||
}
|
||||
|
||||
private _ctSliderChanged(ev: CustomEvent) {
|
||||
const ct = parseInt((ev.target as any).value, 10);
|
||||
|
||||
if (isNaN(ct)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
color_temp: ct,
|
||||
});
|
||||
}
|
||||
|
||||
private _wvSliderChanged(ev: CustomEvent) {
|
||||
const wv = parseInt((ev.target as any).value, 10);
|
||||
|
||||
if (isNaN(wv)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
white_value: wv,
|
||||
});
|
||||
}
|
||||
|
||||
private _segmentClick() {
|
||||
if (this._hueSegments === 24 && this._saturationSegments === 8) {
|
||||
this._hueSegments = 0;
|
||||
this._saturationSegments = 0;
|
||||
} else {
|
||||
this._hueSegments = 24;
|
||||
this._saturationSegments = 8;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new color has been picked.
|
||||
* should be throttled with the 'throttle=' attribute of the color picker
|
||||
*/
|
||||
private _colorPicked(ev: CustomEvent) {
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100],
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content.is-on {
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
||||
.content > * {
|
||||
width: 100%;
|
||||
max-height: 84px;
|
||||
overflow: hidden;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.color_temp {
|
||||
--ha-slider-background: -webkit-linear-gradient(
|
||||
right,
|
||||
rgb(255, 160, 0) 0%,
|
||||
white 50%,
|
||||
rgb(166, 209, 255) 100%
|
||||
);
|
||||
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
|
||||
--paper-slider-knob-start-border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.segmentationContainer {
|
||||
position: relative;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
ha-color-picker {
|
||||
--ha-color-picker-wheel-borderwidth: 5;
|
||||
--ha-color-picker-wheel-bordercolor: white;
|
||||
--ha-color-picker-wheel-shadow: none;
|
||||
--ha-color-picker-marker-borderwidth: 2;
|
||||
--ha-color-picker-marker-bordercolor: white;
|
||||
}
|
||||
|
||||
.segmentationButton {
|
||||
position: absolute;
|
||||
top: 5%;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"more-info-light": MoreInfoLight;
|
||||
}
|
||||
}
|
421
src/dialogs/more-info/controls/more-info-media_player.js
Normal file
421
src/dialogs/more-info/controls/more-info-media_player.js
Normal file
@@ -0,0 +1,421 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { attributeClassNames } from "../../../common/entity/attribute_class_names";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import "../../../components/ha-paper-slider";
|
||||
import "../../../components/ha-icon";
|
||||
import { EventsMixin } from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import HassMediaPlayerEntity from "../../../util/hass-media-player-model";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-flex-alignment"></style>
|
||||
<style>
|
||||
.media-state {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
ha-icon-button[highlight] {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.volume {
|
||||
margin-bottom: 8px;
|
||||
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.5s ease-in;
|
||||
}
|
||||
|
||||
.has-volume_level .volume {
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
ha-icon.source-input {
|
||||
padding: 7px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
ha-paper-dropdown-menu.source-input {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class$="[[computeClassNames(stateObj)]]">
|
||||
<div class="layout horizontal">
|
||||
<div class="flex">
|
||||
<ha-icon-button
|
||||
icon="hass:power"
|
||||
highlight$="[[playerObj.isOff]]"
|
||||
on-click="handleTogglePower"
|
||||
hidden$="[[computeHidePowerButton(playerObj)]]"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<div>
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[computeShowPlaybackControls(playerObj)]]"
|
||||
>
|
||||
<ha-icon-button
|
||||
icon="hass:skip-previous"
|
||||
on-click="handlePrevious"
|
||||
hidden$="[[!playerObj.supportsPreviousTrack]]"
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
icon="[[computePlaybackControlIcon(playerObj)]]"
|
||||
on-click="handlePlaybackControl"
|
||||
hidden$="[[!computePlaybackControlIcon(playerObj)]]"
|
||||
highlight=""
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
icon="hass:skip-next"
|
||||
on-click="handleNext"
|
||||
hidden$="[[!playerObj.supportsNextTrack]]"
|
||||
></ha-icon-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- VOLUME -->
|
||||
<div
|
||||
class="volume_buttons center horizontal layout"
|
||||
hidden$="[[computeHideVolumeButtons(playerObj)]]"
|
||||
>
|
||||
<ha-icon-button
|
||||
on-click="handleVolumeTap"
|
||||
icon="hass:volume-off"
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
id="volumeDown"
|
||||
disabled$="[[playerObj.isMuted]]"
|
||||
on-mousedown="handleVolumeDown"
|
||||
on-touchstart="handleVolumeDown"
|
||||
on-touchend="handleVolumeTouchEnd"
|
||||
icon="hass:volume-medium"
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
id="volumeUp"
|
||||
disabled$="[[playerObj.isMuted]]"
|
||||
on-mousedown="handleVolumeUp"
|
||||
on-touchstart="handleVolumeUp"
|
||||
on-touchend="handleVolumeTouchEnd"
|
||||
icon="hass:volume-high"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<div
|
||||
class="volume center horizontal layout"
|
||||
hidden$="[[!playerObj.supportsVolumeSet]]"
|
||||
>
|
||||
<ha-icon-button
|
||||
on-click="handleVolumeTap"
|
||||
hidden$="[[playerObj.supportsVolumeButtons]]"
|
||||
icon="[[computeMuteVolumeIcon(playerObj)]]"
|
||||
></ha-icon-button>
|
||||
<ha-paper-slider
|
||||
disabled$="[[playerObj.isMuted]]"
|
||||
min="0"
|
||||
max="100"
|
||||
value="[[playerObj.volumeSliderValue]]"
|
||||
on-change="volumeSliderChanged"
|
||||
class="flex"
|
||||
ignore-bar-touch=""
|
||||
dir="{{rtl}}"
|
||||
>
|
||||
</ha-paper-slider>
|
||||
</div>
|
||||
<!-- SOURCE PICKER -->
|
||||
<div
|
||||
class="controls layout horizontal justified"
|
||||
hidden$="[[computeHideSelectSource(playerObj)]]"
|
||||
>
|
||||
<ha-icon class="source-input" icon="hass:login-variant"></ha-icon>
|
||||
<ha-paper-dropdown-menu
|
||||
class="flex source-input"
|
||||
dynamic-align=""
|
||||
label-float=""
|
||||
label="[[localize('ui.card.media_player.source')]]"
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-name"
|
||||
selected="[[playerObj.source]]"
|
||||
on-selected-changed="handleSourceChanged"
|
||||
>
|
||||
<template is="dom-repeat" items="[[playerObj.sourceList]]">
|
||||
<paper-item item-name$="[[item]]">[[item]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</div>
|
||||
<!-- SOUND MODE PICKER -->
|
||||
<template is="dom-if" if="[[!computeHideSelectSoundMode(playerObj)]]">
|
||||
<div class="controls layout horizontal justified">
|
||||
<ha-icon class="source-input" icon="hass:music-note"></ha-icon>
|
||||
<ha-paper-dropdown-menu
|
||||
class="flex source-input"
|
||||
dynamic-align
|
||||
label-float
|
||||
label="[[localize('ui.card.media_player.sound_mode')]]"
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-name"
|
||||
selected="[[playerObj.soundMode]]"
|
||||
on-selected-changed="handleSoundModeChanged"
|
||||
>
|
||||
<template is="dom-repeat" items="[[playerObj.soundModeList]]">
|
||||
<paper-item item-name$="[[item]]">[[item]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</div>
|
||||
</template>
|
||||
<!-- TTS -->
|
||||
<div
|
||||
hidden$="[[computeHideTTS(ttsLoaded, playerObj)]]"
|
||||
class="layout horizontal end"
|
||||
>
|
||||
<paper-input
|
||||
id="ttsInput"
|
||||
label="[[localize('ui.card.media_player.text_to_speak')]]"
|
||||
class="flex"
|
||||
value="{{ttsMessage}}"
|
||||
on-keydown="ttsCheckForEnter"
|
||||
></paper-input>
|
||||
<ha-icon-button icon="hass:send" on-click="sendTTS"></ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: Object,
|
||||
playerObj: {
|
||||
type: Object,
|
||||
computed: "computePlayerObj(hass, stateObj)",
|
||||
observer: "playerObjChanged",
|
||||
},
|
||||
|
||||
ttsLoaded: {
|
||||
type: Boolean,
|
||||
computed: "computeTTSLoaded(hass)",
|
||||
},
|
||||
|
||||
ttsMessage: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
|
||||
rtl: {
|
||||
type: String,
|
||||
computed: "_computeRTLDirection(hass)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computePlayerObj(hass, stateObj) {
|
||||
return new HassMediaPlayerEntity(hass, stateObj);
|
||||
}
|
||||
|
||||
playerObjChanged(newVal, oldVal) {
|
||||
if (oldVal) {
|
||||
setTimeout(() => {
|
||||
this.fire("iron-resize");
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
computeClassNames(stateObj) {
|
||||
return attributeClassNames(stateObj, ["volume_level"]);
|
||||
}
|
||||
|
||||
computeMuteVolumeIcon(playerObj) {
|
||||
return playerObj.isMuted ? "hass:volume-off" : "hass:volume-high";
|
||||
}
|
||||
|
||||
computeHideVolumeButtons(playerObj) {
|
||||
return !playerObj.supportsVolumeButtons || playerObj.isOff;
|
||||
}
|
||||
|
||||
computeShowPlaybackControls(playerObj) {
|
||||
return !playerObj.isOff && playerObj.hasMediaControl;
|
||||
}
|
||||
|
||||
computePlaybackControlIcon(playerObj) {
|
||||
if (playerObj.isPlaying) {
|
||||
return playerObj.supportsPause ? "hass:pause" : "hass:stop";
|
||||
}
|
||||
if (playerObj.hasMediaControl || playerObj.isOff || playerObj.isIdle) {
|
||||
if (
|
||||
playerObj.hasMediaControl &&
|
||||
playerObj.supportsPause &&
|
||||
!playerObj.isPaused
|
||||
) {
|
||||
return "hass:play-pause";
|
||||
}
|
||||
return playerObj.supportsPlay ? "hass:play" : null;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
computeHidePowerButton(playerObj) {
|
||||
return playerObj.isOff
|
||||
? !playerObj.supportsTurnOn
|
||||
: !playerObj.supportsTurnOff;
|
||||
}
|
||||
|
||||
computeHideSelectSource(playerObj) {
|
||||
return (
|
||||
playerObj.isOff ||
|
||||
!playerObj.supportsSelectSource ||
|
||||
!playerObj.sourceList
|
||||
);
|
||||
}
|
||||
|
||||
computeHideSelectSoundMode(playerObj) {
|
||||
return (
|
||||
playerObj.isOff ||
|
||||
!playerObj.supportsSelectSoundMode ||
|
||||
!playerObj.soundModeList
|
||||
);
|
||||
}
|
||||
|
||||
computeHideTTS(ttsLoaded, playerObj) {
|
||||
return !ttsLoaded || !playerObj.supportsPlayMedia;
|
||||
}
|
||||
|
||||
computeTTSLoaded(hass) {
|
||||
return isComponentLoaded(hass, "tts");
|
||||
}
|
||||
|
||||
handleTogglePower() {
|
||||
this.playerObj.togglePower();
|
||||
}
|
||||
|
||||
handlePrevious() {
|
||||
this.playerObj.previousTrack();
|
||||
}
|
||||
|
||||
handlePlaybackControl() {
|
||||
this.playerObj.mediaPlayPause();
|
||||
}
|
||||
|
||||
handleNext() {
|
||||
this.playerObj.nextTrack();
|
||||
}
|
||||
|
||||
handleSourceChanged(ev) {
|
||||
if (!this.playerObj) return;
|
||||
|
||||
const oldVal = this.playerObj.source;
|
||||
const newVal = ev.detail.value;
|
||||
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
|
||||
this.playerObj.selectSource(newVal);
|
||||
}
|
||||
|
||||
handleSoundModeChanged(ev) {
|
||||
if (!this.playerObj) return;
|
||||
|
||||
const oldVal = this.playerObj.soundMode;
|
||||
const newVal = ev.detail.value;
|
||||
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
|
||||
this.playerObj.selectSoundMode(newVal);
|
||||
}
|
||||
|
||||
handleVolumeTap() {
|
||||
if (!this.playerObj.supportsVolumeMute) {
|
||||
return;
|
||||
}
|
||||
this.playerObj.volumeMute(!this.playerObj.isMuted);
|
||||
}
|
||||
|
||||
handleVolumeTouchEnd(ev) {
|
||||
/* when touch ends, we must prevent this from
|
||||
* becoming a mousedown, up, click by emulation */
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
handleVolumeUp() {
|
||||
const obj = this.$.volumeUp;
|
||||
this.handleVolumeWorker("volume_up", obj, true);
|
||||
}
|
||||
|
||||
handleVolumeDown() {
|
||||
const obj = this.$.volumeDown;
|
||||
this.handleVolumeWorker("volume_down", obj, true);
|
||||
}
|
||||
|
||||
handleVolumeWorker(service, obj, force) {
|
||||
if (force || (obj !== undefined && obj.pointerDown)) {
|
||||
this.playerObj.callService(service);
|
||||
setTimeout(() => this.handleVolumeWorker(service, obj, false), 500);
|
||||
}
|
||||
}
|
||||
|
||||
volumeSliderChanged(ev) {
|
||||
const volPercentage = parseFloat(ev.target.value);
|
||||
const volume = volPercentage > 0 ? volPercentage / 100 : 0;
|
||||
this.playerObj.setVolume(volume);
|
||||
}
|
||||
|
||||
ttsCheckForEnter(ev) {
|
||||
if (ev.keyCode === 13) this.sendTTS();
|
||||
}
|
||||
|
||||
sendTTS() {
|
||||
const services = this.hass.services.tts;
|
||||
const serviceKeys = Object.keys(services).sort();
|
||||
let service;
|
||||
let i;
|
||||
|
||||
for (i = 0; i < serviceKeys.length; i++) {
|
||||
if (serviceKeys[i].indexOf("_say") !== -1) {
|
||||
service = serviceKeys[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!service) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("tts", service, {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
message: this.ttsMessage,
|
||||
});
|
||||
this.ttsMessage = "";
|
||||
this.$.ttsInput.focus();
|
||||
}
|
||||
|
||||
_computeRTLDirection(hass) {
|
||||
return computeRTLDirection(hass);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("more-info-media_player", MoreInfoMediaPlayer);
|
@@ -1,431 +0,0 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import "../../../components/ha-slider";
|
||||
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
|
||||
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity";
|
||||
import {
|
||||
ControlButton,
|
||||
MediaPickedEvent,
|
||||
SUPPORTS_PLAY,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_PLAY_MEDIA,
|
||||
SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_SELECT_SOUND_MODE,
|
||||
SUPPORT_SELECT_SOURCE,
|
||||
SUPPORT_STOP,
|
||||
SUPPORT_TURN_OFF,
|
||||
SUPPORT_TURN_ON,
|
||||
SUPPORT_VOLUME_BUTTONS,
|
||||
SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_VOLUME_SET,
|
||||
} from "../../../data/media-player";
|
||||
import { HomeAssistant, MediaEntity } from "../../../types";
|
||||
|
||||
@customElement("more-info-media_player")
|
||||
class MoreInfoMediaPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: MediaEntity;
|
||||
|
||||
@query("#ttsInput") private _ttsInput?: HTMLInputElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const controls = this._getControls();
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
return html`
|
||||
${!controls
|
||||
? ""
|
||||
: html`
|
||||
<div class="controls">
|
||||
<div class="basic-controls">
|
||||
${controls!.map(
|
||||
(control) => html`
|
||||
<ha-icon-button
|
||||
action=${control.action}
|
||||
.icon=${control.icon}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
${supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
icon="hass:folder-multiple"
|
||||
.title=${this.hass.localize(
|
||||
"ui.card.media_player.browse_media"
|
||||
)}
|
||||
@click=${this._showBrowseMedia}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`}
|
||||
${(supportsFeature(stateObj, SUPPORT_VOLUME_SET) ||
|
||||
supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS)) &&
|
||||
![UNAVAILABLE, UNKNOWN, "off"].includes(stateObj.state)
|
||||
? html`
|
||||
<div class="volume">
|
||||
${supportsFeature(stateObj, SUPPORT_VOLUME_MUTE)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.icon=${stateObj.attributes.is_volume_muted
|
||||
? "hass:volume-off"
|
||||
: "hass:volume-high"}
|
||||
@click=${this._toggleMute}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(stateObj, SUPPORT_VOLUME_SET)
|
||||
? html`
|
||||
<ha-slider
|
||||
id="input"
|
||||
pin
|
||||
ignore-bar-touch
|
||||
.dir=${computeRTLDirection(this.hass!)}
|
||||
.value=${Number(stateObj.attributes.volume_level) * 100}
|
||||
@change=${this._selectedValueChanged}
|
||||
></ha-slider>
|
||||
`
|
||||
: supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
action="volume_down"
|
||||
icon="hass:volume-minus"
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
action="volume_up"
|
||||
icon="hass:volume-plus"
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${stateObj.state !== "off" &&
|
||||
supportsFeature(stateObj, SUPPORT_SELECT_SOURCE) &&
|
||||
stateObj.attributes.source_list?.length
|
||||
? html`
|
||||
<div class="source-input">
|
||||
<ha-icon class="source-input" icon="hass:login-variant"></ha-icon>
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this.hass.localize("ui.card.media_player.source")}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-name"
|
||||
.selected=${stateObj.attributes.source!}
|
||||
@iron-select=${this._handleSourceChanged}
|
||||
>
|
||||
${stateObj.attributes.source_list!.map(
|
||||
(source) =>
|
||||
html`
|
||||
<paper-item .itemName=${source}>${source}</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(stateObj, SUPPORT_SELECT_SOUND_MODE) &&
|
||||
stateObj.attributes.sound_mode_list?.length
|
||||
? html`
|
||||
<div class="sound-input">
|
||||
<ha-icon icon="hass:music-note"></ha-icon>
|
||||
<ha-paper-dropdown-menu
|
||||
dynamic-align
|
||||
label-float
|
||||
.label=${this.hass.localize("ui.card.media_player.sound_mode")}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-name"
|
||||
.selected=${stateObj.attributes.sound_mode!}
|
||||
@iron-select=${this._handleSoundModeChanged}
|
||||
>
|
||||
${stateObj.attributes.sound_mode_list.map(
|
||||
(mode) => html`
|
||||
<paper-item .itemName=${mode}>${mode}</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${isComponentLoaded(this.hass, "tts") &&
|
||||
supportsFeature(stateObj, SUPPORT_PLAY_MEDIA)
|
||||
? html`
|
||||
<div class="tts">
|
||||
<paper-input
|
||||
id="ttsInput"
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.text_to_speak"
|
||||
)}
|
||||
@keydown=${this._ttsCheckForEnter}
|
||||
></paper-input>
|
||||
<ha-icon-button icon="hass:send" @click=${
|
||||
this._sendTTS
|
||||
}></ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-icon-button[action="turn_off"],
|
||||
ha-icon-button[action="turn_on"],
|
||||
ha-slider,
|
||||
#ttsInput {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.basic-controls {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.volume,
|
||||
.source-input,
|
||||
.sound-input,
|
||||
.tts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.source-input ha-icon,
|
||||
.sound-input ha-icon {
|
||||
padding: 7px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.source-input ha-paper-dropdown-menu,
|
||||
.sound-input ha-paper-dropdown-menu {
|
||||
margin-left: 10px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _getControls(): ControlButton[] | undefined {
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
if (!stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const state = stateObj.state;
|
||||
|
||||
if (UNAVAILABLE_STATES.includes(state)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (state === "off") {
|
||||
return supportsFeature(stateObj, SUPPORT_TURN_ON)
|
||||
? [
|
||||
{
|
||||
icon: "hass:power",
|
||||
action: "turn_on",
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (state === "idle") {
|
||||
return supportsFeature(stateObj, SUPPORTS_PLAY)
|
||||
? [
|
||||
{
|
||||
icon: "hass:play",
|
||||
action: "media_play",
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const buttons: ControlButton[] = [];
|
||||
|
||||
if (supportsFeature(stateObj, SUPPORT_TURN_OFF)) {
|
||||
buttons.push({
|
||||
icon: "hass:power",
|
||||
action: "turn_off",
|
||||
});
|
||||
}
|
||||
|
||||
if (supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK)) {
|
||||
buttons.push({
|
||||
icon: "hass:skip-previous",
|
||||
action: "media_previous_track",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
(state === "playing" &&
|
||||
(supportsFeature(stateObj, SUPPORT_PAUSE) ||
|
||||
supportsFeature(stateObj, SUPPORT_STOP))) ||
|
||||
(state === "paused" && supportsFeature(stateObj, SUPPORTS_PLAY))
|
||||
) {
|
||||
buttons.push({
|
||||
icon:
|
||||
state !== "playing"
|
||||
? "hass:play"
|
||||
: supportsFeature(stateObj, SUPPORT_PAUSE)
|
||||
? "hass:pause"
|
||||
: "hass:stop",
|
||||
action: "media_play_pause",
|
||||
});
|
||||
}
|
||||
|
||||
if (supportsFeature(stateObj, SUPPORT_NEXT_TRACK)) {
|
||||
buttons.push({
|
||||
icon: "hass:skip-next",
|
||||
action: "media_next_track",
|
||||
});
|
||||
}
|
||||
|
||||
return buttons.length > 0 ? buttons : undefined;
|
||||
}
|
||||
|
||||
private _handleClick(e: MouseEvent): void {
|
||||
this.hass!.callService(
|
||||
"media_player",
|
||||
(e.currentTarget! as HTMLElement).getAttribute("action")!,
|
||||
{
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _toggleMute() {
|
||||
this.hass!.callService("media_player", "volume_mute", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
is_volume_muted: !this.stateObj!.attributes.is_volume_muted,
|
||||
});
|
||||
}
|
||||
|
||||
private _selectedValueChanged(e: Event): void {
|
||||
this.hass!.callService("media_player", "volume_set", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
volume_level:
|
||||
Number((e.currentTarget! as HTMLElement).getAttribute("value")!) / 100,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSourceChanged(e: CustomEvent) {
|
||||
const newVal = e.detail.item.itemName;
|
||||
|
||||
if (!newVal || this.stateObj!.attributes.source === newVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("media_player", "select_source", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
source: newVal,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSoundModeChanged(e: CustomEvent) {
|
||||
const newVal = e.detail.item.itemName;
|
||||
|
||||
if (!newVal || this.stateObj?.attributes.sound_mode === newVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("media_player", "select_sound_mode", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
sound_mode: newVal,
|
||||
});
|
||||
}
|
||||
|
||||
private _ttsCheckForEnter(e: KeyboardEvent) {
|
||||
if (e.keyCode === 13) this._sendTTS();
|
||||
}
|
||||
|
||||
private _sendTTS() {
|
||||
const ttsInput = this._ttsInput;
|
||||
if (!ttsInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const services = this.hass.services.tts;
|
||||
const serviceKeys = Object.keys(services).sort();
|
||||
|
||||
const service = serviceKeys.find((key) => key.indexOf("_say") !== -1);
|
||||
|
||||
if (!service) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("tts", service, {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
message: ttsInput.value,
|
||||
});
|
||||
ttsInput.value = "";
|
||||
}
|
||||
|
||||
private _showBrowseMedia(): void {
|
||||
showMediaBrowserDialog(this, {
|
||||
action: "play",
|
||||
entityId: this.stateObj!.entity_id,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
|
||||
this._playMedia(
|
||||
pickedMedia.media_content_id,
|
||||
pickedMedia.media_content_type
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _playMedia(media_content_id: string, media_content_type: string) {
|
||||
this.hass!.callService("media_player", "play_media", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
media_content_id,
|
||||
media_content_type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"more-info-media_player": MoreInfoMediaPlayer;
|
||||
}
|
||||
}
|
@@ -2,8 +2,6 @@ import { ExternalMessaging } from "./external_messaging";
|
||||
|
||||
export interface ExternalConfig {
|
||||
hasSettingsScreen: boolean;
|
||||
canWriteTag: boolean;
|
||||
hasExoPlayer: boolean;
|
||||
}
|
||||
|
||||
export const getExternalConfig = (
|
||||
|
@@ -22,14 +22,6 @@
|
||||
.header img {
|
||||
margin-right: 16px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #111111;
|
||||
color: #e1e1e1;
|
||||
--primary-text-color: #e1e1e1;
|
||||
--secondary-text-color: #9b9b9b;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
@@ -48,7 +48,7 @@
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: var(--primary-background-color, #111111);
|
||||
background-color: #111111;
|
||||
}
|
||||
#ha-init-skeleton::before {
|
||||
background-color: #1c1c1c;
|
||||
|
@@ -23,14 +23,6 @@
|
||||
.header img {
|
||||
margin-right: 16px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #111111;
|
||||
color: #e1e1e1;
|
||||
--primary-text-color: #e1e1e1;
|
||||
--secondary-text-color: #9b9b9b;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
@@ -5,15 +5,15 @@ import type { AppDrawerElement } from "@polymer/app-layout/app-drawer/app-drawer
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { toggleAttribute } from "../common/dom/toggle_attribute";
|
||||
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
@@ -49,15 +49,7 @@ class HomeAssistantMain extends LitElement {
|
||||
const disableSwipe =
|
||||
!sidebarNarrow || NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1;
|
||||
|
||||
// Style block in render because of the mixin that is not supported
|
||||
return html`
|
||||
<style>
|
||||
app-drawer {
|
||||
--app-drawer-content-container: {
|
||||
background-color: var(--primary-background-color, #fff);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<app-drawer-layout
|
||||
fullbleed
|
||||
.forceNarrow=${sidebarNarrow}
|
||||
|
@@ -8,9 +8,9 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -27,7 +27,6 @@ import type { PolymerChangedEvent } from "../polymer-types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
const amsterdam = [52.3731339, 4.8903147];
|
||||
const mql = matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
@customElement("onboarding-core-config")
|
||||
class OnboardingCoreConfig extends LitElement {
|
||||
@@ -94,7 +93,6 @@ class OnboardingCoreConfig extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.location=${this._locationValue}
|
||||
.fitZoom=${14}
|
||||
.darkMode=${mql.matches}
|
||||
@change=${this._locationChanged}
|
||||
></ha-location-editor>
|
||||
</div>
|
||||
|
@@ -1,85 +1,67 @@
|
||||
// @ts-ignore
|
||||
import fullcalendarStyle from "@fullcalendar/common/main.css";
|
||||
import { Calendar } from "@fullcalendar/core";
|
||||
import type { CalendarOptions } from "@fullcalendar/core";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
// @ts-ignore
|
||||
import daygridStyle from "@fullcalendar/daygrid/main.css";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import listPlugin from "@fullcalendar/list";
|
||||
// @ts-ignore
|
||||
import listStyle from "@fullcalendar/list/main.css";
|
||||
import "@material/mwc-button";
|
||||
import { mdiViewAgenda, mdiViewDay, mdiViewModule, mdiViewWeek } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
LitElement,
|
||||
CSSResult,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
unsafeCSS,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoize from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button-toggle-group";
|
||||
import { Calendar } from "@fullcalendar/core";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
// @ts-ignore
|
||||
import fullcalendarStyle from "@fullcalendar/core/main.css";
|
||||
// @ts-ignore
|
||||
import daygridStyle from "@fullcalendar/daygrid/main.css";
|
||||
import "@material/mwc-button";
|
||||
|
||||
import "../../components/ha-icon-button";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import "../../components/ha-button-toggle-group";
|
||||
|
||||
import type {
|
||||
CalendarEvent,
|
||||
CalendarViewChanged,
|
||||
FullCalendarView,
|
||||
HomeAssistant,
|
||||
CalendarEvent,
|
||||
ToggleButton,
|
||||
HomeAssistant,
|
||||
} from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-full-calendar": HAFullCalendar;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"view-changed": CalendarViewChanged;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultFullCalendarConfig: CalendarOptions = {
|
||||
const fullCalendarConfig = {
|
||||
headerToolbar: false,
|
||||
plugins: [dayGridPlugin, listPlugin, interactionPlugin],
|
||||
plugins: [dayGridPlugin],
|
||||
initialView: "dayGridMonth",
|
||||
dayMaxEventRows: true,
|
||||
height: "parent",
|
||||
eventDisplay: "list-item",
|
||||
};
|
||||
|
||||
const viewButtons: ToggleButton[] = [
|
||||
{ label: "Month View", value: "dayGridMonth", iconPath: mdiViewModule },
|
||||
{ label: "Week View", value: "dayGridWeek", iconPath: mdiViewWeek },
|
||||
{ label: "Day View", value: "dayGridDay", iconPath: mdiViewDay },
|
||||
{ label: "List View", value: "listWeek", iconPath: mdiViewAgenda },
|
||||
{ label: "Month View", value: "dayGridMonth", icon: "hass:view-module" },
|
||||
{ label: "Week View", value: "dayGridWeek", icon: "hass:view-week" },
|
||||
{ label: "Day View", value: "dayGridDay", icon: "hass:view-day" },
|
||||
];
|
||||
|
||||
class HAFullCalendar extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
@property() public events: CalendarEvent[] = [];
|
||||
|
||||
@property({ attribute: false }) public events: CalendarEvent[] = [];
|
||||
|
||||
@property({ attribute: false }) public views: FullCalendarView[] = [
|
||||
"dayGridMonth",
|
||||
"dayGridWeek",
|
||||
"dayGridDay",
|
||||
];
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public narrow!: boolean;
|
||||
|
||||
@internalProperty() private calendar?: Calendar;
|
||||
|
||||
@internalProperty() private _activeView: FullCalendarView = "dayGridMonth";
|
||||
@internalProperty() private _activeView = "dayGridMonth";
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const viewToggleButtons = this._viewToggleButtons(this.views);
|
||||
|
||||
return html`
|
||||
${this.calendar
|
||||
? html`
|
||||
@@ -114,12 +96,27 @@ class HAFullCalendar extends LitElement {
|
||||
${this.calendar.view.title}
|
||||
</h1>
|
||||
<ha-button-toggle-group
|
||||
.buttons=${viewToggleButtons}
|
||||
.buttons=${viewButtons}
|
||||
.active=${this._activeView}
|
||||
@value-changed=${this._handleView}
|
||||
></ha-button-toggle-group>
|
||||
`
|
||||
: html`
|
||||
<div class="controls">
|
||||
<mwc-button
|
||||
outlined
|
||||
class="today"
|
||||
@click=${this._handleToday}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.calendar.today"
|
||||
)}</mwc-button
|
||||
>
|
||||
<ha-button-toggle-group
|
||||
.buttons=${viewButtons}
|
||||
.active=${this._activeView}
|
||||
@value-changed=${this._handleView}
|
||||
></ha-button-toggle-group>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<h1>
|
||||
${this.calendar.view.title}
|
||||
@@ -141,21 +138,6 @@ class HAFullCalendar extends LitElement {
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<mwc-button
|
||||
outlined
|
||||
class="today"
|
||||
@click=${this._handleToday}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.calendar.today"
|
||||
)}</mwc-button
|
||||
>
|
||||
<ha-button-toggle-group
|
||||
.buttons=${viewToggleButtons}
|
||||
.active=${this._activeView}
|
||||
@value-changed=${this._handleView}
|
||||
></ha-button-toggle-group>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
@@ -175,25 +157,14 @@ class HAFullCalendar extends LitElement {
|
||||
this.calendar.removeAllEventSources();
|
||||
this.calendar.addEventSource(this.events);
|
||||
}
|
||||
|
||||
if (changedProps.has("views") && !this.views.includes(this._activeView)) {
|
||||
this._activeView = this.views[0];
|
||||
this.calendar!.changeView(this._activeView);
|
||||
this._fireViewChanged();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
const config: CalendarOptions = {
|
||||
...defaultFullCalendarConfig,
|
||||
locale: this.hass.language,
|
||||
};
|
||||
|
||||
config.dateClick = (info) => this._handleDateClick(info);
|
||||
config.eventClick = (info) => this._handleEventClick(info);
|
||||
const config = { ...fullCalendarConfig, locale: this.hass.language };
|
||||
|
||||
this.calendar = new Calendar(
|
||||
this.shadowRoot!.getElementById("calendar")!,
|
||||
// @ts-ignore
|
||||
config
|
||||
);
|
||||
|
||||
@@ -201,25 +172,6 @@ class HAFullCalendar extends LitElement {
|
||||
this._fireViewChanged();
|
||||
}
|
||||
|
||||
private _handleEventClick(info): void {
|
||||
if (info.view.type !== "dayGridMonth") {
|
||||
return;
|
||||
}
|
||||
|
||||
this._activeView = "dayGridDay";
|
||||
this.calendar!.changeView("dayGridDay");
|
||||
this.calendar!.gotoDate(info.event.startStr);
|
||||
}
|
||||
|
||||
private _handleDateClick(info): void {
|
||||
if (info.view.type !== "dayGridMonth") {
|
||||
return;
|
||||
}
|
||||
this._activeView = "dayGridDay";
|
||||
this.calendar!.changeView("dayGridDay");
|
||||
this.calendar!.gotoDate(info.dateStr);
|
||||
}
|
||||
|
||||
private _handleNext(): void {
|
||||
this.calendar!.next();
|
||||
this._fireViewChanged();
|
||||
@@ -249,21 +201,14 @@ class HAFullCalendar extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _viewToggleButtons = memoize((views) =>
|
||||
viewButtons.filter((button) =>
|
||||
views.includes(button.value as FullCalendarView)
|
||||
)
|
||||
);
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
${unsafeCSS(fullcalendarStyle)}
|
||||
${unsafeCSS(daygridStyle)}
|
||||
${unsafeCSS(listStyle)}
|
||||
|
||||
:host {
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--fc-theme-standard-border-color: var(--divider-color);
|
||||
@@ -317,15 +262,6 @@ class HAFullCalendar extends LitElement {
|
||||
#calendar {
|
||||
flex-grow: 1;
|
||||
background-color: var(--card-background-color);
|
||||
min-height: 400px;
|
||||
--fc-neutral-bg-color: var(--card-background-color);
|
||||
--fc-list-event-hover-bg-color: var(--card-background-color);
|
||||
--fc-theme-standard-border-color: var(--divider-color);
|
||||
--fc-border-color: var(--divider-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.fc-theme-standard .fc-scrollgrid {
|
||||
@@ -337,20 +273,15 @@ class HAFullCalendar extends LitElement {
|
||||
}
|
||||
|
||||
th.fc-col-header-cell.fc-day {
|
||||
color: var(--secondary-text-color);
|
||||
color: #70757a;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.fc-daygrid-dot-event:hover {
|
||||
background-color: inherit
|
||||
}
|
||||
|
||||
.fc-daygrid-day-top {
|
||||
text-align: center;
|
||||
padding-top: 5px;
|
||||
justify-content: center;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
table.fc-scrollgrid-sync-table
|
||||
@@ -365,21 +296,13 @@ class HAFullCalendar extends LitElement {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.fc .fc-daygrid-day-number {
|
||||
padding: 3px !important;
|
||||
}
|
||||
|
||||
.fc .fc-daygrid-day.fc-day-today {
|
||||
td.fc-day-today {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
td.fc-day-today .fc-daygrid-day-top {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
td.fc-day-today .fc-daygrid-day-number {
|
||||
height: 24px;
|
||||
color: var(--text-primary-color) !important;
|
||||
color: var(--text-primary-color);
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
@@ -419,66 +342,6 @@ class HAFullCalendar extends LitElement {
|
||||
.fc-popover-header {
|
||||
background-color: var(--secondary-background-color) !important;
|
||||
}
|
||||
|
||||
.fc-theme-standard .fc-list-day-frame {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.fc-list.fc-view,
|
||||
.fc-list-event.fc-event td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fc-list-day.fc-day th {
|
||||
border-bottom: none;
|
||||
border-top: 1px solid var(--fc-theme-standard-border-color, #ddd) !important;
|
||||
}
|
||||
|
||||
.fc-list-day-text {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.fc-list-day-side-text {
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.fc-list-table td,
|
||||
.fc-list-day-frame {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
:host([narrow]) .fc-dayGridMonth-view
|
||||
.fc-daygrid-dot-event
|
||||
.fc-event-time,
|
||||
:host([narrow]) .fc-dayGridMonth-view
|
||||
.fc-daygrid-dot-event
|
||||
.fc-event-title,
|
||||
:host([narrow]) .fc-dayGridMonth-view .fc-daygrid-day-bottom {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([narrow]) .fc .fc-dayGridMonth-view .fc-daygrid-event-harness-abs {
|
||||
visibility: visible !important;
|
||||
position: static;
|
||||
}
|
||||
|
||||
:host([narrow]) .fc-dayGridMonth-view .fc-daygrid-day-events {
|
||||
display: flex;
|
||||
min-height: 2em !important;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
max-height: 2em;
|
||||
height: 2em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host([narrow]) .fc-dayGridMonth-view .fc-scrollgrid-sync-table {
|
||||
overflow: hidden;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -53,6 +53,12 @@ class PanelCalendar extends LitElement {
|
||||
selected: true,
|
||||
calendar,
|
||||
}));
|
||||
|
||||
if (!this._start || !this._end) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._fetchEvents(this._start, this._end, this._selectedCalendars);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -82,8 +88,8 @@ class PanelCalendar extends LitElement {
|
||||
<mwc-formfield .label=${selCal.calendar.name}>
|
||||
<mwc-checkbox
|
||||
style=${styleMap({
|
||||
"--mdc-theme-secondary": selCal.calendar
|
||||
.backgroundColor!,
|
||||
"--mdc-theme-secondary":
|
||||
selCal.calendar.backgroundColor,
|
||||
})}
|
||||
.value=${selCal.calendar.entity_id}
|
||||
.checked=${selCal.selected}
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import "@material/mwc-fab";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import {
|
||||
@@ -18,8 +16,7 @@ import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "@material/mwc-fab";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
createAreaRegistryEntry,
|
||||
@@ -29,6 +26,8 @@ import {
|
||||
devicesInArea,
|
||||
} from "../../../data/device_registry";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
@@ -38,6 +37,7 @@ import {
|
||||
loadAreaRegistryDetailDialog,
|
||||
showAreaRegistryDetailDialog,
|
||||
} from "./show-dialog-area-registry-detail";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
|
||||
@customElement("ha-config-areas-dashboard")
|
||||
export class HaConfigAreasDashboard extends LitElement {
|
||||
|
@@ -34,7 +34,6 @@ import "./types/ha-automation-trigger-time";
|
||||
import "./types/ha-automation-trigger-time_pattern";
|
||||
import "./types/ha-automation-trigger-webhook";
|
||||
import "./types/ha-automation-trigger-zone";
|
||||
import "./types/ha-automation-trigger-tag";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
|
||||
@@ -47,7 +46,6 @@ const OPTIONS = [
|
||||
"mqtt",
|
||||
"numeric_state",
|
||||
"sun",
|
||||
"tag",
|
||||
"template",
|
||||
"time",
|
||||
"time_pattern",
|
||||
|
@@ -1,72 +0,0 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { TagTrigger } from "../../../../../data/automation";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { TriggerElement } from "../ha-automation-trigger-row";
|
||||
import { Tag, fetchTags } from "../../../../../data/tag";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-automation-trigger-tag")
|
||||
export class HaTagTrigger extends LitElement implements TriggerElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public trigger!: TagTrigger;
|
||||
|
||||
@internalProperty() private _tags: Tag[] = [];
|
||||
|
||||
public static get defaultConfig() {
|
||||
return { tag_id: "" };
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._fetchTags();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const { tag_id } = this.trigger;
|
||||
return html`
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.tag.label"
|
||||
)}
|
||||
?disabled=${this._tags.length === 0}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${tag_id}
|
||||
attr-for-selected="tag_id"
|
||||
@iron-select=${this._tagChanged}
|
||||
>
|
||||
${this._tags.map(
|
||||
(tag) => html`
|
||||
<paper-item tag_id=${tag.id} .tag=${tag}>
|
||||
${tag.name || tag.id}
|
||||
</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchTags() {
|
||||
this._tags = await fetchTags(this.hass);
|
||||
}
|
||||
|
||||
private _tagChanged(ev) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.trigger,
|
||||
tag_id: ev.detail.item.tag.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
@@ -12,13 +12,7 @@ import {
|
||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||
import { haStyle } from "../../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../../types";
|
||||
import {
|
||||
OZWDevice,
|
||||
fetchOZWNodeStatus,
|
||||
getIdentifiersFromDevice,
|
||||
OZWNodeIdentifiers,
|
||||
} from "../../../../../../data/ozw";
|
||||
import { showOZWRefreshNodeDialog } from "../../../../integrations/integration-panels/ozw/show-dialog-ozw-refresh-node";
|
||||
import { OZWDevice, fetchOZWNodeStatus } from "../../../../../../data/ozw";
|
||||
|
||||
@customElement("ha-device-info-ozw")
|
||||
export class HaDeviceInfoOzw extends LitElement {
|
||||
@@ -26,34 +20,26 @@ export class HaDeviceInfoOzw extends LitElement {
|
||||
|
||||
@property() public device!: DeviceRegistryEntry;
|
||||
|
||||
@property()
|
||||
private node_id = 0;
|
||||
|
||||
@property()
|
||||
private ozw_instance = 1;
|
||||
|
||||
@internalProperty() private _ozwDevice?: OZWDevice;
|
||||
|
||||
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;
|
||||
|
||||
this._fetchNodeDetails();
|
||||
this._fetchNodeDetails(this.device);
|
||||
}
|
||||
}
|
||||
|
||||
protected async _fetchNodeDetails() {
|
||||
protected async _fetchNodeDetails(device) {
|
||||
const ozwIdentifier = device.identifiers.find(
|
||||
(identifier) => identifier[0] === "ozw"
|
||||
);
|
||||
if (!ozwIdentifier) {
|
||||
return;
|
||||
}
|
||||
const identifiers = ozwIdentifier[1].split(".");
|
||||
this._ozwDevice = await fetchOZWNodeStatus(
|
||||
this.hass,
|
||||
this.ozw_instance,
|
||||
this.node_id
|
||||
identifiers[0],
|
||||
identifiers[1]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,19 +69,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,
|
||||
|
@@ -119,11 +119,9 @@ export class HaDeviceActionsZha extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.hass.callService("zha", "remove", {
|
||||
this.hass.callService("zha", "remove", {
|
||||
ieee_address: this._zhaDevice!.ieee,
|
||||
});
|
||||
|
||||
history.back();
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
|
@@ -44,7 +44,6 @@ import "./device-detail/ha-device-entities-card";
|
||||
import "./device-detail/ha-device-info-card";
|
||||
import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
|
||||
stateName?: string | null;
|
||||
@@ -308,15 +307,11 @@ export class HaConfigDevicePage extends LitElement {
|
||||
: "";
|
||||
})
|
||||
: html`
|
||||
<paper-item class="no-link">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.add_prompt",
|
||||
"name",
|
||||
this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations"
|
||||
)
|
||||
)}
|
||||
</paper-item>
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.no_automations"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
</ha-card>
|
||||
`
|
||||
@@ -380,15 +375,11 @@ export class HaConfigDevicePage extends LitElement {
|
||||
: "";
|
||||
})
|
||||
: html`
|
||||
<paper-item class="no-link">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.add_prompt",
|
||||
"name",
|
||||
this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes"
|
||||
)
|
||||
)}
|
||||
</paper-item>
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.no_scenes"
|
||||
)}</paper-item
|
||||
>
|
||||
`
|
||||
}
|
||||
</ha-card>
|
||||
@@ -437,13 +428,9 @@ export class HaConfigDevicePage extends LitElement {
|
||||
: html`
|
||||
<paper-item class="no-link">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.add_prompt",
|
||||
"name",
|
||||
this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts"
|
||||
)
|
||||
)}
|
||||
</paper-item>
|
||||
"ui.panel.config.devices.script.no_scripts"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
</ha-card>
|
||||
`
|
||||
@@ -562,14 +549,11 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
const renameEntityid =
|
||||
this.showAdvanced &&
|
||||
(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
confirm(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_ids"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_ids_warning"
|
||||
),
|
||||
}));
|
||||
)
|
||||
);
|
||||
|
||||
const updateProms = entities.map((entity) => {
|
||||
const name = entity.name || entity.stateName;
|
||||
@@ -718,10 +702,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
ha-card {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
ha-card a {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -25,8 +25,8 @@ import {
|
||||
} from "../../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
findBatteryChargingEntity,
|
||||
findBatteryEntity,
|
||||
findBatteryChargingEntity,
|
||||
} from "../../../data/entity_registry";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
@@ -181,8 +181,8 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = narrow
|
||||
(narrow: boolean): DataTableColumnContainer =>
|
||||
narrow
|
||||
? {
|
||||
name: {
|
||||
title: "Device",
|
||||
@@ -199,6 +199,36 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
`;
|
||||
},
|
||||
},
|
||||
battery_entity: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.battery"
|
||||
),
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
width: "90px",
|
||||
template: (
|
||||
batteryEntityPair: DeviceRowData["battery_entity"]
|
||||
) => {
|
||||
const battery =
|
||||
batteryEntityPair && batteryEntityPair[0]
|
||||
? this.hass.states[batteryEntityPair[0]]
|
||||
: undefined;
|
||||
const batteryCharging =
|
||||
batteryEntityPair && batteryEntityPair[1]
|
||||
? this.hass.states[batteryEntityPair[1]]
|
||||
: undefined;
|
||||
return battery
|
||||
? html`
|
||||
${isNaN(battery.state as any) ? "-" : battery.state}%
|
||||
<ha-battery-icon
|
||||
.hass=${this.hass!}
|
||||
.batteryStateObj=${battery}
|
||||
.batteryChargingStateObj=${batteryCharging}
|
||||
></ha-battery-icon>
|
||||
`
|
||||
: html` - `;
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: {
|
||||
@@ -210,69 +240,70 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
},
|
||||
};
|
||||
|
||||
columns.manufacturer = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.manufacturer"
|
||||
),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.model = {
|
||||
title: this.hass.localize("ui.panel.config.devices.data_table.model"),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.area = {
|
||||
title: this.hass.localize("ui.panel.config.devices.data_table.area"),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.integration = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.integration"
|
||||
),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.battery_entity = {
|
||||
title: this.hass.localize("ui.panel.config.devices.data_table.battery"),
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
width: narrow ? "90px" : "15%",
|
||||
maxWidth: "90px",
|
||||
template: (batteryEntityPair: DeviceRowData["battery_entity"]) => {
|
||||
const battery =
|
||||
batteryEntityPair && batteryEntityPair[0]
|
||||
? this.hass.states[batteryEntityPair[0]]
|
||||
: undefined;
|
||||
const batteryCharging =
|
||||
batteryEntityPair && batteryEntityPair[1]
|
||||
? this.hass.states[batteryEntityPair[1]]
|
||||
: undefined;
|
||||
return battery && !isNaN(battery.state as any)
|
||||
? html`
|
||||
${battery.state}%
|
||||
<ha-battery-icon
|
||||
.hass=${this.hass!}
|
||||
.batteryStateObj=${battery}
|
||||
.batteryChargingStateObj=${batteryCharging}
|
||||
></ha-battery-icon>
|
||||
`
|
||||
: html` - `;
|
||||
},
|
||||
};
|
||||
return columns;
|
||||
}
|
||||
manufacturer: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.manufacturer"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
model: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.model"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
area: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.area"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
integration: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.integration"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
battery_entity: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.battery"
|
||||
),
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
width: "15%",
|
||||
maxWidth: "90px",
|
||||
template: (
|
||||
batteryEntityPair: DeviceRowData["battery_entity"]
|
||||
) => {
|
||||
const battery =
|
||||
batteryEntityPair && batteryEntityPair[0]
|
||||
? this.hass.states[batteryEntityPair[0]]
|
||||
: undefined;
|
||||
const batteryCharging =
|
||||
batteryEntityPair && batteryEntityPair[1]
|
||||
? this.hass.states[batteryEntityPair[1]]
|
||||
: undefined;
|
||||
return battery && !isNaN(battery.state as any)
|
||||
? html`
|
||||
${battery.state}%
|
||||
<ha-battery-icon
|
||||
.hass=${this.hass!}
|
||||
.batteryStateObj=${battery}
|
||||
.batteryChargingStateObj=${batteryCharging}
|
||||
></ha-battery-icon>
|
||||
`
|
||||
: html` - `;
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
public constructor() {
|
||||
|
@@ -32,7 +32,6 @@ import {
|
||||
mdiInformation,
|
||||
mdiMathLog,
|
||||
mdiPencil,
|
||||
mdiNfcVariant,
|
||||
} from "@mdi/js";
|
||||
|
||||
declare global {
|
||||
@@ -100,15 +99,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
core: true,
|
||||
},
|
||||
],
|
||||
experimental: [
|
||||
{
|
||||
component: "tags",
|
||||
path: "/config/tags",
|
||||
translationKey: "ui.panel.config.tags.caption",
|
||||
iconPath: mdiNfcVariant,
|
||||
core: true,
|
||||
},
|
||||
],
|
||||
lovelace: [
|
||||
{
|
||||
component: "lovelace",
|
||||
@@ -205,13 +195,6 @@ class HaPanelConfig extends HassRouterPage {
|
||||
/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation"
|
||||
),
|
||||
},
|
||||
tags: {
|
||||
tag: "ha-config-tags",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "panel-config-tags" */ "./tags/ha-config-tags"
|
||||
),
|
||||
},
|
||||
cloud: {
|
||||
tag: "ha-config-cloud",
|
||||
load: () =>
|
||||
@@ -352,13 +335,6 @@ class HaPanelConfig extends HassRouterPage {
|
||||
/* webpackChunkName: "panel-config-mqtt" */ "./integrations/integration-panels/mqtt/mqtt-config-panel"
|
||||
),
|
||||
},
|
||||
ozw: {
|
||||
tag: "ozw-config-router",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "panel-config-ozw" */ "./integrations/integration-panels/ozw/ozw-config-router"
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -14,7 +14,6 @@ import {
|
||||
ConfigEntry,
|
||||
updateConfigEntry,
|
||||
deleteConfigEntry,
|
||||
reloadConfigEntry,
|
||||
} from "../../../data/config_entries";
|
||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
@@ -29,8 +28,7 @@ import { haStyle } from "../../../resources/styles";
|
||||
import "../../../components/ha-icon-next";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { mdiDotsVertical, mdiOpenInNew } from "@mdi/js";
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
|
||||
export interface ConfigEntryUpdatedEvent {
|
||||
entry: ConfigEntry;
|
||||
@@ -57,10 +55,6 @@ const integrationsWithPanel = {
|
||||
buttonLocalizeKey: "ui.panel.config.zha.button",
|
||||
path: "/config/zha/dashboard",
|
||||
},
|
||||
ozw: {
|
||||
buttonLocalizeKey: "ui.panel.config.ozw.button",
|
||||
path: "/config/ozw/dashboard",
|
||||
},
|
||||
zwave: {
|
||||
buttonLocalizeKey: "ui.panel.config.zwave.button",
|
||||
path: "/config/zwave",
|
||||
@@ -230,7 +224,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<ha-button-menu corner="BOTTOM_START">
|
||||
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
|
||||
<mwc-icon-button
|
||||
.title=${this.hass.localize("ui.common.menu")}
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
@@ -238,7 +232,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
>
|
||||
<ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-list-item @request-selected="${this._handleSystemOptions}">
|
||||
<mwc-list-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.system_options"
|
||||
)}
|
||||
@@ -261,17 +255,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
</mwc-list-item>
|
||||
</a>
|
||||
`}
|
||||
${item.state === "loaded" && item.supports_unload
|
||||
? html`<mwc-list-item @request-selected="${this._handleReload}">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reload"
|
||||
)}
|
||||
</mwc-list-item>`
|
||||
: ""}
|
||||
<mwc-list-item
|
||||
class="warning"
|
||||
@request-selected="${this._handleDelete}"
|
||||
>
|
||||
<mwc-list-item class="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete"
|
||||
)}
|
||||
@@ -321,31 +305,17 @@ export class HaIntegrationCard extends LitElement {
|
||||
showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry);
|
||||
}
|
||||
|
||||
private _handleReload(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
const configEntry = ((ev.target as HTMLElement).closest("ha-card") as any)
|
||||
.configEntry;
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._showSystemOptions(configEntry);
|
||||
break;
|
||||
case 1:
|
||||
this._removeIntegration(configEntry);
|
||||
break;
|
||||
}
|
||||
this._reloadIntegration(
|
||||
((ev.target as HTMLElement).closest("ha-card") as any).configEntry
|
||||
);
|
||||
}
|
||||
|
||||
private _handleDelete(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
this._removeIntegration(
|
||||
((ev.target as HTMLElement).closest("ha-card") as any).configEntry
|
||||
);
|
||||
}
|
||||
|
||||
private _handleSystemOptions(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
this._showSystemOptions(
|
||||
((ev.target as HTMLElement).closest("ha-card") as any).configEntry
|
||||
);
|
||||
}
|
||||
|
||||
private _showSystemOptions(configEntry: ConfigEntry) {
|
||||
@@ -379,21 +349,6 @@ export class HaIntegrationCard extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _reloadIntegration(configEntry: ConfigEntry) {
|
||||
const entryId = configEntry.entry_id;
|
||||
|
||||
reloadConfigEntry(this.hass, entryId).then((result) => {
|
||||
const locale_key = result.require_restart
|
||||
? "reload_restart_confirm"
|
||||
: "reload_confirm";
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.${locale_key}`
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async _editEntryName(ev) {
|
||||
const configEntry = ev.target.closest("ha-card").configEntry;
|
||||
const newName = await showPromptDialog(this, {
|
||||
|
@@ -1,272 +0,0 @@
|
||||
import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
PropertyValues,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "../../../../../components/ha-code-editor";
|
||||
import "../../../../../components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { OZWRefreshNodeDialogParams } from "./show-dialog-ozw-refresh-node";
|
||||
|
||||
import {
|
||||
fetchOZWNodeMetadata,
|
||||
OZWDeviceMetaData,
|
||||
OZWDevice,
|
||||
nodeQueryStages,
|
||||
} from "../../../../../data/ozw";
|
||||
|
||||
@customElement("dialog-ozw-refresh-node")
|
||||
class DialogOZWRefreshNode extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _node_id?: number;
|
||||
|
||||
@internalProperty() private _ozw_instance = 1;
|
||||
|
||||
@internalProperty() private _nodeMetaData?: OZWDeviceMetaData;
|
||||
|
||||
@internalProperty() private _node?: OZWDevice;
|
||||
|
||||
@internalProperty() private _active = false;
|
||||
|
||||
@internalProperty() private _complete = false;
|
||||
|
||||
private _refreshDevicesTimeoutHandle?: number;
|
||||
|
||||
private _subscribed?: Promise<() => Promise<void>>;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribe();
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.update(changedProperties);
|
||||
if (changedProperties.has("node_id")) {
|
||||
this._fetchData();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
if (!this._node_id) {
|
||||
return;
|
||||
}
|
||||
const metaDataResponse = await fetchOZWNodeMetadata(
|
||||
this.hass,
|
||||
this._ozw_instance,
|
||||
this._node_id
|
||||
);
|
||||
|
||||
this._nodeMetaData = metaDataResponse.metadata;
|
||||
}
|
||||
|
||||
public async showDialog(params: OZWRefreshNodeDialogParams): Promise<void> {
|
||||
this._node_id = params.node_id;
|
||||
this._ozw_instance = params.ozw_instance;
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._node_id) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closing="${this._close}"
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.panel.config.ozw.refresh_node.title")
|
||||
)}
|
||||
>
|
||||
${this._complete
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.refresh_node.complete"
|
||||
)}
|
||||
</p>
|
||||
<mwc-button slot="primaryAction" @click=${this._close}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: html`
|
||||
${this._active
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
<div>
|
||||
<p>
|
||||
<b>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.refresh_node.refreshing_description"
|
||||
)}
|
||||
</b>
|
||||
</p>
|
||||
${this._node
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.refresh_node.node_status"
|
||||
)}:
|
||||
${this._node.node_query_stage}
|
||||
(${this.hass.localize(
|
||||
"ui.panel.config.ozw.refresh_node.step"
|
||||
)}
|
||||
${nodeQueryStages.indexOf(
|
||||
this._node.node_query_stage
|
||||
) + 1}/17)
|
||||
</p>
|
||||
<p>
|
||||
<em>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.node_query_stages." +
|
||||
this._node.node_query_stage.toLowerCase()
|
||||
)}</em
|
||||
>
|
||||
</p>
|
||||
`
|
||||
: ``}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.refresh_node.description"
|
||||
)}
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.refresh_node.battery_note"
|
||||
)}
|
||||
</p>
|
||||
`}
|
||||
${this._nodeMetaData?.WakeupHelp !== ""
|
||||
? html`
|
||||
<b>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.refresh_node.wakeup_header"
|
||||
)}
|
||||
${this._nodeMetaData!.Name}
|
||||
</b>
|
||||
<blockquote>
|
||||
${this._nodeMetaData!.WakeupHelp}
|
||||
<br />
|
||||
<em>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.refresh_node.wakeup_instructions_source"
|
||||
)}
|
||||
</em>
|
||||
</blockquote>
|
||||
`
|
||||
: ""}
|
||||
${!this._active
|
||||
? html`
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._startRefresh}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.refresh_node.start_refresh_button"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: html``}
|
||||
`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _startRefresh(): void {
|
||||
this._subscribe();
|
||||
}
|
||||
|
||||
private _handleMessage(message: any): void {
|
||||
if (message.type === "node_updated") {
|
||||
this._node = message;
|
||||
if (message.node_query_stage === "Complete") {
|
||||
this._unsubscribe();
|
||||
this._complete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _unsubscribe(): void {
|
||||
this._active = false;
|
||||
if (this._refreshDevicesTimeoutHandle) {
|
||||
clearTimeout(this._refreshDevicesTimeoutHandle);
|
||||
}
|
||||
if (this._subscribed) {
|
||||
this._subscribed.then((unsub) => unsub());
|
||||
this._subscribed = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _subscribe(): void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
this._active = true;
|
||||
this._subscribed = this.hass.connection.subscribeMessage(
|
||||
(message) => this._handleMessage(message),
|
||||
{
|
||||
type: "ozw/refresh_node_info",
|
||||
node_id: this._node_id,
|
||||
ozw_instance: this._ozw_instance,
|
||||
}
|
||||
);
|
||||
this._refreshDevicesTimeoutHandle = window.setTimeout(
|
||||
() => this._unsubscribe(),
|
||||
120000
|
||||
);
|
||||
}
|
||||
|
||||
private _close(): void {
|
||||
this._complete = false;
|
||||
this._node_id = undefined;
|
||||
this._node = undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
blockquote {
|
||||
display: block;
|
||||
background-color: #ddd;
|
||||
padding: 8px;
|
||||
margin: 8px 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
blockquote em {
|
||||
font-size: 0.9em;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-container ha-circular-progress {
|
||||
margin-right: 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-ozw-refresh-node": DialogOZWRefreshNode;
|
||||
}
|
||||
}
|
@@ -1,227 +0,0 @@
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@material/mwc-fab";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
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[] = [];
|
||||
|
||||
@customElement("ozw-config-dashboard")
|
||||
class OZWConfigDashboard 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;
|
||||
|
||||
@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 render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${ozwTabs}
|
||||
back-path="/config/integrations"
|
||||
>
|
||||
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
|
||||
<div slot="header">
|
||||
${this.hass.localize("ui.panel.config.ozw.select_instance.header")}
|
||||
</div>
|
||||
|
||||
<div slot="introduction">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.select_instance.introduction"
|
||||
)}
|
||||
</div>
|
||||
${this._instances.length > 0
|
||||
? html`
|
||||
${this._instances.map((instance) => {
|
||||
let status = "unknown";
|
||||
let icon = mdiCircle;
|
||||
if (networkOnlineStatuses.includes(instance.Status)) {
|
||||
status = "online";
|
||||
icon = mdiCheckCircle;
|
||||
}
|
||||
if (networkStartingStatuses.includes(instance.Status)) {
|
||||
status = "starting";
|
||||
}
|
||||
if (networkOfflineStatuses.includes(instance.Status)) {
|
||||
status = "offline";
|
||||
icon = mdiCloseCircle;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<a
|
||||
href="/config/ozw/network/${instance.ozw_instance}"
|
||||
aria-role="option"
|
||||
tabindex="-1"
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-svg-icon .path=${mdiZWave} slot="item-icon">
|
||||
</ha-svg-icon>
|
||||
<paper-item-body>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.common.instance"
|
||||
)}
|
||||
${instance.ozw_instance}
|
||||
<div secondary>
|
||||
<ha-svg-icon
|
||||
.path=${icon}
|
||||
class="network-status-icon ${status}"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.network_status." + status
|
||||
)}
|
||||
-
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.network_status.details." +
|
||||
instance.Status.toLowerCase()
|
||||
)}<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.common.controller"
|
||||
)}
|
||||
: ${instance.getControllerPath}<br />
|
||||
OZWDaemon ${instance.OZWDaemon_Version} (OpenZWave
|
||||
${instance.OpenZWave_Version})
|
||||
</div>
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
</ha-card>
|
||||
`;
|
||||
})}
|
||||
`
|
||||
: ``}
|
||||
</ha-config-section>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-card:last-child {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
ha-config-section {
|
||||
margin-top: -12px;
|
||||
}
|
||||
:host([narrow]) ha-config-section {
|
||||
margin-top: -20px;
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
ha-card a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
paper-item-body {
|
||||
margin: 16px 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
position: relative;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
ha-svg-icon.network-status-icon {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
.online {
|
||||
color: green;
|
||||
}
|
||||
.starting {
|
||||
color: orange;
|
||||
}
|
||||
.offline {
|
||||
color: red;
|
||||
}
|
||||
ha-svg-icon,
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.iron-selected paper-item::before,
|
||||
a:not(.iron-selected):focus::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
transition: opacity 15ms linear;
|
||||
will-change: opacity;
|
||||
}
|
||||
a:not(.iron-selected):focus::before {
|
||||
background-color: currentColor;
|
||||
opacity: var(--dark-divider-opacity);
|
||||
}
|
||||
.iron-selected paper-item:focus::before,
|
||||
.iron-selected:focus paper-item::before {
|
||||
opacity: 0.2;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ozw-config-dashboard": OZWConfigDashboard;
|
||||
}
|
||||
}
|
@@ -1,253 +0,0 @@
|
||||
import "@material/mwc-fab";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/buttons/ha-call-service-button";
|
||||
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";
|
||||
|
||||
export const ozwTabs: PageNavigation[] = [];
|
||||
|
||||
@customElement("ozw-config-network")
|
||||
class OZWConfigNetwork 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 ozw_instance = 0;
|
||||
|
||||
@internalProperty() private _network?: OZWInstance;
|
||||
|
||||
@internalProperty() private _statistics?: OZWNetworkStatistics;
|
||||
|
||||
@internalProperty() private _status = "unknown";
|
||||
|
||||
@internalProperty() private _icon = mdiCircle;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.ozw_instance <= 0) {
|
||||
navigate(this, "/config/ozw/dashboard", true);
|
||||
}
|
||||
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}
|
||||
>
|
||||
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
|
||||
<div slot="header">
|
||||
${this.hass.localize("ui.panel.config.ozw.network.header")}
|
||||
</div>
|
||||
|
||||
<div slot="introduction">
|
||||
${this.hass.localize("ui.panel.config.ozw.network.introduction")}
|
||||
</div>
|
||||
${this._network
|
||||
? html`
|
||||
<ha-card class="content network-status">
|
||||
<div class="card-content">
|
||||
<div class="details">
|
||||
<ha-svg-icon
|
||||
.path=${this._icon}
|
||||
class="network-status-icon ${this._status}"
|
||||
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
|
||||
)}
|
||||
<br />
|
||||
<small>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.network_status.details." +
|
||||
this._network.Status.toLowerCase()
|
||||
)}
|
||||
</small>
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.common.ozw_instance"
|
||||
)}
|
||||
${this._network.ozw_instance}
|
||||
${this._statistics
|
||||
? html`
|
||||
•
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.network.node_count",
|
||||
"count",
|
||||
this._statistics.node_count
|
||||
)}
|
||||
`
|
||||
: ``}
|
||||
<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.common.controller"
|
||||
)}:
|
||||
${this._network.getControllerPath}<br />
|
||||
OZWDaemon ${this._network.OZWDaemon_Version} (OpenZWave
|
||||
${this._network.OpenZWave_Version})
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
${this._generateServiceButton("add_node")}
|
||||
${this._generateServiceButton("remove_node")}
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: ``}
|
||||
</ha-config-section>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.online {
|
||||
color: green;
|
||||
}
|
||||
.starting {
|
||||
color: orange;
|
||||
}
|
||||
.offline {
|
||||
color: red;
|
||||
}
|
||||
.content {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
position: relative;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.network-status {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.network-status div.details {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.network-status ha-svg-icon {
|
||||
display: block;
|
||||
margin: 0px auto 16px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.network-status small {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
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-config-network": OZWConfigNetwork;
|
||||
}
|
||||
}
|
@@ -1,70 +0,0 @@
|
||||
import { customElement, property } from "lit-element";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../../../../layouts/hass-router-page";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
|
||||
@customElement("ozw-config-router")
|
||||
class OZWConfigRouter extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
private _configEntry = new URLSearchParams(window.location.search).get(
|
||||
"config_entry"
|
||||
);
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboard",
|
||||
showLoading: true,
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "ozw-config-dashboard",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "ozw-config-dashboard" */ "./ozw-config-dashboard"
|
||||
),
|
||||
},
|
||||
network: {
|
||||
tag: "ozw-config-network",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "ozw-config-network" */ "./ozw-config-network"
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(el): void {
|
||||
el.route = this.routeTail;
|
||||
el.hass = this.hass;
|
||||
el.isWide = this.isWide;
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ozw-config-router": OZWConfigRouter;
|
||||
}
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
|
||||
export interface OZWRefreshNodeDialogParams {
|
||||
ozw_instance: number;
|
||||
node_id: number;
|
||||
}
|
||||
|
||||
export const loadRefreshNodeDialog = () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-ozw-refresh-node" */ "./dialog-ozw-refresh-node"
|
||||
);
|
||||
|
||||
export const showOZWRefreshNodeDialog = (
|
||||
element: HTMLElement,
|
||||
refreshNodeDialogParams: OZWRefreshNodeDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-ozw-refresh-node",
|
||||
dialogImport: loadRefreshNodeDialog,
|
||||
dialogParams: refreshNodeDialogParams,
|
||||
});
|
||||
};
|
@@ -10,8 +10,6 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../components/ha-picture-upload";
|
||||
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
|
||||
import "../../../components/entity/ha-entities-picker";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/user/ha-user-picker";
|
||||
@@ -20,17 +18,9 @@ import { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { PersonDetailDialogParams } from "./show-dialog-person-detail";
|
||||
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
|
||||
const includeDomains = ["device_tracker"];
|
||||
|
||||
const cropOptions: CropOptions = {
|
||||
round: true,
|
||||
type: "image/jpeg",
|
||||
quality: 0.75,
|
||||
aspectRatio: 1,
|
||||
};
|
||||
|
||||
class DialogPersonDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@@ -40,8 +30,6 @@ class DialogPersonDetail extends LitElement {
|
||||
|
||||
@internalProperty() private _deviceTrackers!: string[];
|
||||
|
||||
@internalProperty() private _picture!: string | null;
|
||||
|
||||
@internalProperty() private _error?: string;
|
||||
|
||||
@internalProperty() private _params?: PersonDetailDialogParams;
|
||||
@@ -62,12 +50,10 @@ class DialogPersonDetail extends LitElement {
|
||||
this._name = this._params.entry.name || "";
|
||||
this._userId = this._params.entry.user_id || undefined;
|
||||
this._deviceTrackers = this._params.entry.device_trackers || [];
|
||||
this._picture = this._params.entry.picture || null;
|
||||
} else {
|
||||
this._name = "";
|
||||
this._userId = undefined;
|
||||
this._deviceTrackers = [];
|
||||
this._picture = null;
|
||||
}
|
||||
await this.updateComplete;
|
||||
}
|
||||
@@ -80,7 +66,7 @@ class DialogPersonDetail extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this._close}
|
||||
@closing=${this._close}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
@@ -106,14 +92,6 @@ class DialogPersonDetail extends LitElement {
|
||||
required
|
||||
auto-validate
|
||||
></paper-input>
|
||||
<ha-picture-upload
|
||||
.hass=${this.hass}
|
||||
.value=${this._picture}
|
||||
crop
|
||||
.cropOptions=${cropOptions}
|
||||
@change=${this._pictureChanged}
|
||||
></ha-picture-upload>
|
||||
|
||||
<ha-user-picker
|
||||
label="${this.hass!.localize(
|
||||
"ui.panel.config.person.detail.linked_user"
|
||||
@@ -219,11 +197,6 @@ class DialogPersonDetail extends LitElement {
|
||||
this._deviceTrackers = ev.detail.value;
|
||||
}
|
||||
|
||||
private _pictureChanged(ev: PolymerChangedEvent<string | null>) {
|
||||
this._error = undefined;
|
||||
this._picture = (ev.target as HaPictureUpload).value;
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
@@ -231,7 +204,6 @@ class DialogPersonDetail extends LitElement {
|
||||
name: this._name.trim(),
|
||||
device_trackers: this._deviceTrackers,
|
||||
user_id: this._userId || null,
|
||||
picture: this._picture,
|
||||
};
|
||||
if (this._params!.entry) {
|
||||
await this._params!.updateEntry(values);
|
||||
@@ -268,9 +240,6 @@ class DialogPersonDetail extends LitElement {
|
||||
.form {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
ha-picture-upload {
|
||||
display: block;
|
||||
}
|
||||
ha-user-picker {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import {
|
||||
css,
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
} 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;
|
||||
@@ -85,20 +84,11 @@ class HaConfigPerson extends LitElement {
|
||||
<ha-card class="storage">
|
||||
${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>`
|
||||
: ""}
|
||||
<paper-item @click=${this._openEditEntry} .entry=${entry}>
|
||||
<paper-item-body>
|
||||
${entry.name}
|
||||
</paper-item-body>
|
||||
</paper-icon-item>
|
||||
</paper-item>
|
||||
`;
|
||||
})}
|
||||
${this._storageItems.length === 0
|
||||
@@ -121,20 +111,11 @@ class HaConfigPerson extends LitElement {
|
||||
<ha-card header="Configuration.yaml persons">
|
||||
${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>`
|
||||
: ""}
|
||||
<paper-item>
|
||||
<paper-item-body>
|
||||
${entry.name}
|
||||
</paper-item-body>
|
||||
</paper-icon-item>
|
||||
</paper-item>
|
||||
`;
|
||||
})}
|
||||
</ha-card>
|
||||
@@ -247,21 +228,15 @@ 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;
|
||||
}
|
||||
paper-icon-item {
|
||||
paper-item {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
ha-card.storage paper-icon-item {
|
||||
ha-card.storage paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
@@ -16,7 +16,6 @@ import { HomeAssistant, Route } from "../../../types";
|
||||
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { isServiceLoaded } from "../../../common/config/is_service_loaded";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-card";
|
||||
@@ -36,20 +35,6 @@ const reloadableDomains = [
|
||||
"input_number",
|
||||
"input_datetime",
|
||||
"input_select",
|
||||
"template",
|
||||
"universal",
|
||||
"rest",
|
||||
"command_line",
|
||||
"filter",
|
||||
"statistics",
|
||||
"generic",
|
||||
"generic_thermostat",
|
||||
"homekit",
|
||||
"min_max",
|
||||
"history_stats",
|
||||
"trend",
|
||||
"ping",
|
||||
"filesize",
|
||||
];
|
||||
|
||||
@customElement("ha-config-server-control")
|
||||
@@ -179,20 +164,18 @@ export class HaConfigServerControl extends LitElement {
|
||||
"ui.panel.config.server_control.section.server_management.restart"
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
${!isComponentLoaded(this.hass, "hassio")
|
||||
? html` <ha-call-service-button
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
domain="homeassistant"
|
||||
service="stop"
|
||||
confirmation=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.confirm_stop"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.stop"
|
||||
)}
|
||||
</ha-call-service-button>`
|
||||
: ""}
|
||||
<ha-call-service-button
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
domain="homeassistant"
|
||||
service="stop"
|
||||
confirmation=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.confirm_stop"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.stop"
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
@@ -219,7 +202,7 @@ export class HaConfigServerControl extends LitElement {
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
${reloadableDomains.map((domain) =>
|
||||
isServiceLoaded(this.hass, domain, "reload")
|
||||
isComponentLoaded(this.hass, domain)
|
||||
? html`<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
|
@@ -1,210 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/map/ha-location-editor";
|
||||
import { Tag, UpdateTagParams } from "../../../data/tag";
|
||||
import { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { TagDetailDialogParams } from "./show-dialog-tag-detail";
|
||||
|
||||
@customElement("dialog-tag-detail")
|
||||
class DialogTagDetail extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _id?: string;
|
||||
|
||||
@internalProperty() private _name!: string;
|
||||
|
||||
@internalProperty() private _error?: string;
|
||||
|
||||
@internalProperty() private _params?: TagDetailDialogParams;
|
||||
|
||||
@internalProperty() private _submitting = false;
|
||||
|
||||
public showDialog(params: TagDetailDialogParams): void {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
if (this._params.entry) {
|
||||
this._name = this._params.entry.name || "";
|
||||
} else {
|
||||
this._id = "";
|
||||
this._name = "";
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._params.entry
|
||||
? this._params.entry.name || this._params.entry.id
|
||||
: this.hass!.localize("ui.panel.config.tags.detail.new_tag")
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
<div class="form">
|
||||
${this._params.entry
|
||||
? html`${this.hass!.localize(
|
||||
"ui.panel.config.tags.detail.tag_id"
|
||||
)}:
|
||||
${this._params.entry.id}`
|
||||
: ""}
|
||||
<paper-input
|
||||
dialogInitialFocus
|
||||
.value=${this._name}
|
||||
.configValue=${"name"}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label="${this.hass!.localize(
|
||||
"ui.panel.config.tags.detail.name"
|
||||
)}"
|
||||
.errorMessage="${this.hass!.localize(
|
||||
"ui.panel.config.tags.detail.required_error_msg"
|
||||
)}"
|
||||
required
|
||||
auto-validate
|
||||
></paper-input>
|
||||
${!this._params.entry
|
||||
? html` <paper-input
|
||||
.value=${this._id}
|
||||
.configValue=${"id"}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.tags.detail.tag_id"
|
||||
)}
|
||||
.placeholder=${this.hass!.localize(
|
||||
"ui.panel.config.tags.detail.tag_id_placeholder"
|
||||
)}
|
||||
></paper-input>`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
${this._params.entry
|
||||
? html`
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
class="warning"
|
||||
@click="${this._deleteEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.tags.detail.delete")}
|
||||
</mwc-button>
|
||||
`
|
||||
: html``}
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click="${this._updateEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this._params.entry
|
||||
? this.hass!.localize("ui.panel.config.tags.detail.update")
|
||||
: this.hass!.localize("ui.panel.config.tags.detail.create")}
|
||||
</mwc-button>
|
||||
${this._params.openWrite && !this._params.entry
|
||||
? html` <mwc-button
|
||||
slot="primaryAction"
|
||||
@click="${this._updateWriteEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.tags.detail.create_and_write"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
const configValue = (ev.target as any).configValue;
|
||||
|
||||
this._error = undefined;
|
||||
this[`_${configValue}`] = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
let newValue: Tag | undefined;
|
||||
try {
|
||||
const values: UpdateTagParams = {
|
||||
name: this._name.trim(),
|
||||
};
|
||||
if (this._params!.entry) {
|
||||
newValue = await this._params!.updateEntry!(values);
|
||||
} else {
|
||||
newValue = await this._params!.createEntry(values, this._id);
|
||||
}
|
||||
this.closeDialog();
|
||||
} catch (err) {
|
||||
this._error = err ? err.message : "Unknown error";
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
return newValue;
|
||||
}
|
||||
|
||||
private async _updateWriteEntry() {
|
||||
const openWrite = this._params?.openWrite;
|
||||
const tag = await this._updateEntry();
|
||||
if (!tag || !openWrite) {
|
||||
return;
|
||||
}
|
||||
openWrite(tag);
|
||||
}
|
||||
|
||||
private async _deleteEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.removeEntry!()) {
|
||||
this._params = undefined;
|
||||
}
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-tag-detail": DialogTagDetail;
|
||||
}
|
||||
}
|
@@ -1,293 +0,0 @@
|
||||
import "@material/mwc-fab";
|
||||
import { mdiCog, mdiContentDuplicate, mdiPlus, mdiRobot } from "@mdi/js";
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-relative-time";
|
||||
import { showAutomationEditor, TagTrigger } from "../../../data/automation";
|
||||
import {
|
||||
createTag,
|
||||
deleteTag,
|
||||
EVENT_TAG_SCANNED,
|
||||
fetchTags,
|
||||
Tag,
|
||||
TagScannedEvent,
|
||||
updateTag,
|
||||
UpdateTagParams,
|
||||
} from "../../../data/tag";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { getExternalConfig } from "../../../external_app/external_config";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showTagDetailDialog } from "./show-dialog-tag-detail";
|
||||
import "./tag-image";
|
||||
|
||||
export interface TagRowData extends Tag {
|
||||
last_scanned_datetime: Date | null;
|
||||
}
|
||||
|
||||
@customElement("ha-config-tags")
|
||||
export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
@internalProperty() private _tags: Tag[] = [];
|
||||
|
||||
@internalProperty() private _canWriteTags = false;
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(
|
||||
narrow: boolean,
|
||||
canWriteTags: boolean,
|
||||
_language
|
||||
): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
template: (_icon, tag) => html`<tag-image .tag=${tag}></tag-image>`,
|
||||
},
|
||||
display_name: {
|
||||
title: this.hass.localize("ui.panel.config.tags.headers.name"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
template: (name, tag: any) => html`${name}
|
||||
${narrow
|
||||
? html`<div class="secondary">
|
||||
${tag.last_scanned_datetime
|
||||
? html`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetimeObj=${tag.last_scanned_datetime}
|
||||
></ha-relative-time>`
|
||||
: this.hass.localize("ui.panel.config.tags.never_scanned")}
|
||||
</div>`
|
||||
: ""}`,
|
||||
},
|
||||
};
|
||||
if (!narrow) {
|
||||
columns.last_scanned_datetime = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.tags.headers.last_scanned"
|
||||
),
|
||||
sortable: true,
|
||||
direction: "desc",
|
||||
width: "20%",
|
||||
template: (last_scanned_datetime) => html`
|
||||
${last_scanned_datetime
|
||||
? html`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetimeObj=${last_scanned_datetime}
|
||||
></ha-relative-time>`
|
||||
: this.hass.localize("ui.panel.config.tags.never_scanned")}
|
||||
`,
|
||||
};
|
||||
}
|
||||
if (canWriteTags) {
|
||||
columns.write = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_write, tag: any) => html` <mwc-icon-button
|
||||
.tag=${tag}
|
||||
@click=${(ev: Event) =>
|
||||
this._openWrite((ev.currentTarget as any).tag)}
|
||||
title=${this.hass.localize("ui.panel.config.tags.write")}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiContentDuplicate}></ha-svg-icon>
|
||||
</mwc-icon-button>`,
|
||||
};
|
||||
}
|
||||
columns.automation = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_automation, tag: any) => html` <mwc-icon-button
|
||||
.tag=${tag}
|
||||
@click=${(ev: Event) =>
|
||||
this._createAutomation((ev.currentTarget as any).tag)}
|
||||
title=${this.hass.localize("ui.panel.config.tags.create_automation")}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiRobot}></ha-svg-icon>
|
||||
</mwc-icon-button>`,
|
||||
};
|
||||
columns.edit = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_settings, tag: any) => html` <mwc-icon-button
|
||||
.tag=${tag}
|
||||
@click=${(ev: Event) =>
|
||||
this._openDialog((ev.currentTarget as any).tag)}
|
||||
title=${this.hass.localize("ui.panel.config.tags.edit")}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiCog}></ha-svg-icon>
|
||||
</mwc-icon-button>`,
|
||||
};
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
private _data = memoizeOne((tags: Tag[]): TagRowData[] => {
|
||||
return tags.map((tag) => {
|
||||
return {
|
||||
...tag,
|
||||
display_name: tag.name || tag.id,
|
||||
last_scanned_datetime: tag.last_scanned
|
||||
? new Date(tag.last_scanned)
|
||||
: null,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._fetchTags();
|
||||
if (this.hass && this.hass.auth.external) {
|
||||
getExternalConfig(this.hass.auth.external).then((conf) => {
|
||||
this._canWriteTags = conf.canWriteTag;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
this.hass.connection.subscribeEvents<TagScannedEvent>((ev) => {
|
||||
const foundTag = this._tags.find((tag) => tag.id === ev.data.tag_id);
|
||||
if (!foundTag) {
|
||||
this._fetchTags();
|
||||
return;
|
||||
}
|
||||
foundTag.last_scanned = ev.time_fired;
|
||||
this._tags = [...this._tags];
|
||||
}, EVENT_TAG_SCANNED),
|
||||
];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.experimental}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this._canWriteTags,
|
||||
this.hass.language
|
||||
)}
|
||||
.data=${this._data(this._tags)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.tags.no_tags")}
|
||||
hasFab
|
||||
>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
title=${this.hass.localize("ui.panel.config.tags.add_tag")}
|
||||
@click=${this._addTag}
|
||||
>
|
||||
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
|
||||
</mwc-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchTags() {
|
||||
this._tags = await fetchTags(this.hass);
|
||||
}
|
||||
|
||||
private _openWrite(tag: Tag) {
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "tag/write",
|
||||
payload: { name: tag.name || null, tag: tag.id },
|
||||
});
|
||||
}
|
||||
|
||||
private _createAutomation(tag: Tag) {
|
||||
const data = {
|
||||
alias: this.hass.localize(
|
||||
"ui.panel.config.tags.automation_title",
|
||||
"name",
|
||||
tag.name || tag.id
|
||||
),
|
||||
trigger: [{ platform: "tag", tag_id: tag.id } as TagTrigger],
|
||||
};
|
||||
showAutomationEditor(this, data);
|
||||
}
|
||||
|
||||
private _addTag() {
|
||||
this._openDialog();
|
||||
}
|
||||
|
||||
private _openDialog(entry?: Tag) {
|
||||
showTagDetailDialog(this, {
|
||||
entry,
|
||||
openWrite: this._canWriteTags ? (tag) => this._openWrite(tag) : undefined,
|
||||
createEntry: (values, tagId) => this._createTag(values, tagId),
|
||||
updateEntry: entry
|
||||
? (values) => this._updateTag(entry, values)
|
||||
: undefined,
|
||||
removeEntry: entry ? () => this._removeTag(entry) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private async _createTag(
|
||||
values: Partial<UpdateTagParams>,
|
||||
tagId?: string
|
||||
): Promise<Tag> {
|
||||
const newTag = await createTag(this.hass, values, tagId);
|
||||
this._tags = [...this._tags, newTag];
|
||||
return newTag;
|
||||
}
|
||||
|
||||
private async _updateTag(
|
||||
selectedTag: Tag,
|
||||
values: Partial<UpdateTagParams>
|
||||
): Promise<Tag> {
|
||||
const updated = await updateTag(this.hass, selectedTag.id, values);
|
||||
this._tags = this._tags.map((tag) =>
|
||||
tag.id === selectedTag.id ? updated : tag
|
||||
);
|
||||
return updated;
|
||||
}
|
||||
|
||||
private async _removeTag(selectedTag: Tag) {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: "Remove tag?",
|
||||
text: `Are you sure you want to remove tag ${
|
||||
selectedTag.name || selectedTag.id
|
||||
}?`,
|
||||
dismissText: this.hass!.localize("ui.common.no"),
|
||||
confirmText: this.hass!.localize("ui.common.yes"),
|
||||
}))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await deleteTag(this.hass, selectedTag.id);
|
||||
this._tags = this._tags.filter((tag) => tag.id !== selectedTag.id);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-tags": HaConfigTags;
|
||||
}
|
||||
}
|
@@ -1,27 +0,0 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { Tag, UpdateTagParams } from "../../../data/tag";
|
||||
|
||||
export interface TagDetailDialogParams {
|
||||
entry?: Tag;
|
||||
openWrite?: (tag: Tag) => void;
|
||||
createEntry: (
|
||||
values: Partial<UpdateTagParams>,
|
||||
tagId?: string
|
||||
) => Promise<Tag>;
|
||||
updateEntry?: (updates: Partial<UpdateTagParams>) => Promise<Tag>;
|
||||
removeEntry?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const loadTagDetailDialog = () =>
|
||||
import(/* webpackChunkName: "dialog-tag-detail" */ "./dialog-tag-detail");
|
||||
|
||||
export const showTagDetailDialog = (
|
||||
element: HTMLElement,
|
||||
systemLogDetailParams: TagDetailDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-tag-detail",
|
||||
dialogImport: loadTagDetailDialog,
|
||||
dialogParams: systemLogDetailParams,
|
||||
});
|
||||
};
|
@@ -1,93 +0,0 @@
|
||||
import {
|
||||
property,
|
||||
customElement,
|
||||
LitElement,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { mdiNfcVariant } from "@mdi/js";
|
||||
import { TagRowData } from "./ha-config-tags";
|
||||
|
||||
@customElement("tag-image")
|
||||
export class HaTagImage extends LitElement {
|
||||
@property() public tag?: TagRowData;
|
||||
|
||||
private _timeout?: number;
|
||||
|
||||
protected updated() {
|
||||
const msSinceLastScaned = this.tag?.last_scanned_datetime
|
||||
? new Date().getTime() - this.tag.last_scanned_datetime.getTime()
|
||||
: undefined;
|
||||
|
||||
if (msSinceLastScaned && msSinceLastScaned < 1000) {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = undefined;
|
||||
this.classList.remove("just-scanned");
|
||||
requestAnimationFrame(() => this.classList.add("just-scanned"));
|
||||
} else {
|
||||
this.classList.add("just-scanned");
|
||||
}
|
||||
this._timeout = window.setTimeout(() => {
|
||||
this.classList.remove("just-scanned");
|
||||
this._timeout = undefined;
|
||||
}, 10000);
|
||||
} else if (!msSinceLastScaned || msSinceLastScaned > 10000) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = undefined;
|
||||
this.classList.remove("just-scanned");
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.tag) {
|
||||
return html``;
|
||||
}
|
||||
return html`<div class="container">
|
||||
<div class="image">
|
||||
<ha-svg-icon .path=${mdiNfcVariant}></ha-svg-icon>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.image {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-size: cover;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.container {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
:host(.just-scanned) .container {
|
||||
animation: glow 10s;
|
||||
}
|
||||
@keyframes glow {
|
||||
0% {
|
||||
box-shadow: 0px 0px 24px 0px rgba(var(--rgb-primary-color), 0);
|
||||
}
|
||||
10% {
|
||||
box-shadow: 0px 0px 24px 0px rgba(var(--rgb-primary-color), 1);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0px 0px 24px 0px rgba(var(--rgb-primary-color), 0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"tag-image": HaTagImage;
|
||||
}
|
||||
}
|
@@ -1,21 +1,20 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-formfield";
|
||||
import { createAuthForUser } from "../../../data/auth";
|
||||
import {
|
||||
createUser,
|
||||
@@ -28,6 +27,7 @@ import { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { AddUserDialogParams } from "./show-dialog-add-user";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
|
||||
@customElement("dialog-add-user")
|
||||
export class DialogAddUser extends LitElement {
|
||||
@@ -46,8 +46,6 @@ export class DialogAddUser extends LitElement {
|
||||
|
||||
@internalProperty() private _password?: string;
|
||||
|
||||
@internalProperty() private _passwordConfirm?: string;
|
||||
|
||||
@internalProperty() private _isAdmin?: boolean;
|
||||
|
||||
public showDialog(params: AddUserDialogParams) {
|
||||
@@ -55,7 +53,6 @@ export class DialogAddUser extends LitElement {
|
||||
this._name = "";
|
||||
this._username = "";
|
||||
this._password = "";
|
||||
this._passwordConfirm = "";
|
||||
this._isAdmin = false;
|
||||
this._error = undefined;
|
||||
this._loading = false;
|
||||
@@ -86,20 +83,17 @@ export class DialogAddUser extends LitElement {
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
<paper-input
|
||||
class="name"
|
||||
name="name"
|
||||
.label=${this.hass.localize("ui.panel.config.users.add_user.name")}
|
||||
.value=${this._name}
|
||||
required
|
||||
auto-validate
|
||||
autocapitalize="on"
|
||||
.errorMessage=${this.hass.localize("ui.common.error_required")}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
@value-changed=${this._nameChanged}
|
||||
@blur=${this._maybePopulateUsername}
|
||||
></paper-input>
|
||||
|
||||
<paper-input
|
||||
class="username"
|
||||
name="username"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.username"
|
||||
)}
|
||||
@@ -107,40 +101,20 @@ export class DialogAddUser extends LitElement {
|
||||
required
|
||||
auto-validate
|
||||
autocapitalize="none"
|
||||
@value-changed=${this._handleValueChanged}
|
||||
@value-changed=${this._usernameChanged}
|
||||
.errorMessage=${this.hass.localize("ui.common.error_required")}
|
||||
></paper-input>
|
||||
|
||||
<paper-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password"
|
||||
)}
|
||||
type="password"
|
||||
name="password"
|
||||
.value=${this._password}
|
||||
required
|
||||
auto-validate
|
||||
@value-changed=${this._handleValueChanged}
|
||||
@value-changed=${this._passwordChanged}
|
||||
.errorMessage=${this.hass.localize("ui.common.error_required")}
|
||||
></paper-input>
|
||||
|
||||
<paper-input
|
||||
label="${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password_confirm"
|
||||
)}"
|
||||
name="passwordConfirm"
|
||||
.value=${this._passwordConfirm}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
required
|
||||
type="password"
|
||||
.invalid=${this._password !== "" &&
|
||||
this._passwordConfirm !== "" &&
|
||||
this._passwordConfirm !== this._password}
|
||||
.errorMessage="${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password_not_match"
|
||||
)}"
|
||||
></paper-input>
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize("ui.panel.config.users.editor.admin")}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
@@ -173,10 +147,7 @@ export class DialogAddUser extends LitElement {
|
||||
: html`
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
.disabled=${!this._name ||
|
||||
!this._username ||
|
||||
!this._password ||
|
||||
this._password !== this._passwordConfirm}
|
||||
.disabled=${!this._name || !this._username || !this._password}
|
||||
@click=${this._createUser}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.users.add_user.create")}
|
||||
@@ -202,10 +173,19 @@ export class DialogAddUser extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleValueChanged(ev: PolymerChangedEvent<string>): void {
|
||||
private _nameChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
const name = (ev.target as any).name;
|
||||
this[`_${name}`] = ev.detail.value;
|
||||
this._name = ev.detail.value;
|
||||
}
|
||||
|
||||
private _usernameChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._username = ev.detail.value;
|
||||
}
|
||||
|
||||
private _passwordChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._password = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _adminChanged(ev): Promise<void> {
|
||||
|
@@ -6,28 +6,23 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-switch";
|
||||
import { adminChangePassword } from "../../../data/auth";
|
||||
import "../../../components/ha-formfield";
|
||||
import {
|
||||
SYSTEM_GROUP_ID_ADMIN,
|
||||
SYSTEM_GROUP_ID_USER,
|
||||
} from "../../../data/user";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { UserDetailDialogParams } from "./show-dialog-user-detail";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
|
||||
@customElement("dialog-user-detail")
|
||||
class DialogUserDetail extends LitElement {
|
||||
@@ -146,15 +141,7 @@ class DialogUserDetail extends LitElement {
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
${!user.system_generated && this.hass.user?.is_owner
|
||||
? html`<mwc-button @click=${this._changePassword}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
<div slot="primaryAction">
|
||||
<mwc-button
|
||||
@click=${this._updateEntry}
|
||||
@@ -215,52 +202,6 @@ class DialogUserDetail extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _changePassword() {
|
||||
const credential = this._params?.entry.credentials.find(
|
||||
(cred) => cred.type === "homeassistant"
|
||||
);
|
||||
if (!credential) {
|
||||
showAlertDialog(this, {
|
||||
title: "No Home Assistant credentials found.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const newPassword = await showPromptDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.users.editor.change_password"),
|
||||
inputType: "password",
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.users.editor.new_password"
|
||||
),
|
||||
});
|
||||
if (!newPassword) {
|
||||
return;
|
||||
}
|
||||
const confirmPassword = await showPromptDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.users.editor.change_password"),
|
||||
inputType: "password",
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password_confirm"
|
||||
),
|
||||
});
|
||||
if (!confirmPassword) {
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password_not_match"
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
await adminChangePassword(this.hass, this._params!.entry.id, newPassword);
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password_changed"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _close(): void {
|
||||
this._params = undefined;
|
||||
}
|
||||
|
204
src/panels/developer-tools/template/developer-tools-template.js
Normal file
204
src/panels/developer-tools/template/developer-tools-template.js
Normal file
@@ -0,0 +1,204 @@
|
||||
import "../../../components/ha-circular-progress";
|
||||
import { timeOut } from "@polymer/polymer/lib/utils/async";
|
||||
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../../components/ha-code-editor";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import "../../../styles/polymer-ha-style";
|
||||
|
||||
class HaPanelDevTemplate extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style iron-flex iron-positioning"></style>
|
||||
<style>
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.edit-pane {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.edit-pane a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.horizontal .edit-pane {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.render-pane {
|
||||
position: relative;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.render-spinner {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.rendered {
|
||||
@apply --paper-font-code1;
|
||||
clear: both;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.rendered.error {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class$="[[computeFormClasses(narrow)]]">
|
||||
<div class="edit-pane">
|
||||
<p>
|
||||
[[localize('ui.panel.developer-tools.tabs.templates.description')]]
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="http://jinja.pocoo.org/docs/dev/templates/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>[[localize('ui.panel.developer-tools.tabs.templates.jinja_documentation')]]</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://home-assistant.io/docs/configuration/templating/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>[[localize('ui.panel.developer-tools.tabs.templates.template_extensions')]]</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<p>[[localize('ui.panel.developer-tools.tabs.templates.editor')]]</p>
|
||||
<ha-code-editor
|
||||
mode="jinja2"
|
||||
value="[[template]]"
|
||||
error="[[error]]"
|
||||
autofocus
|
||||
on-value-changed="templateChanged"
|
||||
></ha-code-editor>
|
||||
</div>
|
||||
|
||||
<div class="render-pane">
|
||||
<ha-circular-progress
|
||||
class="render-spinner"
|
||||
active="[[rendering]]"
|
||||
size="small"
|
||||
></ha-circular-progress>
|
||||
<pre class$="[[computeRenderedClasses(error)]]">[[processed]]</pre>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
error: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
rendering: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
template: {
|
||||
type: String,
|
||||
/* eslint-disable max-len */
|
||||
value: `Imitate available variables:
|
||||
{% set my_test_json = {
|
||||
"temperature": 25,
|
||||
"unit": "°C"
|
||||
} %}
|
||||
|
||||
The temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}.
|
||||
|
||||
{% if is_state("device_tracker.paulus", "home") and
|
||||
is_state("device_tracker.anne_therese", "home") -%}
|
||||
You are both home, you silly
|
||||
{%- else -%}
|
||||
Anne Therese is at {{ states("device_tracker.anne_therese") }}
|
||||
Paulus is at {{ states("device_tracker.paulus") }}
|
||||
{%- endif %}
|
||||
|
||||
For loop example:
|
||||
{% for state in states.sensor -%}
|
||||
{%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}
|
||||
{{ state.name | lower }} is {{state.state_with_unit}}
|
||||
{%- endfor %}.`,
|
||||
/* eslint-enable max-len */
|
||||
},
|
||||
|
||||
processed: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.renderTemplate();
|
||||
}
|
||||
|
||||
computeFormClasses(narrow) {
|
||||
return narrow ? "content" : "content layout horizontal";
|
||||
}
|
||||
|
||||
computeRenderedClasses(error) {
|
||||
return error ? "error rendered" : "rendered";
|
||||
}
|
||||
|
||||
templateChanged(ev) {
|
||||
this.template = ev.detail.value;
|
||||
if (this.error) {
|
||||
this.error = false;
|
||||
}
|
||||
this._debouncer = Debouncer.debounce(
|
||||
this._debouncer,
|
||||
timeOut.after(500),
|
||||
() => {
|
||||
this.renderTemplate();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
renderTemplate() {
|
||||
this.rendering = true;
|
||||
|
||||
this.hass.callApi("POST", "template", { template: this.template }).then(
|
||||
function (processed) {
|
||||
this.processed = processed;
|
||||
this.rendering = false;
|
||||
}.bind(this),
|
||||
function (error) {
|
||||
this.processed =
|
||||
(error && error.body && error.body.message) ||
|
||||
this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.unknown_error_template"
|
||||
);
|
||||
this.error = true;
|
||||
this.rendering = false;
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("developer-tools-template", HaPanelDevTemplate);
|
@@ -1,280 +0,0 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-code-editor";
|
||||
import { subscribeRenderTemplate } from "../../../data/ws-templates";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
const DEMO_TEMPLATE = `{## Imitate available variables: ##}
|
||||
{% set my_test_json = {
|
||||
"temperature": 25,
|
||||
"unit": "°C"
|
||||
} %}
|
||||
|
||||
The temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}.
|
||||
|
||||
{% if is_state("sun.sun", "above_horizon") -%}
|
||||
The sun rose {{ relative_time(states.sun.sun.last_changed) }} ago.
|
||||
{%- else -%}
|
||||
The sun will rise at {{ as_timestamp(strptime(state_attr("sun.sun", "next_rising"), "")) | timestamp_local }}.
|
||||
{%- endif %}
|
||||
|
||||
For loop example getting 3 entity values:
|
||||
|
||||
{% for states in states | slice(3) -%}
|
||||
{% set state = states | first %}
|
||||
{%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}
|
||||
{{ state.name | lower }} is {{state.state_with_unit}}
|
||||
{%- endfor %}.`;
|
||||
|
||||
@customElement("developer-tools-template")
|
||||
class HaPanelDevTemplate extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@internalProperty() private _error = false;
|
||||
|
||||
@internalProperty() private _rendering = false;
|
||||
|
||||
@internalProperty() private _processed = "";
|
||||
|
||||
@internalProperty() private _unsubRenderTemplate?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _template = "";
|
||||
|
||||
private _inited = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this._template && !this._unsubRenderTemplate) {
|
||||
this._subscribeTemplate();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
this._unsubscribeTemplate();
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
if (localStorage && localStorage["panel-dev-template-template"]) {
|
||||
this._template = localStorage["panel-dev-template-template"];
|
||||
} else {
|
||||
this._template = DEMO_TEMPLATE;
|
||||
}
|
||||
this._subscribeTemplate();
|
||||
this._inited = true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
class="content ${classMap({
|
||||
layout: !this.narrow,
|
||||
horizontal: !this.narrow,
|
||||
})}"
|
||||
>
|
||||
<div class="edit-pane">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.description"
|
||||
)}
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="http://jinja.pocoo.org/docs/dev/templates/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.jinja_documentation"
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://home-assistant.io/docs/configuration/templating/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.template_extensions"
|
||||
)}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.editor"
|
||||
)}
|
||||
</p>
|
||||
<ha-code-editor
|
||||
mode="jinja2"
|
||||
.value=${this._template}
|
||||
.error=${this._error}
|
||||
autofocus
|
||||
@value-changed=${this._templateChanged}
|
||||
></ha-code-editor>
|
||||
<mwc-button @click=${this._restoreDemo}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.reset"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
|
||||
<div class="render-pane">
|
||||
<ha-circular-progress
|
||||
class="render-spinner"
|
||||
.active=${this._rendering}
|
||||
size="small"
|
||||
></ha-circular-progress>
|
||||
<pre class="rendered ${classMap({ error: this._error })}">
|
||||
${this._processed}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.edit-pane {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.edit-pane a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.horizontal .edit-pane {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.render-pane {
|
||||
position: relative;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.render-spinner {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.rendered {
|
||||
@apply --paper-font-code1;
|
||||
clear: both;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.rendered.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private _debounceRender = debounce(
|
||||
() => {
|
||||
this._subscribeTemplate();
|
||||
this._storeTemplate();
|
||||
},
|
||||
500,
|
||||
false
|
||||
);
|
||||
|
||||
private _templateChanged(ev) {
|
||||
this._template = ev.detail.value;
|
||||
if (this._error) {
|
||||
this._error = false;
|
||||
}
|
||||
this._debounceRender();
|
||||
}
|
||||
|
||||
private async _subscribeTemplate() {
|
||||
this._rendering = true;
|
||||
await this._unsubscribeTemplate();
|
||||
try {
|
||||
this._unsubRenderTemplate = subscribeRenderTemplate(
|
||||
this.hass.connection,
|
||||
(result) => {
|
||||
this._processed = result;
|
||||
},
|
||||
{
|
||||
template: this._template,
|
||||
}
|
||||
);
|
||||
await this._unsubRenderTemplate;
|
||||
} catch (err) {
|
||||
this._error = true;
|
||||
if (err.message) {
|
||||
this._processed = err.message;
|
||||
}
|
||||
this._unsubRenderTemplate = undefined;
|
||||
} finally {
|
||||
this._rendering = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _unsubscribeTemplate(): Promise<void> {
|
||||
if (!this._unsubRenderTemplate) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const unsub = await this._unsubRenderTemplate;
|
||||
unsub();
|
||||
this._unsubRenderTemplate = undefined;
|
||||
} catch (e) {
|
||||
if (e.code === "not_found") {
|
||||
// If we get here, the connection was probably already closed. Ignore.
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _storeTemplate() {
|
||||
if (!this._inited) {
|
||||
return;
|
||||
}
|
||||
localStorage["panel-dev-template-template"] = this._template;
|
||||
}
|
||||
|
||||
private _restoreDemo() {
|
||||
this._template = DEMO_TEMPLATE;
|
||||
this._subscribeTemplate();
|
||||
delete localStorage["panel-dev-template-template"];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"developer-tools-template": HaPanelDevTemplate;
|
||||
}
|
||||
}
|
@@ -118,26 +118,6 @@ class HaLogbook extends LitElement {
|
||||
? ` (${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>
|
||||
|
@@ -1,213 +0,0 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
internalProperty,
|
||||
} from "lit-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../calendar/ha-full-calendar";
|
||||
|
||||
import type {
|
||||
HomeAssistant,
|
||||
CalendarEvent,
|
||||
Calendar,
|
||||
CalendarViewChanged,
|
||||
FullCalendarView,
|
||||
} from "../../../types";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import type { CalendarCardConfig } from "./types";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import "../components/hui-warning";
|
||||
import { fetchCalendarEvents } from "../../../data/calendar";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { HA_COLOR_PALETTE } from "../../../common/const";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { installResizeObserver } from "../common/install-resize-observer";
|
||||
|
||||
@customElement("hui-calendar-card")
|
||||
export class HuiCalendarCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import(
|
||||
/* webpackChunkName: "hui-calendar-card-editor" */ "../editor/config-elements/hui-calendar-card-editor"
|
||||
);
|
||||
return document.createElement("hui-calendar-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
entities: string[],
|
||||
entitiesFill: string[]
|
||||
) {
|
||||
const includeDomains = ["calendar"];
|
||||
const maxEntities = 2;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
includeDomains
|
||||
);
|
||||
|
||||
return {
|
||||
entities: foundEntities,
|
||||
};
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public _events: CalendarEvent[] = [];
|
||||
|
||||
@internalProperty() private _config?: CalendarCardConfig;
|
||||
|
||||
@internalProperty() private _calendars: Calendar[] = [];
|
||||
|
||||
@internalProperty() private _narrow = false;
|
||||
|
||||
@internalProperty() private _veryNarrow = false;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
public setConfig(config: CalendarCardConfig): void {
|
||||
if (!config.entities) {
|
||||
throw new Error("Entities must be defined");
|
||||
}
|
||||
|
||||
if (config.entities && !Array.isArray(config.entities)) {
|
||||
throw new Error("Entities need to be an array");
|
||||
}
|
||||
|
||||
this._calendars = config!.entities.map((entity, idx) => {
|
||||
return {
|
||||
entity_id: entity,
|
||||
backgroundColor: `#${HA_COLOR_PALETTE[idx % HA_COLOR_PALETTE.length]}`,
|
||||
};
|
||||
});
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 4;
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.updateComplete.then(() => this._attachObserver());
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config || !this.hass || !this._calendars.length) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const views: FullCalendarView[] = this._veryNarrow
|
||||
? ["listWeek"]
|
||||
: ["listWeek", "dayGridMonth", "dayGridDay"];
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="header">${this._config.title}</div>
|
||||
<ha-full-calendar
|
||||
.narrow=${this._narrow}
|
||||
.events=${this._events}
|
||||
.hass=${this.hass}
|
||||
.views=${views}
|
||||
@view-changed=${this._handleViewChanged}
|
||||
></ha-full-calendar>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const oldConfig = changedProps.get("_config") as
|
||||
| CalendarCardConfig
|
||||
| undefined;
|
||||
|
||||
if (
|
||||
!oldHass ||
|
||||
!oldConfig ||
|
||||
(changedProps.has("hass") && oldHass.themes !== this.hass.themes) ||
|
||||
(changedProps.has("_config") && oldConfig.theme !== this._config.theme)
|
||||
) {
|
||||
applyThemesOnElement(this, this.hass.themes, this._config!.theme);
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleViewChanged(
|
||||
ev: HASSDomEvent<CalendarViewChanged>
|
||||
): Promise<void> {
|
||||
this._events = await fetchCalendarEvents(
|
||||
this.hass!,
|
||||
ev.detail.start,
|
||||
ev.detail.end,
|
||||
this._calendars
|
||||
);
|
||||
}
|
||||
|
||||
private _measureCard() {
|
||||
const card = this.shadowRoot!.querySelector("ha-card");
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
this._narrow = card.offsetWidth < 870;
|
||||
this._veryNarrow = card.offsetWidth < 350;
|
||||
}
|
||||
|
||||
private async _attachObserver(): Promise<void> {
|
||||
if (!this._resizeObserver) {
|
||||
await installResizeObserver();
|
||||
this._resizeObserver = new ResizeObserver(
|
||||
debounce(() => this._measureCard(), 250, false)
|
||||
);
|
||||
}
|
||||
const card = this.shadowRoot!.querySelector("ha-card");
|
||||
// If we show an error or warning there is no ha-card
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
this._resizeObserver.observe(card);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-card {
|
||||
position: relative;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: var(--ha-card-header-color, --primary-text-color);
|
||||
font-size: var(--ha-card-header-font-size, 24px);
|
||||
line-height: 1.2;
|
||||
padding-top: 16px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-calendar-card": HuiCalendarCard;
|
||||
}
|
||||
}
|
@@ -19,8 +19,6 @@ import { processConfigEntities } from "../common/process-config-entities";
|
||||
import { EntityConfig } from "../entity-rows/types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { HistoryGraphCardConfig } from "./types";
|
||||
import { HistoryResult } from "../../../data/history";
|
||||
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
|
||||
|
||||
@customElement("hui-history-graph-card")
|
||||
export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
@@ -51,7 +49,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@internalProperty() private _stateHistory?: HistoryResult;
|
||||
@internalProperty() private _stateHistory?: any;
|
||||
|
||||
@internalProperty() private _config?: HistoryGraphCardConfig;
|
||||
|
||||
@@ -61,9 +59,9 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
|
||||
private _cacheConfig?: CacheConfig;
|
||||
|
||||
private _fetching = false;
|
||||
private _interval?: number;
|
||||
|
||||
private _date?: Date;
|
||||
private _fetching = false;
|
||||
|
||||
public getCardSize(): number {
|
||||
return 4;
|
||||
@@ -99,8 +97,9 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
};
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return hasConfigOrEntitiesChanged(this, changedProps);
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._clearInterval();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
@@ -109,19 +108,21 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!changedProps.has("_config") && !changedProps.has("hass")) {
|
||||
if (!changedProps.has("_config")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldConfig = changedProps.get("_config") as HistoryGraphCardConfig;
|
||||
|
||||
if (changedProps.has("_config") && oldConfig !== this._config) {
|
||||
this._getStateHistory();
|
||||
} else if (
|
||||
this._cacheConfig.refresh &&
|
||||
Date.now() - this._date!.getTime() >= this._cacheConfig.refresh * 100
|
||||
) {
|
||||
if (oldConfig !== this._config) {
|
||||
this._getStateHistory();
|
||||
this._clearInterval();
|
||||
|
||||
if (!this._interval && this._cacheConfig.refresh) {
|
||||
this._interval = window.setInterval(() => {
|
||||
this._getStateHistory();
|
||||
}, this._cacheConfig.refresh * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,23 +155,27 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
if (this._fetching) {
|
||||
return;
|
||||
}
|
||||
this._date = new Date();
|
||||
this._fetching = true;
|
||||
try {
|
||||
this._stateHistory = {
|
||||
...(await getRecentWithCache(
|
||||
this.hass!,
|
||||
this._cacheConfig!.cacheKey,
|
||||
this._cacheConfig!,
|
||||
this.hass!.localize,
|
||||
this.hass!.language
|
||||
)),
|
||||
};
|
||||
this._stateHistory = await getRecentWithCache(
|
||||
this.hass!,
|
||||
this._cacheConfig!.cacheKey,
|
||||
this._cacheConfig!,
|
||||
this.hass!.localize,
|
||||
this.hass!.language
|
||||
);
|
||||
} finally {
|
||||
this._fetching = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _clearInterval(): void {
|
||||
if (this._interval) {
|
||||
window.clearInterval(this._interval);
|
||||
this._interval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.content {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import "../../../components/ha-icon-button";
|
||||
import "@polymer/paper-progress/paper-progress";
|
||||
import type { PaperProgressElement } from "@polymer/paper-progress/paper-progress";
|
||||
import {
|
||||
@@ -5,9 +6,9 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
@@ -24,17 +25,12 @@ import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
|
||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
||||
import {
|
||||
computeMediaDescription,
|
||||
CONTRAST_RATIO,
|
||||
ControlButton,
|
||||
getCurrentProgress,
|
||||
MediaPickedEvent,
|
||||
SUPPORTS_PLAY,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_PREVIOUS_TRACK,
|
||||
@@ -47,11 +43,11 @@ import type { HomeAssistant, MediaEntity } from "../../../types";
|
||||
import { contrast } from "../common/color/contrast";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { installResizeObserver } from "../common/install-resize-observer";
|
||||
import "../components/hui-marquee";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { MediaControlCardConfig } from "./types";
|
||||
import { installResizeObserver } from "../common/install-resize-observer";
|
||||
|
||||
function getContrastRatio(
|
||||
rgb1: [number, number, number],
|
||||
@@ -161,6 +157,11 @@ const customGenerator = (colors: Swatch[]) => {
|
||||
return [foregroundColor, backgroundColor.hex];
|
||||
};
|
||||
|
||||
interface ControlButton {
|
||||
icon: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
@customElement("hui-media-control-card")
|
||||
export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
@@ -188,7 +189,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
return { type: "media-control", entity: foundEntities[0] || "" };
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@internalProperty() private _config?: MediaControlCardConfig;
|
||||
|
||||
@@ -392,27 +393,12 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
${controls!.map(
|
||||
(control) => html`
|
||||
<ha-icon-button
|
||||
.title=${this.hass.localize(
|
||||
`ui.card.media_player.${control.action}`
|
||||
)}
|
||||
.icon=${control.icon}
|
||||
action=${control.action}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
`
|
||||
)}
|
||||
${supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="browse-media"
|
||||
icon="hass:folder-multiple"
|
||||
.title=${this.hass.localize(
|
||||
"ui.card.media_player.browse_media"
|
||||
)}
|
||||
@click=${this._handleBrowseMedia}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
@@ -661,31 +647,14 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleBrowseMedia(): void {
|
||||
showMediaBrowserDialog(this, {
|
||||
action: "play",
|
||||
entityId: this._config!.entity,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
|
||||
this._playMedia(
|
||||
pickedMedia.media_content_id,
|
||||
pickedMedia.media_content_type
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _playMedia(media_content_id: string, media_content_type: string) {
|
||||
this.hass!.callService("media_player", "play_media", {
|
||||
entity_id: this._config!.entity,
|
||||
media_content_id,
|
||||
media_content_type,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleClick(e: MouseEvent): void {
|
||||
const action = (e.currentTarget! as HTMLElement).getAttribute("action")!;
|
||||
this.hass!.callService("media_player", action, {
|
||||
entity_id: this._config!.entity,
|
||||
});
|
||||
this.hass!.callService(
|
||||
"media_player",
|
||||
(e.currentTarget! as HTMLElement).getAttribute("action")!,
|
||||
{
|
||||
entity_id: this._config!.entity,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _updateProgressBar(): void {
|
||||
@@ -866,12 +835,6 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
--mdc-icon-size: 40px;
|
||||
}
|
||||
|
||||
ha-icon-button.browse-media {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
|
||||
.top-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -941,10 +904,6 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
--mdc-icon-size: 36px;
|
||||
}
|
||||
|
||||
.narrow ha-icon-button.browse-media {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
|
||||
.no-progress.player:not(.no-controls) {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
@@ -436,7 +436,6 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
.forecast-image-icon > * {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
--mdc-icon-size: 40px;
|
||||
}
|
||||
|
||||
.forecast-icon {
|
||||
@@ -470,7 +469,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
:host([narrow]) .icon-image .weather-icon {
|
||||
:host([narrow]) .weather-icon {
|
||||
--mdc-icon-size: 52px;
|
||||
}
|
||||
|
||||
|
@@ -12,12 +12,6 @@ export interface AlarmPanelCardConfig extends LovelaceCardConfig {
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
export interface CalendarCardConfig extends LovelaceCardConfig {
|
||||
entities: string[];
|
||||
title?: string;
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
export interface ConditionalCardConfig extends LovelaceCardConfig {
|
||||
card: LovelaceCardConfig;
|
||||
conditions: Condition[];
|
||||
|
@@ -1,8 +1,11 @@
|
||||
import { PropertyValues } from "lit-element";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { processConfigEntities } from "./process-config-entities";
|
||||
|
||||
function hasConfigChanged(element: any, changedProps: PropertyValues): boolean {
|
||||
// Check if config or Entity changed
|
||||
export function hasConfigOrEntityChanged(
|
||||
element: any,
|
||||
changedProps: PropertyValues
|
||||
): boolean {
|
||||
if (changedProps.has("_config")) {
|
||||
return true;
|
||||
}
|
||||
@@ -20,41 +23,9 @@ function hasConfigChanged(element: any, changedProps: PropertyValues): boolean {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if config or Entity changed
|
||||
export function hasConfigOrEntityChanged(
|
||||
element: any,
|
||||
changedProps: PropertyValues
|
||||
): boolean {
|
||||
if (hasConfigChanged(element, changedProps)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant;
|
||||
|
||||
return (
|
||||
oldHass.states[element._config!.entity] !==
|
||||
element.hass!.states[element._config!.entity]
|
||||
);
|
||||
}
|
||||
|
||||
// Check if config or Entities changed
|
||||
export function hasConfigOrEntitiesChanged(
|
||||
element: any,
|
||||
changedProps: PropertyValues
|
||||
): boolean {
|
||||
if (hasConfigChanged(element, changedProps)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant;
|
||||
|
||||
const entities = processConfigEntities(element._config!.entities);
|
||||
|
||||
return entities.some(
|
||||
(entity) =>
|
||||
oldHass.states[entity.entity] !== element.hass!.states[entity.entity]
|
||||
);
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import "../cards/hui-button-card";
|
||||
import "../cards/hui-calendar-card";
|
||||
import "../cards/hui-entities-card";
|
||||
import "../cards/hui-entity-button-card";
|
||||
import "../cards/hui-entity-card";
|
||||
@@ -53,7 +52,6 @@ const LAZY_LOAD_TYPES = {
|
||||
map: () => import("../cards/hui-map-card"),
|
||||
markdown: () => import("../cards/hui-markdown-card"),
|
||||
picture: () => import("../cards/hui-picture-card"),
|
||||
calendar: () => import("../cards/hui-calendar-card"),
|
||||
};
|
||||
|
||||
// This will not return an error card but will throw the error
|
||||
|
@@ -1,133 +0,0 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
internalProperty,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { CalendarCardConfig } from "../../cards/types";
|
||||
import "../../components/hui-entity-editor";
|
||||
import "../../../../components/entity/ha-entities-picker";
|
||||
import "../../components/hui-theme-select-editor";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import type { EditorTarget, EntitiesEditorEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import {
|
||||
string,
|
||||
optional,
|
||||
object,
|
||||
boolean,
|
||||
array,
|
||||
union,
|
||||
assert,
|
||||
} from "superstruct";
|
||||
|
||||
const cardConfigStruct = object({
|
||||
type: string(),
|
||||
title: optional(union([string(), boolean()])),
|
||||
theme: optional(string()),
|
||||
entities: array(string()),
|
||||
});
|
||||
|
||||
@customElement("hui-calendar-card-editor")
|
||||
export class HuiCalendarCardEditor extends LitElement
|
||||
implements LovelaceCardEditor {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) private _config?: CalendarCardConfig;
|
||||
|
||||
@internalProperty() private _configEntities?: string[];
|
||||
|
||||
public setConfig(config: CalendarCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
this._configEntities = config.entities;
|
||||
}
|
||||
|
||||
get _title(): string {
|
||||
return this._config!.title || "";
|
||||
}
|
||||
|
||||
get _theme(): string {
|
||||
return this._config!.theme || "";
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${configElementStyle}
|
||||
<div class="card-config">
|
||||
<div class="side-by-side">
|
||||
<paper-input
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.title"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.value=${this._title}
|
||||
.configValue=${"title"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></paper-input>
|
||||
<hui-theme-select-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._theme}
|
||||
.configValue=${"theme"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-theme-select-editor>
|
||||
</div>
|
||||
</div>
|
||||
<h3>
|
||||
${"Calendar Entities" +
|
||||
" (" +
|
||||
this.hass!.localize("ui.panel.lovelace.editor.card.config.required") +
|
||||
")"}
|
||||
</h3>
|
||||
<ha-entities-picker
|
||||
.hass=${this.hass!}
|
||||
.value=${this._configEntities}
|
||||
.includeDomains=${["calendar"]}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-entities-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: EntitiesEditorEvent | CustomEvent): void {
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = ev.target! as EditorTarget;
|
||||
|
||||
if (this[`_${target.configValue}`] === target.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.detail && ev.detail.value && Array.isArray(ev.detail.value)) {
|
||||
this._config = { ...this._config, entities: ev.detail.value };
|
||||
} else if (target.configValue) {
|
||||
if (target.value === "") {
|
||||
delete this._config[target.configValue!];
|
||||
} else {
|
||||
this._config = {
|
||||
...this._config,
|
||||
[target.configValue]: target.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-calendar-card-editor": HuiCalendarCardEditor;
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@@ -8,28 +9,25 @@ import {
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/dialog/ha-paper-dialog";
|
||||
import type { HaPaperDialog } from "../../../components/dialog/ha-paper-dialog";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import type { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { SaveDialogParams } from "./show-save-config-dialog";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-circular-progress";
|
||||
|
||||
const EMPTY_CONFIG = { views: [] };
|
||||
|
||||
const coreDocumentationURLBase = "https://www.home-assistant.io/lovelace/";
|
||||
|
||||
@customElement("hui-dialog-save-config")
|
||||
export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||
export class HuiSaveConfig extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@internalProperty() private _params?: SaveDialogParams;
|
||||
@@ -38,20 +36,18 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||
|
||||
@internalProperty() private _saving: boolean;
|
||||
|
||||
@query("ha-paper-dialog") private _dialog?: HaPaperDialog;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
this._saving = false;
|
||||
}
|
||||
|
||||
public showDialog(params: SaveDialogParams): void {
|
||||
public async showDialog(params: SaveDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._emptyConfig = false;
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
return true;
|
||||
await this.updateComplete;
|
||||
this._dialog!.open();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -59,27 +55,15 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
@closed=${this._close}
|
||||
.heading=${html`${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.header"
|
||||
)}<a
|
||||
class="header_button"
|
||||
href=${coreDocumentationURLBase}
|
||||
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-paper-dialog
|
||||
with-backdrop
|
||||
opened
|
||||
@opened-changed=${this._openedChanged}
|
||||
>
|
||||
<div>
|
||||
<h2>
|
||||
${this.hass!.localize("ui.panel.lovelace.editor.save_config.header")}
|
||||
</h2>
|
||||
<paper-dialog-scrollable>
|
||||
<p>
|
||||
${this.hass!.localize("ui.panel.lovelace.editor.save_config.para")}
|
||||
</p>
|
||||
@@ -121,46 +105,55 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||
</p>
|
||||
<ha-yaml-editor
|
||||
.defaultValue=${this._params!.lovelace.config}
|
||||
@editor-refreshed=${this._editorRefreshed}
|
||||
></ha-yaml-editor>
|
||||
`}
|
||||
</paper-dialog-scrollable>
|
||||
<div class="paper-dialog-buttons">
|
||||
${this._params.mode === "storage"
|
||||
? html`
|
||||
<mwc-button @click="${this._closeDialog}"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.cancel"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
?disabled="${this._saving}"
|
||||
@click="${this._saveConfig}"
|
||||
>
|
||||
<ha-circular-progress
|
||||
?active="${this._saving}"
|
||||
alt="Saving"
|
||||
></ha-circular-progress>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.save"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button @click=${this._closeDialog}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.close"
|
||||
)}
|
||||
</mwc-button>
|
||||
`}
|
||||
</div>
|
||||
${this._params.mode === "storage"
|
||||
? html`
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.cancel"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
?disabled=${this._saving}
|
||||
@click=${this._saveConfig}
|
||||
>
|
||||
<ha-circular-progress
|
||||
?active=${this._saving}
|
||||
alt="Saving"
|
||||
></ha-circular-progress>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.save"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.close"
|
||||
)}
|
||||
</mwc-button>
|
||||
`}
|
||||
</ha-dialog>
|
||||
</ha-paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _close(ev?: Event) {
|
||||
if (ev) {
|
||||
ev.stopPropagation();
|
||||
private _closeDialog(): void {
|
||||
this._dialog!.close();
|
||||
}
|
||||
|
||||
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
|
||||
if (!ev.detail.value) {
|
||||
this._params = undefined;
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _editorRefreshed() {
|
||||
fireEvent(this._dialog! as HTMLElement, "iron-resize");
|
||||
}
|
||||
|
||||
private _emptyConfigChanged(ev) {
|
||||
@@ -179,7 +172,7 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||
);
|
||||
lovelace.setEditMode(true);
|
||||
this._saving = false;
|
||||
this.closeDialog();
|
||||
this._closeDialog();
|
||||
} catch (err) {
|
||||
alert(`Saving failed: ${err.message}`);
|
||||
this._saving = false;
|
||||
|
@@ -9,10 +9,6 @@ export const coreCards: Card[] = [
|
||||
type: "button",
|
||||
showElement: true,
|
||||
},
|
||||
{
|
||||
type: "calendar",
|
||||
showElement: true,
|
||||
},
|
||||
{
|
||||
type: "entities",
|
||||
showElement: true,
|
||||
|
@@ -17,7 +17,7 @@ export interface EntityFilterEntityConfig extends EntityConfig {
|
||||
}
|
||||
export interface DividerConfig {
|
||||
type: "divider";
|
||||
style: { [key: string]: string };
|
||||
style: string;
|
||||
}
|
||||
export interface SectionConfig {
|
||||
type: "section";
|
||||
|
@@ -40,7 +40,7 @@ export const buttonsHeaderFooterConfigStruct = object({
|
||||
export const graphHeaderFooterConfigStruct = object({
|
||||
type: string(),
|
||||
entity: string(),
|
||||
detail: optional(number()),
|
||||
detail: optional(string()),
|
||||
hours_to_show: optional(number()),
|
||||
});
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user