Compare commits

..

1 Commits

Author SHA1 Message Date
Bram Kragten 6b2754c4cc Prefer native hls support 2020-06-10 10:59:23 +02:00
197 changed files with 5283 additions and 7300 deletions
+5 -1
View File
@@ -66,7 +66,11 @@
"import/prefer-default-export": 0,
"import/no-unresolved": 0,
"import/no-cycle": 0,
"import/extensions": 0,
"import/extensions": [
2,
"ignorePackages",
{ "ts": "never", "js": "never" }
],
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"object-curly-newline": 0,
"default-case": 0,
+2 -2
View File
@@ -1,3 +1,3 @@
import "~app/resources/ha-style";
import "~app/resources/roboto";
import "../../../src/resources/ha-style";
import "../../../src/resources/roboto";
import "./layout/hc-connect";
+3 -3
View File
@@ -1,7 +1,7 @@
/* eslint-disable no-undef */
import { CAST_NS } from "~app/cast/const";
import { HassMessage } from "~app/cast/receiver_messages";
import "~app/resources/custom-card-support";
import { CAST_NS } from "../../../src/cast/const";
import { HassMessage } from "../../../src/cast/receiver_messages";
import "../../../src/resources/custom-card-support";
import { castContext } from "./cast_context";
import { HcMain } from "./layout/hc-main";
import { ReceivedMessage } from "./types";
-2
View File
@@ -192,8 +192,6 @@ export class HcMain extends HassElement {
this._handleNewLovelaceConfig(lovelaceConfig)
);
} catch (err) {
// eslint-disable-next-line
console.log("Error fetching Lovelace configuration", err, msg);
// Generate a Lovelace config.
this._unsubLovelace = () => undefined;
await this._generateLovelaceConfig();
+2 -2
View File
@@ -2,8 +2,8 @@ import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "~app/components/ha-switch";
import "~app/components/ha-formfield";
import "../../../src/components/ha-switch";
import "../../../src/components/ha-formfield";
import "./demo-card";
class DemoCards extends PolymerElement {
+2 -2
View File
@@ -1,5 +1,5 @@
import "~app/resources/compatibility";
import "~app/resources/roboto";
import "../../src/resources/compatibility";
import "../../src/resources/roboto";
import "./hassio-main";
const styleEl = document.createElement("style");
+1 -9
View File
@@ -14,9 +14,7 @@ import {
createHassioSession,
fetchHassioHomeAssistantInfo,
fetchHassioSupervisorInfo,
fetchHassioInfo,
HassioHomeAssistantInfo,
HassioInfo,
HassioPanelInfo,
HassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
@@ -77,8 +75,6 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
@property() private _hostInfo: HassioHostInfo;
@property() private _hassioInfo?: HassioInfo;
@property() private _hassOsInfo?: HassioHassOSInfo;
@property() private _hassInfo: HassioHomeAssistantInfo;
@@ -151,7 +147,6 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
hass: this.hass,
narrow: this.narrow,
supervisorInfo: this._supervisorInfo,
hassioInfo: this._hassioInfo,
hostInfo: this._hostInfo,
hassInfo: this._hassInfo,
hassOsInfo: this._hassOsInfo,
@@ -161,7 +156,6 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
el.hass = this.hass;
el.narrow = this.narrow;
el.supervisorInfo = this._supervisorInfo;
el.hassioInfo = this._hassioInfo;
el.hostInfo = this._hostInfo;
el.hassInfo = this._hassInfo;
el.hassOsInfo = this._hassOsInfo;
@@ -175,14 +169,12 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
return;
}
const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([
const [supervisorInfo, hostInfo, hassInfo] = await Promise.all([
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
]);
this._supervisorInfo = supervisorInfo;
this._hassioInfo = hassioInfo;
this._hostInfo = hostInfo;
this._hassInfo = hassInfo;
-4
View File
@@ -3,7 +3,6 @@ import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
HassioInfo,
} from "../../src/data/hassio/supervisor";
import {
HassRouterPage,
@@ -27,8 +26,6 @@ class HassioPanelRouter extends HassRouterPage {
@property({ attribute: false }) public supervisorInfo: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property({ attribute: false }) public hostInfo: HassioHostInfo;
@property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo;
@@ -57,7 +54,6 @@ class HassioPanelRouter extends HassRouterPage {
el.route = this.route;
el.narrow = this.narrow;
el.supervisorInfo = this.supervisorInfo;
el.hassioInfo = this.hassioInfo;
el.hostInfo = this.hostInfo;
el.hassInfo = this.hassInfo;
el.hassOsInfo = this.hassOsInfo;
-4
View File
@@ -10,7 +10,6 @@ import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
HassioInfo,
} from "../../src/data/hassio/supervisor";
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../src/types";
@@ -49,8 +48,6 @@ class HassioPanel extends LitElement {
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property({ attribute: false }) public hostInfo!: HassioHostInfo;
@property({ attribute: false }) public hassInfo!: HassioHomeAssistantInfo;
@@ -64,7 +61,6 @@ class HassioPanel extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.supervisorInfo=${this.supervisorInfo}
.hassioInfo=${this.hassioInfo}
.hostInfo=${this.hostInfo}
.hassInfo=${this.hassInfo}
.hassOsInfo=${this.hassOsInfo}
-9
View File
@@ -19,7 +19,6 @@ import {
shutdownHost,
updateOS,
} from "../../../src/data/hassio/host";
import { HassioInfo } from "../../../src/data/hassio/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
@@ -36,8 +35,6 @@ class HassioHostInfo extends LitElement {
@property() public hostInfo!: HassioHostInfoType;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property() public hassOsInfo!: HassioHassOSInfo;
@property() private _errors?: string;
@@ -57,12 +54,6 @@ class HassioHostInfo extends LitElement {
<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>
+2 -8
View File
@@ -11,10 +11,7 @@ import {
HassioHassOSInfo,
HassioHostInfo,
} from "../../../src/data/hassio/host";
import {
HassioSupervisorInfo,
HassioInfo,
} from "../../../src/data/hassio/supervisor";
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
@@ -34,11 +31,9 @@ class HassioSystem extends LitElement {
@property() public supervisorInfo!: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property() public hostInfo!: HassioHostInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
@property() public hassOsInfo!: HassioHassOSInfo;
public render(): TemplateResult | void {
return html`
@@ -60,7 +55,6 @@ class HassioSystem extends LitElement {
></hassio-supervisor-info>
<hassio-host-info
.hass=${this.hass}
.hassioInfo=${this.hassioInfo}
.hostInfo=${this.hostInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-host-info>
+2 -6
View File
@@ -22,7 +22,6 @@
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
"dependencies": {
"~app": "file:./src",
"@formatjs/intl-pluralrules": "^1.5.8",
"@fullcalendar/core": "^5.0.0-beta.2",
"@fullcalendar/daygrid": "^5.0.0-beta.2",
@@ -75,7 +74,6 @@
"@thomasloven/round-slider": "0.5.0",
"@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@vue/web-component-wrapper": "^1.2.0",
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0",
"chartjs-chart-timeline": "^0.3.0",
@@ -89,7 +87,7 @@
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^0.12.4",
"home-assistant-js-websocket": "^5.3.0",
"home-assistant-js-websocket": "^5.2.1",
"idb-keyval": "^3.2.0",
"intl-messageformat": "^8.3.9",
"js-yaml": "^3.13.1",
@@ -108,8 +106,6 @@
"roboto-fontface": "^0.10.0",
"superstruct": "^0.6.1",
"unfetch": "^4.1.0",
"vue": "^2.6.11",
"vue2-daterange-picker": "^0.5.1",
"web-animations-js": "^2.3.2",
"workbox-core": "^5.1.3",
"workbox-precaching": "^5.1.3",
@@ -189,7 +185,7 @@
"sinon": "^7.3.1",
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^3.0.6",
"terser-webpack-plugin": "^1.2.3",
"ts-lit-plugin": "^1.1.10",
"ts-mocha": "^6.0.0",
"typescript": "^3.8.3",
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20200620.0",
version="20200603.1",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
+16 -8
View File
@@ -6,18 +6,19 @@ import {
property,
PropertyValues,
} from "lit-element";
import {
AuthProvider,
fetchAuthProviders,
AuthUrlSearchParams,
} from "../data/auth";
import { AuthProvider, fetchAuthProviders } from "../data/auth";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { registerServiceWorker } from "../util/register-service-worker";
import "./ha-auth-flow";
import { extractSearchParamsObject } from "../common/url/search-params";
import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider");
interface QueryParams {
client_id?: string;
redirect_uri?: string;
state?: string;
}
class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@property() public clientId?: string;
@@ -32,7 +33,14 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
constructor() {
super();
this.translationFragment = "page-authorize";
const query = extractSearchParamsObject() as AuthUrlSearchParams;
const query: QueryParams = {};
const values = location.search.substr(1).split("&");
for (const item of values) {
const value = item.split("=");
if (value.length > 1) {
query[decodeURIComponent(value[0])] = decodeURIComponent(value[1]);
}
}
if (query.client_id) {
this.clientId = query.client_id;
}
@@ -137,7 +145,7 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
response.status === 400 &&
authProviders.code === "onboarding_required"
) {
location.href = `/onboarding.html${location.search}`;
location.href = "/?";
return;
}
+1 -1
View File
@@ -6,7 +6,7 @@ export const isValidEntityId = (entityId: string) =>
export const createValidEntityId = (input: string) =>
input
.toLowerCase()
.replace(/\s|'|\./g, "_") // replace spaces, points and quotes with underscore
.replace(/\s|'/g, "_") // replace spaces and quotes with underscore
.replace(/\W/g, "") // remove not allowed chars
.replace(/_{2,}/g, "_") // replace multiple underscores with 1
.replace(/_$/, ""); // remove underscores at the end
-8
View File
@@ -1,8 +0,0 @@
export const extractSearchParamsObject = (): { [key: string]: string } => {
const query = {};
const searchParams = new URLSearchParams(location.search);
for (const [key, value] of searchParams.entries()) {
query[key] = value;
}
return query;
};
@@ -8,9 +8,6 @@ class HaProgressButton extends PolymerElement {
static get template() {
return html`
<style>
:host {
outline: none;
}
.container {
position: relative;
display: inline-block;
@@ -619,11 +619,6 @@ export class HaDataTable extends LitElement {
text-transform: inherit;
}
.mdc-data-table__cell a {
color: inherit;
text-decoration: none;
}
.mdc-data-table__cell--numeric {
text-align: right;
}
-228
View File
@@ -1,228 +0,0 @@
import Vue from "vue";
import wrap from "@vue/web-component-wrapper";
import DateRangePicker from "vue2-daterange-picker";
// @ts-ignore
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
import { fireEvent } from "../common/dom/fire_event";
import { Constructor } from "../types";
import { customElement } from "lit-element/lib/decorators";
const Component = Vue.extend({
props: {
twentyfourHours: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
ranges: {
type: Boolean,
default: true,
},
startDate: {
type: [String, Date],
default() {
return new Date();
},
},
endDate: {
type: [String, Date],
default() {
return new Date();
},
},
},
render(createElement) {
// @ts-ignore
return createElement(DateRangePicker, {
props: {
"time-picker": true,
"auto-apply": false,
opens: "right",
"show-dropdowns": false,
"time-picker24-hour": this.twentyfourHours,
disabled: this.disabled,
ranges: this.ranges ? {} : false,
},
model: {
value: {
startDate: this.startDate,
endDate: this.endDate,
},
callback: (value) => {
// @ts-ignore
fireEvent(this.$el as HTMLElement, "change", value);
},
expression: "dateRange",
},
scopedSlots: {
input() {
return createElement("slot", {
domProps: { name: "input" },
});
},
header() {
return createElement("slot", {
domProps: { name: "header" },
});
},
ranges() {
return createElement("slot", {
domProps: { name: "ranges" },
});
},
footer() {
return createElement("slot", {
domProps: { name: "footer" },
});
},
},
});
},
});
const WrappedElement: Constructor<HTMLElement> = wrap(Vue, Component);
@customElement("date-range-picker")
class DateRangePickerElement extends WrappedElement {
constructor() {
super();
const style = document.createElement("style");
style.innerHTML = `
${dateRangePickerStyles}
.calendars {
display: flex;
}
.daterangepicker {
left: 0px !important;
top: auto;
background-color: var(--card-background-color);
border: none;
border-radius: var(--ha-card-border-radius, 4px);
box-shadow: var(
--ha-card-box-shadow,
0px 2px 1px -1px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 1px 3px 0px rgba(0, 0, 0, 0.12)
);
color: var(--primary-text-color);
min-width: initial !important;
}
.daterangepicker:after {
border-bottom: 6px solid var(--card-background-color);
}
.daterangepicker .calendar-table {
background-color: var(--card-background-color);
border: none;
}
.daterangepicker .calendar-table td,
.daterangepicker .calendar-table th {
background-color: transparent;
color: var(--secondary-text-color);
border-radius: 0;
outline: none;
width: 32px;
height: 32px;
}
.daterangepicker td.off,
.daterangepicker td.off.end-date,
.daterangepicker td.off.in-range,
.daterangepicker td.off.start-date {
background-color: var(--secondary-background-color);
color: var(--disabled-text-color);
}
.daterangepicker td.in-range {
background-color: var(--light-primary-color);
color: var(--primary-text-color);
}
.daterangepicker td.active,
.daterangepicker td.active:hover {
background-color: var(--primary-color);
color: var(--text-primary-color);
}
.daterangepicker td.start-date.end-date {
border-radius: 50%;
}
.daterangepicker td.start-date {
border-radius: 50% 0 0 50%;
}
.daterangepicker td.end-date {
border-radius: 0 50% 50% 0;
}
.reportrange-text {
background: none !important;
padding: 0 !important;
border: none !important;
}
.daterangepicker .calendar-table .next span,
.daterangepicker .calendar-table .prev span {
border: solid var(--primary-text-color);
border-width: 0 2px 2px 0;
}
.daterangepicker .ranges li {
outline: none;
}
.daterangepicker .ranges li:hover {
background-color: var(--secondary-background-color);
}
.daterangepicker .ranges li.active {
background-color: var(--primary-color);
color: var(--text-primary-color);
}
.daterangepicker select.ampmselect,
.daterangepicker select.hourselect,
.daterangepicker select.minuteselect,
.daterangepicker select.secondselect {
background: transparent;
border: 1px solid var(--divider-color);
color: var(--primary-color);
}
.daterangepicker .drp-buttons .btn {
border: 1px solid var(--primary-color);
background-color: transparent;
color: var(--primary-color);
border-radius: 4px;
padding: 8px;
cursor: pointer;
}
.calendars-container {
flex-direction: column;
align-items: center;
}
.drp-calendar.col.right .calendar-table {
display: none;
}
.daterangepicker.show-ranges .drp-calendar.left {
border-left: 0px;
}
.daterangepicker .drp-calendar.left {
padding: 8px;
}
.daterangepicker.show-calendar .ranges {
margin-top: 0;
padding-top: 8px;
border-right: 1px solid var(--divider-color);
}
@media only screen and (max-width: 800px) {
.calendars {
flex-direction: column;
}
}
.calendar-table {
padding: 0 !important;
}
`;
const shadowRoot = this.shadowRoot!;
shadowRoot.appendChild(style);
// Stop click events from reaching the document, otherwise it will close the picker immediately.
shadowRoot.addEventListener("click", (ev) => ev.stopPropagation());
}
}
declare global {
interface HTMLElementTagNameMap {
"date-range-picker": DateRangePickerElement;
}
}
+19 -13
View File
@@ -125,19 +125,23 @@ class HaCameraStream extends LitElement {
}
private async _startHls(): Promise<void> {
// eslint-disable-next-line
const Hls = ((await import(
/* webpackChunkName: "hls.js" */ "hls.js"
)) as any).default as HLSModule;
let hlsSupported = Hls.isSupported();
const videoEl = this._videoEl;
if (!hlsSupported) {
hlsSupported =
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
const nativeHlsSupported =
!!videoEl.canPlayType &&
(videoEl.canPlayType("application/vnd.apple.mpegURL") !== "" ||
videoEl.canPlayType("audio/mpegurl") !== "");
let hlsSupported = false;
let Hls: HLSModule | undefined;
if (!nativeHlsSupported) {
Hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
.default as HLSModule;
hlsSupported = Hls.isSupported();
}
if (!hlsSupported) {
if (!nativeHlsSupported && !hlsSupported) {
this._forceMJPEG = this.stateObj!.entity_id;
return;
}
@@ -148,10 +152,10 @@ class HaCameraStream extends LitElement {
this.stateObj!.entity_id
);
if (Hls.isSupported()) {
this._renderHLSPolyfill(videoEl, Hls, url);
} else {
if (nativeHlsSupported) {
this._renderHLSNative(videoEl, url);
} else {
this._renderHLSPolyfill(videoEl, Hls!, url);
}
return;
} catch (err) {
@@ -177,7 +181,9 @@ class HaCameraStream extends LitElement {
url: string
) {
const hls = new Hls({
liveBackBufferLength: 60,
liveSyncDurationCount: 3,
liveBackBufferLength: 0,
liveDurationInfinity: true,
});
this._hlsPolyfillInstance = hls;
hls.attachMedia(videoEl);
+4 -16
View File
@@ -12,8 +12,6 @@ import {
class HaCard extends LitElement {
@property() public header?: string;
@property({ type: Boolean, reflect: true }) public outlined = false;
static get styles(): CSSResult {
return css`
:host {
@@ -21,12 +19,12 @@ class HaCard extends LitElement {
--ha-card-background,
var(--paper-card-background-color, white)
);
border-radius: var(--ha-card-border-radius, 4px);
border-radius: var(--ha-card-border-radius, 2px);
box-shadow: var(
--ha-card-box-shadow,
0px 2px 1px -1px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 1px 3px 0px rgba(0, 0, 0, 0.12)
0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12),
0 3px 1px -2px rgba(0, 0, 0, 0.2)
);
color: var(--primary-text-color);
display: block;
@@ -34,16 +32,6 @@ class HaCard extends LitElement {
position: relative;
}
:host([outlined]) {
box-shadow: none;
border-width: 1px;
border-style: solid;
border-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
}
.card-header,
:host ::slotted(.card-header) {
color: var(--ha-card-header-color, --primary-text-color);
-195
View File
@@ -1,195 +0,0 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
PropertyValues,
} from "lit-element";
import { HomeAssistant } from "../types";
import { mdiCalendar } from "@mdi/js";
import { formatDateTime } from "../common/datetime/format_date_time";
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import "./ha-svg-icon";
import "@polymer/paper-input/paper-input";
import "@material/mwc-list/mwc-list";
import "./date-range-picker";
export interface DateRangePickerRanges {
[key: string]: [Date, Date];
}
@customElement("ha-date-range-picker")
export class HaDateRangePicker extends LitElement {
@property() public hass!: HomeAssistant;
@property() public startDate!: Date;
@property() public endDate!: Date;
@property() public ranges?: DateRangePickerRanges;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) private _hour24format = false;
protected updated(changedProps: PropertyValues) {
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
this._hour24format = this._compute24hourFormat();
}
}
}
protected render(): TemplateResult {
return html`
<date-range-picker
?disabled=${this.disabled}
twentyfour-hours=${this._hour24format}
start-date=${this.startDate}
end-date=${this.endDate}
?ranges=${this.ranges !== undefined}
>
<div slot="input" class="date-range-inputs">
<ha-svg-icon path=${mdiCalendar}></ha-svg-icon>
<paper-input
.value=${formatDateTime(this.startDate, this.hass.language)}
.label=${this.hass.localize(
"ui.components.date-range-picker.start_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></paper-input>
<paper-input
.value=${formatDateTime(this.endDate, this.hass.language)}
label=${this.hass.localize(
"ui.components.date-range-picker.end_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></paper-input>
</div>
${this.ranges
? html`<div slot="ranges" class="date-range-ranges">
<mwc-list @click=${this._setDateRange}>
${Object.entries(this.ranges).map(
([name, dates]) => html`<mwc-list-item
.activated=${this.startDate.getTime() ===
dates[0].getTime() &&
this.endDate.getTime() === dates[1].getTime()}
.startDate=${dates[0]}
.endDate=${dates[1]}
>
${name}
</mwc-list-item>`
)}
</mwc-list>
</div>`
: ""}
<div slot="footer" class="date-range-footer">
<mwc-button @click=${this._cancelDateRange}
>${this.hass.localize("ui.common.cancel")}</mwc-button
>
<mwc-button @click=${this._applyDateRange}
>${this.hass.localize(
"ui.components.date-range-picker.select"
)}</mwc-button
>
</div>
</date-range-picker>
`;
}
private _compute24hourFormat() {
return (
new Intl.DateTimeFormat(this.hass.language, {
hour: "numeric",
})
.formatToParts(new Date(2020, 0, 1, 13))
.find((part) => part.type === "hour")!.value.length === 2
);
}
private _setDateRange(ev: Event) {
const target = ev.target as any;
const startDate = target.startDate;
const endDate = target.endDate;
const dateRangePicker = this._dateRangePicker;
dateRangePicker.clickRange([startDate, endDate]);
dateRangePicker.clickedApply();
}
private _cancelDateRange() {
this._dateRangePicker.clickCancel();
}
private _applyDateRange() {
this._dateRangePicker.clickedApply();
}
private get _dateRangePicker() {
const dateRangePicker = this.shadowRoot!.querySelector(
"date-range-picker"
) as any;
return dateRangePicker.vueComponent.$children[0];
}
private _handleInputClick() {
// close the date picker, so it will open again on the click event
if (this._dateRangePicker.open) {
this._dateRangePicker.open = false;
}
}
static get styles(): CSSResult {
return css`
ha-svg-icon {
margin-right: 8px;
}
.date-range-inputs {
display: flex;
align-items: center;
}
.date-range-ranges {
border-right: 1px solid var(--divider-color);
}
@media only screen and (max-width: 800px) {
.date-range-ranges {
border-right: none;
border-bottom: 1px solid var(--divider-color);
}
}
.date-range-footer {
display: flex;
justify-content: flex-end;
padding: 8px;
border-top: 1px solid var(--divider-color);
}
paper-input {
display: inline-block;
max-width: 200px;
}
paper-input:last-child {
margin-left: 8px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-date-range-picker": HaDateRangePicker;
}
}
-3
View File
@@ -25,9 +25,6 @@ export class HaDialog extends MwcDialog {
return [
style,
css`
.mdc-dialog {
z-index: var(--dialog-z-index, 7);
}
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);
}
+11 -4
View File
@@ -24,21 +24,28 @@ export class HaIconButton extends LitElement {
protected render(): TemplateResult {
return html`
<mwc-icon-button .label=${this.label} .disabled=${this.disabled}>
<mwc-icon-button
.label=${this.label}
?disabled=${this.disabled}
@click=${this._handleClick}
>
<ha-icon .icon=${this.icon}></ha-icon>
</mwc-icon-button>
`;
}
private _handleClick(ev) {
if (this.disabled) {
ev.stopPropagation();
}
}
static get styles(): CSSResult {
return css`
:host {
display: inline-block;
outline: none;
}
:host([disabled]) {
pointer-events: none;
}
mwc-icon-button {
--mdc-theme-on-primary: currentColor;
--mdc-theme-text-disabled-on-light: var(--disabled-text-color);
+1 -1
View File
@@ -54,7 +54,7 @@ class HaMarkdown extends LitElement {
}
ha-markdown-element code,
pre {
background-color: var(--markdown-code-background-color, none);
background-color: var(--markdown-code-background-color, #f6f8fa);
border-radius: 3px;
}
ha-markdown-element code {
+46
View File
@@ -0,0 +1,46 @@
/*
Wrapper for paper-textarea.
paper-textarea crashes on iOS when created programmatically. This only impacts
our automation and script editors as they are using Preact. Polymer is using
template elements and does not have this issue.
paper-textarea issue: https://github.com/PolymerElements/paper-input/issues/556
WebKit issue: https://bugs.webkit.org/show_bug.cgi?id=174629
*/
import "@polymer/paper-input/paper-textarea";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
class HaTextarea extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: block;
}
</style>
<paper-textarea
label="[[label]]"
placeholder="[[placeholder]]"
value="{{value}}"
></paper-textarea>
`;
}
static get properties() {
return {
name: String,
label: String,
placeholder: String,
value: {
type: String,
notify: true,
},
};
}
}
customElements.define("ha-textarea", HaTextarea);
-6
View File
@@ -1,11 +1,5 @@
import { HomeAssistant } from "../types";
export interface AuthUrlSearchParams {
client_id?: string;
redirect_uri?: string;
state?: string;
}
export interface AuthProvider {
name: string;
id: string;
+1 -1
View File
@@ -5,7 +5,7 @@ import { HomeAssistant } from "../types";
import { DataEntryFlowProgress, DataEntryFlowStep } from "./data_entry_flow";
import { domainToName } from "./integration";
export const DISCOVERY_SOURCES = ["unignore", "homekit", "ssdp", "zeroconf", "discovery"];
export const DISCOVERY_SOURCES = ["unignore", "homekit", "ssdp", "zeroconf"];
export const createConfigFlow = (hass: HomeAssistant, handler: string) =>
hass.callApi<DataEntryFlowStep>("POST", "config/config_entries/flow", {
-8
View File
@@ -12,11 +12,6 @@ export interface ConfigUpdateValues {
internal_url?: string | null;
}
export interface CheckConfigResult {
result: "valid" | "invalid";
errors: string | null;
}
export const saveCoreConfig = (
hass: HomeAssistant,
values: Partial<ConfigUpdateValues>
@@ -30,6 +25,3 @@ export const detectCoreConfig = (hass: HomeAssistant) =>
hass.callWS<Partial<ConfigUpdateValues>>({
type: "config/core/detect",
});
export const checkCoreConfig = (hass: HomeAssistant) =>
hass.callApi<CheckConfigResult>("POST", "config/core/check_config");
-20
View File
@@ -4,20 +4,6 @@ import { hassioApiResultExtractor, HassioResponse } from "./common";
export type HassioHomeAssistantInfo = any;
export type HassioSupervisorInfo = any;
export type HassioInfo = {
arch: string;
channel: string;
docker: string;
hassos?: string;
homeassistant: string;
hostname: string;
logging: string;
maching: string;
supervisor: string;
supported_arch: string[];
timezone: string;
};
export type HassioPanelInfo = PanelInfo<
| undefined
| {
@@ -52,12 +38,6 @@ export const fetchHassioSupervisorInfo = async (hass: HomeAssistant) => {
);
};
export const fetchHassioInfo = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioInfo>>("GET", "hassio/info")
);
};
export const fetchHassioLogs = async (
hass: HomeAssistant,
provider: string
-60
View File
@@ -1,67 +1,7 @@
import { HomeAssistant } from "../types";
export interface LogbookEntry {
when: string;
name: string;
message: string;
entity_id?: string;
domain: string;
context_user_id?: string;
}
const DATA_CACHE: {
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
} = {};
export const getLogbookData = (
hass: HomeAssistant,
startDate: string,
endDate: string,
entityId?: string
) => {
const ALL_ENTITIES = "*";
if (!entityId) {
entityId = ALL_ENTITIES;
}
const cacheKey = `${startDate}${endDate}`;
if (!DATA_CACHE[cacheKey]) {
DATA_CACHE[cacheKey] = {};
}
if (DATA_CACHE[cacheKey][entityId]) {
return DATA_CACHE[cacheKey][entityId];
}
if (entityId !== ALL_ENTITIES && DATA_CACHE[cacheKey][ALL_ENTITIES]) {
return DATA_CACHE[cacheKey][ALL_ENTITIES].then((entities) =>
entities.filter((entity) => entity.entity_id === entityId)
);
}
DATA_CACHE[cacheKey][entityId] = getLogbookDataFromServer(
hass,
startDate,
endDate,
entityId !== ALL_ENTITIES ? entityId : undefined
).then((entries) => entries.reverse());
return DATA_CACHE[cacheKey][entityId];
};
const getLogbookDataFromServer = async (
hass: HomeAssistant,
startDate: string,
endDate: string,
entityId?: string
) => {
const url = `logbook/${startDate}?end_time=${endDate}${
entityId ? `&entity=${entityId}` : ""
}`;
return hass.callApi<LogbookEntry[]>("GET", url);
};
export const clearLogbookCache = (startDate, endDate) => {
DATA_CACHE[`${startDate}${endDate}`] = {};
};
+1 -1
View File
@@ -51,7 +51,7 @@ export const onboardCoreConfigStep = (hass: HomeAssistant) =>
export const onboardIntegrationStep = (
hass: HomeAssistant,
params: { client_id: string; redirect_uri: string }
params: { client_id: string }
) =>
hass.callApi<OnboardingIntegrationStepResponse>(
"POST",
@@ -42,8 +42,6 @@ class StepFlowPickHandler extends LitElement {
private _width?: number;
private _height?: number;
private _getHandlers = memoizeOne(
(h: string[], filter?: string, _localize?: LocalizeFunc) => {
const handlers: HandlerObj[] = h.map((handler) => {
@@ -84,10 +82,7 @@ class StepFlowPickHandler extends LitElement {
@value-changed=${this._filterChanged}
></search-input>
<div
style=${styleMap({
width: `${this._width}px`,
height: `${this._height}px`,
})}
style=${styleMap({ width: `${this._width}px` })}
class=${classMap({ advanced: Boolean(this.showAdvanced) })}
>
${handlers.map(
@@ -144,20 +139,13 @@ class StepFlowPickHandler extends LitElement {
protected updated(changedProps) {
super.updated(changedProps);
// Store the width and height so that when we search, box doesn't jump
const div = this.shadowRoot!.querySelector("div")!;
// Store the width so that when we search, box doesn't jump
if (!this._width) {
const width = div.clientWidth;
const width = this.shadowRoot!.querySelector("div")!.clientWidth;
if (width) {
this._width = width;
}
}
if (!this._height) {
const height = div.clientHeight;
if (height) {
this._height = height;
}
}
}
private async _filterChanged(e) {
@@ -178,8 +166,8 @@ class StepFlowPickHandler extends LitElement {
configFlowContentStyles,
css`
img {
width: 40px;
height: 40px;
max-width: 40px;
max-height: 40px;
}
search-input {
display: block;
@@ -192,12 +180,12 @@ class StepFlowPickHandler extends LitElement {
overflow: auto;
max-height: 600px;
}
@media all and (max-height: 900px) {
@media all and (max-height: 1px) {
div {
max-height: calc(100vh - 134px);
max-height: calc(100vh - 205px);
}
div.advanced {
max-height: calc(100vh - 250px);
max-height: calc(100vh - 300px);
}
}
paper-icon-item {
@@ -66,7 +66,7 @@ class DialogDeviceRegistryDetail extends LitElement {
<paper-input
.value=${this._nameByUser}
@value-changed=${this._nameChanged}
.label=${this.hass.localize("ui.panel.config.devices.name")}
.label=${this.hass.localize("ui.dialogs.devices.name")}
.placeholder=${device.name || ""}
.disabled=${this._submitting}
></paper-input>
+45 -31
View File
@@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import {
css,
@@ -10,7 +11,7 @@ import {
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import "../../components/ha-dialog";
import "../../components/dialog/ha-paper-dialog";
import "../../components/ha-switch";
import { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles";
@@ -40,17 +41,21 @@ class DialogBox extends LitElement {
const confirmPrompt = this._params.confirmation || this._params.prompt;
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
@close=${this._close}
.heading=${this._params.title
? this._params.title
: this._params.confirmation &&
this.hass.localize("ui.dialogs.generic.default_confirmation_title")}
<ha-paper-dialog
with-backdrop
opened
modal
@opened-changed="${this._openedChanged}"
>
<div>
<h2>
${this._params.title
? this._params.title
: this._params.confirmation &&
this.hass.localize(
"ui.dialogs.generic.default_confirmation_title"
)}
</h2>
<paper-dialog-scrollable>
${this._params.text
? html`
<p
@@ -78,21 +83,23 @@ class DialogBox extends LitElement {
></paper-input>
`
: ""}
</div>
${confirmPrompt &&
html`
<mwc-button @click=${this._dismiss} slot="secondaryAction">
${this._params.dismissText
? this._params.dismissText
: this.hass.localize("ui.dialogs.generic.cancel")}
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
${confirmPrompt &&
html`
<mwc-button @click="${this._dismiss}">
${this._params.dismissText
? this._params.dismissText
: this.hass.localize("ui.dialogs.generic.cancel")}
</mwc-button>
`}
<mwc-button @click="${this._confirm}">
${this._params.confirmText
? this._params.confirmText
: this.hass.localize("ui.dialogs.generic.ok")}
</mwc-button>
`}
<mwc-button @click=${this._confirm} slot="primaryAction">
${this._params.confirmText
? this._params.confirmText
: this.hass.localize("ui.dialogs.generic.ok")}
</mwc-button>
</ha-dialog>
</div>
</ha-paper-dialog>
`;
}
@@ -120,8 +127,10 @@ class DialogBox extends LitElement {
this._dismiss();
}
private _close(): void {
this._params = undefined;
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!(ev.detail as any).value) {
this._params = undefined;
}
}
static get styles(): CSSResult[] {
@@ -132,6 +141,15 @@ class DialogBox extends LitElement {
pointer-events: initial !important;
cursor: initial !important;
}
ha-paper-dialog {
min-width: 400px;
max-width: 500px;
}
@media (max-width: 400px) {
ha-paper-dialog {
min-width: initial;
}
}
a {
color: var(--primary-color);
}
@@ -147,10 +165,6 @@ class DialogBox extends LitElement {
.secondary {
color: var(--secondary-text-color);
}
ha-dialog {
/* Place above other dialogs */
--dialog-z-index: 104;
}
`,
];
}
+5 -1
View File
@@ -86,7 +86,7 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
<div class="main-title" main-title="" on-click="enlarge">
[[_computeStateName(stateObj)]]
</div>
<template is="dom-if" if="[[hass.user.is_admin]]">
<template is="dom-if" if="[[_computeConfig(hass)]]">
<ha-icon-button
aria-label$="[[localize('ui.dialogs.more_info_control.settings')]]"
icon="hass:settings"
@@ -219,6 +219,10 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
return stateObj ? computeStateName(stateObj) : "";
}
_computeConfig(hass) {
return hass.user.is_admin && isComponentLoaded(hass, "config");
}
_computeEdit(hass, stateObj) {
const domain = this._computeDomain(stateObj);
return (
@@ -8,18 +8,15 @@ import {
property,
TemplateResult,
} from "lit-element";
import { computeStateName } from "../../../../../../common/entity/compute_state_name";
import "../../../../../../components/ha-dialog";
import "../../../../../../components/ha-switch";
import "../../../../../../components/ha-formfield";
import type { HaSwitch } from "../../../../../../components/ha-switch";
import { computeDeviceName } from "../../../../../../data/device_registry";
import {
fetchMQTTDebugInfo,
MQTTDeviceDebugInfo,
} from "../../../../../../data/mqtt";
import { haStyleDialog } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
import { computeStateName } from "../../common/entity/compute_state_name";
import "../../components/ha-dialog";
import "../../components/ha-switch";
import "../../components/ha-formfield";
import type { HaSwitch } from "../../components/ha-switch";
import { computeDeviceName } from "../../data/device_registry";
import { fetchMQTTDebugInfo, MQTTDeviceDebugInfo } from "../../data/mqtt";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import "./mqtt-discovery-payload";
import "./mqtt-messages";
import { MQTTDeviceDebugInfoDialogParams } from "./show-dialog-mqtt-device-debug-info";
@@ -190,19 +187,19 @@ class DialogMQTTDeviceDebugInfo extends LitElement {
<li class="triggerlistitem">
MQTT discovery data:
<ul class="discoverydata">
<li>
Topic:
<code>${trigger.discovery_data.topic}</code>
</li>
<li>
<mqtt-discovery-payload
.hass=${this.hass}
.payload=${trigger.discovery_data.payload}
.showAsYaml=${this._showAsYaml}
.summary=${"Payload"}
>
</mqtt-discovery-payload>
</li>
<li>
Topic:
<code>${trigger.discovery_data.topic}</code>
</li>
<li>
<mqtt-discovery-payload
.hass=${this.hass}
.payload=${trigger.discovery_data.payload}
.showAsYaml=${this._showAsYaml}
.summary=${"Payload"}
>
</li>
</mqtt-discovery-payload>
</ul>
</li>
`
@@ -9,9 +9,9 @@ import {
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { formatTimeWithSeconds } from "../../../../../../common/datetime/format_time";
import { HomeAssistant } from "../../../../../../types";
import { MQTTMessage } from "../../../../../../data/mqtt";
import { formatTimeWithSeconds } from "../../common/datetime/format_time";
import { HomeAssistant } from "../../types";
import { MQTTMessage } from "../../data/mqtt";
@customElement("mqtt-messages")
class MQTTMessages extends LitElement {
@@ -1,5 +1,5 @@
import { fireEvent } from "../../../../../../common/dom/fire_event";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fireEvent } from "../../common/dom/fire_event";
import { DeviceRegistryEntry } from "../../data/device_registry";
export interface MQTTDeviceDebugInfoDialogParams {
device: DeviceRegistryEntry;
@@ -0,0 +1,113 @@
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "../../components/dialog/ha-paper-dialog";
import type { HaPaperDialog } from "../../components/dialog/ha-paper-dialog";
import { fetchZHADevice, ZHADevice } from "../../data/zha";
import "../../panels/config/zha/zha-device-card";
import type { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import type { ZHADeviceInfoDialogParams } from "./show-dialog-zha-device-info";
@customElement("dialog-zha-device-info")
class DialogZHADeviceInfo extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _params?: ZHADeviceInfoDialogParams;
@property() private _error?: string;
@property() private _device?: ZHADevice;
public async showDialog(params: ZHADeviceInfoDialogParams): Promise<void> {
this._params = params;
this._device = await fetchZHADevice(this.hass, params.ieee);
await this.updateComplete;
this._dialog.open();
}
protected render(): TemplateResult {
if (!this._params || !this._device) {
return html``;
}
return html`
<ha-paper-dialog
with-backdrop
opened
@opened-changed=${this._openedChanged}
>
${this._error
? html` <div class="error">${this._error}</div> `
: html`
<zha-device-card
class="card"
.hass=${this.hass}
.device=${this._device}
@zha-device-removed=${this._onDeviceRemoved}
.showEntityDetail=${false}
.showActions="${this._device.device_type !== "Coordinator"}"
></zha-device-card>
`}
</ha-paper-dialog>
`;
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!ev.detail.value) {
this._params = undefined;
this._error = undefined;
this._device = undefined;
}
}
private _onDeviceRemoved(): void {
this._closeDialog();
}
private get _dialog(): HaPaperDialog {
return this.shadowRoot!.querySelector("ha-paper-dialog")!;
}
private _closeDialog() {
this._dialog.close();
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
ha-paper-dialog > * {
margin: 0;
display: block;
padding: 0;
}
.card {
box-sizing: border-box;
display: flex;
flex: 1 0 300px;
min-width: 0;
max-width: 600px;
word-wrap: break-word;
}
.error {
color: var(--google-red-500);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-zha-device-info": DialogZHADeviceInfo;
}
}
@@ -0,0 +1,21 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface ZHADeviceInfoDialogParams {
ieee: string;
}
export const loadZHADeviceInfoDialog = () =>
import(
/* webpackChunkName: "dialog-zha-device-info" */ "./dialog-zha-device-info"
);
export const showZHADeviceInfoDialog = (
element: HTMLElement,
zhaDeviceInfoParams: ZHADeviceInfoDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-zha-device-info",
dialogImport: loadZHADeviceInfoDialog,
dialogParams: zhaDeviceInfoParams,
});
};
@@ -6,10 +6,10 @@ import {
property,
TemplateResult,
} from "lit-element";
import "../../../../../components/ha-code-editor";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../../components/ha-code-editor";
import { createCloseHeading } from "../../components/ha-dialog";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { ZHADeviceZigbeeInfoDialogParams } from "./show-dialog-zha-device-zigbee-info";
@customElement("dialog-zha-device-zigbee-info")
@@ -1,5 +1,5 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
import { ZHADevice } from "../../../../../data/zha";
import { fireEvent } from "../../common/dom/fire_event";
import { ZHADevice } from "../../data/zha";
export interface ZHADeviceZigbeeInfoDialogParams {
device: ZHADevice;
+1 -1
View File
@@ -2,7 +2,7 @@
if (navigator.userAgent.indexOf("Android") === -1 &&
navigator.userAgent.indexOf("CrOS") === -1) {
function _pf(src, type) {
var el = document.createElement("link");
const el = document.createElement("link");
el.rel = "preload";
el.as = "font";
el.type = "font/woff2";
+6 -10
View File
@@ -58,13 +58,12 @@
window.customPanelJS = "<%= latestCustomPanelJS %>";
window.latestJS = true;
</script>
<script>
{% for extra_module in extra_modules -%}
import("{{ extra_module }}");
<script type="module" crossorigin="use-credentials" src="{{ extra_module }}"></script>
{% endfor -%}
</script>
<script>
(function() {
if (!window.latestJS) {
window.customPanelJS = "<%= es5CustomPanelJS %>";
@@ -80,14 +79,11 @@
_ls("<%= es5CoreJS %>");
_ls("<%= es5AppJS %>");
<% } %>
{% for extra_script in extra_js_es5 -%}
_ls("{{ extra_script }}");
{% endfor -%}
}
</script>
<script>
if (!window.latestJS) {
{% for extra_script in extra_js_es5 -%}
_ls("{{ extra_script }}");
{% endfor -%}
}
})();
</script>
{% for extra_url in extra_urls -%}
-35
View File
@@ -91,20 +91,6 @@ class PartialPanelResolver extends HassRouterPage {
private _waitForStart = false;
private _disconnectedPanel?: ChildNode;
private _hiddenTimeout?: number;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
document.addEventListener(
"visibilitychange",
() => this._handleVisibilityChange(),
false
);
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
@@ -155,27 +141,6 @@ class PartialPanelResolver extends HassRouterPage {
}
}
private _handleVisibilityChange() {
if (document.hidden) {
this._hiddenTimeout = window.setTimeout(() => {
this._hiddenTimeout = undefined;
if (this.lastChild) {
this._disconnectedPanel = this.lastChild;
this.removeChild(this.lastChild);
}
}, 300000);
} else {
if (this._hiddenTimeout) {
clearTimeout(this._hiddenTimeout);
this._hiddenTimeout = undefined;
}
if (this._disconnectedPanel) {
this.appendChild(this._disconnectedPanel);
this._disconnectedPanel = undefined;
}
}
}
private async _updateRoutes(oldPanels?: HomeAssistant["panels"]) {
this.routerOptions = getRoutes(this.hass.panels);
+24 -63
View File
@@ -1,9 +1,9 @@
import {
Auth,
createConnection,
genClientId,
getAuth,
subscribeConfig,
genClientId,
} from "home-assistant-js-websocket";
import {
customElement,
@@ -14,12 +14,12 @@ import {
} from "lit-element";
import { HASSDomEvent } from "../common/dom/fire_event";
import { subscribeOne } from "../common/util/subscribe-one";
import { hassUrl, AuthUrlSearchParams } from "../data/auth";
import { hassUrl } from "../data/auth";
import {
fetchOnboardingOverview,
OnboardingResponses,
OnboardingStep,
onboardIntegrationStep,
ValidOnboardingStep,
} from "../data/onboarding";
import { subscribeUser } from "../data/ws-user";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
@@ -28,28 +28,19 @@ import { HomeAssistant } from "../types";
import { registerServiceWorker } from "../util/register-service-worker";
import "./onboarding-create-user";
import "./onboarding-loading";
import { extractSearchParamsObject } from "../common/url/search-params";
type OnboardingEvent =
| {
type: "user";
result: OnboardingResponses["user"];
}
| {
type: "core_config";
result: OnboardingResponses["core_config"];
}
| {
type: "integration";
};
interface OnboardingEvent<T extends ValidOnboardingStep> {
type: T;
result: OnboardingResponses[T];
}
declare global {
interface HASSDomEvents {
"onboarding-step": OnboardingEvent;
"onboarding-step": OnboardingEvent<ValidOnboardingStep>;
}
interface GlobalEventHandlersEventMap {
"onboarding-step": HASSDomEvent<OnboardingEvent>;
"onboarding-step": HASSDomEvent<OnboardingEvent<ValidOnboardingStep>>;
}
}
@@ -159,7 +150,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
}
}
private async _handleStepDone(ev: HASSDomEvent<OnboardingEvent>) {
private async _handleStepDone(
ev: HASSDomEvent<OnboardingEvent<ValidOnboardingStep>>
) {
const stepResult = ev.detail;
this._steps = this._steps!.map((step) =>
step.step === stepResult.type ? { ...step, done: true } : step
@@ -183,41 +176,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
} else if (stepResult.type === "core_config") {
// We do nothing
} else if (stepResult.type === "integration") {
const result = stepResult.result as OnboardingResponses["integration"];
this._loading = true;
// Determine if oauth redirect has been provided
const externalAuthParams = extractSearchParamsObject() as AuthUrlSearchParams;
const authParams =
externalAuthParams.client_id && externalAuthParams.redirect_uri
? externalAuthParams
: {
client_id: genClientId(),
redirect_uri: `${location.protocol}//${location.host}/?auth_callback=1`,
state: btoa(
JSON.stringify({
hassUrl: `${location.protocol}//${location.host}`,
clientId: genClientId(),
})
),
};
let result: OnboardingResponses["integration"];
try {
result = await onboardIntegrationStep(this.hass!, {
client_id: authParams.client_id!,
redirect_uri: authParams.redirect_uri!,
});
} catch (err) {
this.hass!.connection.close();
await this.hass!.auth.revoke();
alert(`Unable to finish onboarding: ${err.message}`);
document.location.assign("/?");
return;
}
// If we don't close the connection manually, the connection will be
// closed when we navigate away from the page. Firefox allows JS to
// continue to execute, and so HAWS will automatically reconnect once
@@ -230,17 +191,17 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
// Revoke current auth token.
await this.hass!.auth.revoke();
// Build up the url to redirect to
let redirectUrl = authParams.redirect_uri!;
redirectUrl +=
(redirectUrl.includes("?") ? "&" : "?") +
`code=${encodeURIComponent(result.auth_code)}`;
if (authParams.state) {
redirectUrl += `&state=${encodeURIComponent(authParams.state)}`;
}
document.location.assign(redirectUrl);
const state = btoa(
JSON.stringify({
hassUrl: `${location.protocol}//${location.host}`,
clientId: genClientId(),
})
);
document.location.assign(
`/?auth_callback=1&code=${encodeURIComponent(
result.auth_code
)}&state=${state}`
);
}
}
@@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button";
import { genClientId } from "home-assistant-js-websocket";
import {
css,
CSSResult,
@@ -20,6 +21,7 @@ import {
} from "../data/config_flow";
import { DataEntryFlowProgress } from "../data/data_entry_flow";
import { domainToName } from "../data/integration";
import { onboardIntegrationStep } from "../data/onboarding";
import {
loadConfigFlowDialog,
showConfigFlowDialog,
@@ -167,8 +169,12 @@ class OnboardingIntegrations extends LitElement {
}
private async _finish() {
const result = await onboardIntegrationStep(this.hass, {
client_id: genClientId(),
});
fireEvent(this, "onboarding-step", {
type: "integration",
result,
});
}
@@ -1,5 +1,4 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { customElement, LitElement, property } from "lit-element";
import { html } from "lit-html";
import { WaitAction } from "../../../../../data/script";
@@ -20,7 +19,7 @@ export class HaWaitAction extends LitElement implements ActionElement {
const { wait_template, timeout } = this.action;
return html`
<paper-textarea
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_template.wait_template"
)}
@@ -28,7 +27,7 @@ export class HaWaitAction extends LitElement implements ActionElement {
.value=${wait_template}
@value-changed=${this._valueChanged}
dir="ltr"
></paper-textarea>
></ha-textarea>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_template.timeout"
@@ -2,7 +2,7 @@ import "@polymer/paper-input/paper-input";
import { customElement, html, LitElement, property } from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker";
import "@polymer/paper-input/paper-textarea";
import "../../../../../components/ha-textarea";
import { NumericStateCondition } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-condition-row";
@@ -45,7 +45,7 @@ export default class HaNumericStateCondition extends LitElement {
.value=${below}
@value-changed=${this._valueChanged}
></paper-input>
<paper-textarea
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.numeric_state.value_template"
)}
@@ -53,7 +53,7 @@ export default class HaNumericStateCondition extends LitElement {
.value=${value_template}
@value-changed=${this._valueChanged}
dir="ltr"
></paper-textarea>
></ha-textarea>
`;
}
@@ -1,5 +1,5 @@
import { customElement, html, LitElement, property } from "lit-element";
import "@polymer/paper-input/paper-textarea";
import "../../../../../components/ha-textarea";
import { TemplateCondition } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-condition-row";
@@ -17,7 +17,7 @@ export class HaTemplateCondition extends LitElement {
protected render() {
const { value_template } = this.condition;
return html`
<paper-textarea
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.template.value_template"
)}
@@ -25,7 +25,7 @@ export class HaTemplateCondition extends LitElement {
.value=${value_template}
@value-changed=${this._valueChanged}
dir="ltr"
></paper-textarea>
></ha-textarea>
`;
}
@@ -1,6 +1,5 @@
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-input/paper-textarea";
import "../../../components/ha-icon-button";
import {
css,
@@ -118,7 +117,7 @@ export class HaAutomationEditor extends LitElement {
@value-changed=${this._valueChanged}
>
</paper-input>
<paper-textarea
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
@@ -128,7 +127,7 @@ export class HaAutomationEditor extends LitElement {
name="description"
.value=${this._config.description}
@value-changed=${this._valueChanged}
></paper-textarea>
></ha-textarea>
</div>
${stateObj
? html`
@@ -54,7 +54,9 @@ class HaConfigAutomation extends HassRouterPage {
private _getAutomations = memoizeOne(
(states: HassEntities): AutomationEntity[] => {
return Object.values(states).filter(
(entity) => computeStateDomain(entity) === "automation"
(entity) =>
computeStateDomain(entity) === "automation" &&
!entity.attributes.hidden
) as AutomationEntity[];
}
);
@@ -2,7 +2,7 @@ import "@polymer/paper-input/paper-input";
import { customElement, html, LitElement, property } from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker";
import "@polymer/paper-input/paper-textarea";
import "../../../../../components/ha-textarea";
import { ForDict, NumericStateTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-trigger-row";
@@ -61,7 +61,7 @@ export default class HaNumericStateTrigger extends LitElement {
.value=${below}
@value-changed=${this._valueChanged}
></paper-input>
<paper-textarea
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.value_template"
)}
@@ -69,7 +69,7 @@ export default class HaNumericStateTrigger extends LitElement {
.value=${value_template}
@value-changed=${this._valueChanged}
dir="ltr"
></paper-textarea>
></ha-textarea>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.for"
@@ -1,5 +1,5 @@
import { customElement, html, LitElement, property } from "lit-element";
import "@polymer/paper-input/paper-textarea";
import "../../../../../components/ha-textarea";
import { TemplateTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-trigger-row";
@@ -17,7 +17,7 @@ export class HaTemplateTrigger extends LitElement {
protected render() {
const { value_template } = this.trigger;
return html`
<paper-textarea
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.template.value_template"
)}
@@ -25,7 +25,7 @@ export class HaTemplateTrigger extends LitElement {
.value=${value_template}
@value-changed=${this._valueChanged}
dir="ltr"
></paper-textarea>
></ha-textarea>
`;
}
@@ -20,7 +20,6 @@ import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import "./ha-config-navigation";
import { mdiCloudLock } from "@mdi/js";
@customElement("ha-config-dashboard")
class HaConfigDashboard extends LitElement {
@@ -67,7 +66,7 @@ class HaConfigDashboard extends LitElement {
path: "/config/cloud",
translationKey: "ui.panel.config.cloud.caption",
info: this.cloudStatus,
iconPath: mdiCloudLock,
icon: "hass:cloud-lock",
},
]}
></ha-config-navigation>
@@ -85,36 +84,6 @@ class HaConfigDashboard extends LitElement {
</ha-card>
`
)}
${isComponentLoaded(this.hass, "zha")
? html`
<div class="promo-advanced">
${this.hass.localize(
"ui.panel.config.integration_panel_move.missing_zha",
"integrations_page",
html`<a href="/config/integrations">
${this.hass.localize(
"ui.panel.config.integration_panel_move.link_integration_page"
)}
</a>`
)}
</div>
`
: ""}
${isComponentLoaded(this.hass, "zwave")
? html`
<div class="promo-advanced">
${this.hass.localize(
"ui.panel.config.integration_panel_move.missing_zwave",
"integrations_page",
html`<a href="/config/integrations">
${this.hass.localize(
"ui.panel.config.integration_panel_move.link_integration_page"
)}
</a>`
)}
</div>
`
: ""}
${!this.showAdvanced
? html`
<div class="promo-advanced">
@@ -38,10 +38,7 @@ class HaConfigNavigation extends LitElement {
tabindex="-1"
>
<paper-icon-item>
<ha-svg-icon
.path=${page.iconPath}
slot="item-icon"
></ha-svg-icon>
<ha-icon .icon=${page.icon} slot="item-icon"></ha-icon>
<paper-item-body two-line>
${this.hass.localize(
page.translationKey ||
@@ -91,7 +88,7 @@ class HaConfigNavigation extends LitElement {
display: block;
outline: 0;
}
ha-svg-icon,
ha-icon,
ha-icon-next {
color: var(--secondary-text-color);
}
@@ -5,17 +5,16 @@ import {
LitElement,
property,
TemplateResult,
css,
} from "lit-element";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { removeMQTTDeviceEntry } from "../../../../../../data/mqtt";
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
import { showMQTTDeviceDebugInfoDialog } from "./show-dialog-mqtt-device-debug-info";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
import { DeviceRegistryEntry } from "../../../../data/device_registry";
import { removeMQTTDeviceEntry } from "../../../../data/mqtt";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { showMQTTDeviceDebugInfoDialog } from "../../../../dialogs/mqtt-device-debug-info-dialog/show-dialog-mqtt-device-debug-info";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
@customElement("ha-device-actions-mqtt")
export class HaDeviceActionsMqtt extends LitElement {
@customElement("ha-device-card-mqtt")
export class HaDeviceCardMqtt extends LitElement {
@property() public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@@ -48,15 +47,7 @@ export class HaDeviceActionsMqtt extends LitElement {
await showMQTTDeviceDebugInfoDialog(this, { device });
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
:host {
display: flex;
justify-content: space-between;
}
`,
];
static get styles(): CSSResult {
return haStyle;
}
}
@@ -75,7 +75,6 @@ export class HaDeviceCard extends LitElement {
: ""}
<slot></slot>
</div>
<slot name="actions"></slot>
</ha-card>
`;
}
@@ -101,6 +100,7 @@ export class HaDeviceCard extends LitElement {
}
ha-card {
flex: 1 0 100%;
padding-bottom: 10px;
min-width: 0;
}
.device {
@@ -1,138 +0,0 @@
import {
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
css,
PropertyValues,
} from "lit-element";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
import {
ZHADevice,
fetchZHADevice,
reconfigureNode,
} from "../../../../../../data/zha";
import { navigate } from "../../../../../../common/navigate";
import { showZHADeviceZigbeeInfoDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info";
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
import { showZHAClusterDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-cluster";
@customElement("ha-device-actions-zha")
export class HaDeviceActionsZha extends LitElement {
@property() public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@property() private _zhaDevice?: ZHADevice;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
const zigbeeConnection = this.device.connections.find(
(conn) => conn[0] === "zigbee"
);
if (!zigbeeConnection) {
return;
}
fetchZHADevice(this.hass, zigbeeConnection[1]).then((device) => {
this._zhaDevice = device;
});
}
}
protected render(): TemplateResult {
if (!this._zhaDevice) {
return html``;
}
return html`
${this._zhaDevice.device_type !== "Coordinator"
? html`
<mwc-button @click=${this._onReconfigureNodeClick}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.reconfigure"
)}
</mwc-button>
`
: ""}
${this._zhaDevice.power_source === "Mains" &&
(this._zhaDevice.device_type === "Router" ||
this._zhaDevice.device_type === "Coordinator")
? html`
<mwc-button @click=${this._onAddDevicesClick}>
${this.hass!.localize("ui.dialogs.zha_device_info.buttons.add")}
</mwc-button>
`
: ""}
${this._zhaDevice.device_type !== "Coordinator"
? html`
<mwc-button @click=${this._handleZigbeeInfoClicked}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.zigbee_information"
)}
</mwc-button>
<mwc-button @click=${this._showClustersDialog}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.clusters"
)}
</mwc-button>
<mwc-button class="warning" @click=${this._removeDevice}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.remove"
)}
</mwc-button>
`
: ""}
`;
}
private async _showClustersDialog(): Promise<void> {
await showZHAClusterDialog(this, { device: this._zhaDevice! });
}
private async _onReconfigureNodeClick(): Promise<void> {
if (!this.hass) {
return;
}
reconfigureNode(this.hass, this._zhaDevice!.ieee);
}
private _onAddDevicesClick() {
navigate(this, "/config/zha/add/" + this._zhaDevice!.ieee);
}
private async _handleZigbeeInfoClicked() {
showZHADeviceZigbeeInfoDialog(this, { device: this._zhaDevice! });
}
private async _removeDevice() {
const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove"
),
});
if (!confirmed) {
return;
}
this.hass.callService("zha", "remove", {
ieee_address: this._zhaDevice!.ieee,
});
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
:host {
display: flex;
flex-direction: column;
align-items: flex-start;
}
`,
];
}
}
@@ -1,93 +0,0 @@
import {
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
css,
PropertyValues,
} from "lit-element";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
import { ZHADevice, fetchZHADevice } from "../../../../../../data/zha";
import { formatAsPaddedHex } from "../../../../integrations/integration-panels/zha/functions";
@customElement("ha-device-info-zha")
export class HaDeviceActionsZha extends LitElement {
@property() public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@property() private _zhaDevice?: ZHADevice;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
const zigbeeConnection = this.device.connections.find(
(conn) => conn[0] === "zigbee"
);
if (!zigbeeConnection) {
return;
}
fetchZHADevice(this.hass, zigbeeConnection[1]).then((device) => {
this._zhaDevice = device;
});
}
}
protected render(): TemplateResult {
if (!this._zhaDevice) {
return html``;
}
return html`
<h4>Zigbee info</h4>
<div>IEEE: ${this._zhaDevice.ieee}</div>
<div>Nwk: ${formatAsPaddedHex(this._zhaDevice.nwk)}</div>
<div>Device Type: ${this._zhaDevice.device_type}</div>
<div>
LQI:
${this._zhaDevice.lqi ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
<div>
RSSI:
${this._zhaDevice.rssi ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
<div>
${this.hass!.localize("ui.dialogs.zha_device_info.last_seen")}:
${this._zhaDevice.last_seen ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
<div>
${this.hass!.localize("ui.dialogs.zha_device_info.power_source")}:
${this._zhaDevice.power_source ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
${this._zhaDevice.quirk_applied
? html`
<div>
${this.hass!.localize("ui.dialogs.zha_device_info.quirk")}:
${this._zhaDevice.quirk_class}
</div>
`
: ""}
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
h4 {
margin-bottom: 4px;
}
div {
word-break: break-all;
margin-top: 2px;
}
`,
];
}
}
@@ -6,7 +6,6 @@ import {
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one";
@@ -39,12 +38,13 @@ import "../../../layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../types";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import "./device-detail/ha-device-card-mqtt";
import "./device-detail/ha-device-entities-card";
import "./device-detail/ha-device-info-card";
import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation";
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string | null;
stateName?: string;
}
@customElement("ha-config-device-page")
@@ -226,7 +226,16 @@ export class HaConfigDevicePage extends LitElement {
.devices=${this.devices}
.device=${device}
>
${this._renderIntegrationInfo(device, integrations)}
${
integrations.includes("mqtt")
? html`
<ha-device-card-mqtt
.hass=${this.hass}
.device=${device}
></ha-device-card-mqtt>
`
: html``
}
</ha-device-info-card>
${
@@ -430,7 +439,7 @@ export class HaConfigDevicePage extends LitElement {
</hass-tabs-subpage> `;
}
private _computeEntityName(entity: EntityRegistryEntry) {
private _computeEntityName(entity) {
if (entity.name) {
return entity.name;
}
@@ -471,43 +480,6 @@ export class HaConfigDevicePage extends LitElement {
});
}
private _renderIntegrationInfo(
device,
integrations: string[]
): TemplateResult[] {
const templates: TemplateResult[] = [];
if (integrations.includes("mqtt")) {
import(
"./device-detail/integration-elements/mqtt/ha-device-actions-mqtt"
);
templates.push(html`
<div class="card-actions" slot="actions">
<ha-device-actions-mqtt
.hass=${this.hass}
.device=${device}
></ha-device-actions-mqtt>
</div>
`);
}
if (integrations.includes("zha")) {
import("./device-detail/integration-elements/zha/ha-device-actions-zha");
import("./device-detail/integration-elements/zha/ha-device-info-zha");
templates.push(html`
<ha-device-info-zha
.hass=${this.hass}
.device=${device}
></ha-device-info-zha>
<div class="card-actions" slot="actions">
<ha-device-actions-zha
.hass=${this.hass}
.device=${device}
></ha-device-actions-zha>
</div>
`);
}
return templates;
}
private async _showSettings() {
const device = this._device(this.deviceId, this.devices)!;
showDeviceRegistryDetailDialog(this, {
+31 -73
View File
@@ -9,25 +9,6 @@ import "../../layouts/hass-loading-screen";
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../types";
import {
mdiPuzzle,
mdiDevices,
mdiShape,
mdiSofa,
mdiRobot,
mdiPalette,
mdiScriptText,
mdiTools,
mdiViewDashboard,
mdiAccount,
mdiMapMarkerRadius,
mdiAccountBadgeHorizontal,
mdiHomeAssistant,
mdiServer,
mdiInformation,
mdiMathLog,
mdiPencil,
} from "@mdi/js";
declare global {
// for fire event
@@ -42,28 +23,28 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "integrations",
path: "/config/integrations",
translationKey: "ui.panel.config.integrations.caption",
iconPath: mdiPuzzle,
icon: "hass:puzzle",
core: true,
},
{
component: "devices",
path: "/config/devices",
translationKey: "ui.panel.config.devices.caption",
iconPath: mdiDevices,
icon: "hass:devices",
core: true,
},
{
component: "entities",
path: "/config/entities",
translationKey: "ui.panel.config.entities.caption",
iconPath: mdiShape,
icon: "hass:shape",
core: true,
},
{
component: "areas",
path: "/config/areas",
translationKey: "ui.panel.config.areas.caption",
iconPath: mdiSofa,
icon: "hass:sofa",
core: true,
},
],
@@ -72,25 +53,25 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "automation",
path: "/config/automation",
translationKey: "ui.panel.config.automation.caption",
iconPath: mdiRobot,
icon: "hass:robot",
},
{
component: "scene",
path: "/config/scene",
translationKey: "ui.panel.config.scene.caption",
iconPath: mdiPalette,
icon: "hass:palette",
},
{
component: "script",
path: "/config/script",
translationKey: "ui.panel.config.script.caption",
iconPath: mdiScriptText,
icon: "hass:script-text",
},
{
component: "helpers",
path: "/config/helpers",
translationKey: "ui.panel.config.helpers.caption",
iconPath: mdiTools,
icon: "hass:tools",
core: true,
},
],
@@ -99,7 +80,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "lovelace",
path: "/config/lovelace/dashboards",
translationKey: "ui.panel.config.lovelace.caption",
iconPath: mdiViewDashboard,
icon: "hass:view-dashboard",
},
],
persons: [
@@ -107,19 +88,19 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "person",
path: "/config/person",
translationKey: "ui.panel.config.person.caption",
iconPath: mdiAccount,
icon: "hass:account",
},
{
component: "zone",
path: "/config/zone",
translationKey: "ui.panel.config.zone.caption",
iconPath: mdiMapMarkerRadius,
icon: "hass:map-marker-radius",
},
{
component: "users",
path: "/config/users",
translationKey: "ui.panel.config.users.caption",
iconPath: mdiAccountBadgeHorizontal,
icon: "hass:account-badge-horizontal",
core: true,
},
],
@@ -128,41 +109,39 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "core",
path: "/config/core",
translationKey: "ui.panel.config.core.caption",
iconPath: mdiHomeAssistant,
icon: "hass:home-assistant",
core: true,
},
{
component: "server_control",
path: "/config/server_control",
translationKey: "ui.panel.config.server_control.caption",
iconPath: mdiServer,
icon: "hass:server",
core: true,
},
{
component: "logs",
path: "/config/logs",
translationKey: "ui.panel.config.logs.caption",
iconPath: mdiMathLog,
core: true,
},
{
component: "info",
path: "/config/info",
translationKey: "ui.panel.config.info.caption",
iconPath: mdiInformation,
core: true,
},
],
advanced: [
{
component: "customize",
path: "/config/customize",
translationKey: "ui.panel.config.customize.caption",
iconPath: mdiPencil,
icon: "hass:pencil",
core: true,
advancedOnly: true,
},
],
other: [
{
component: "zha",
path: "/config/zha",
translationKey: "component.zha.title",
icon: "hass:zigbee",
},
{
component: "zwave",
path: "/config/zwave",
translationKey: "component.zwave.title",
icon: "hass:z-wave",
},
],
};
@customElement("ha-panel-config")
@@ -218,20 +197,6 @@ class HaPanelConfig extends HassRouterPage {
/* webpackChunkName: "panel-config-server-control" */ "./server_control/ha-config-server-control"
),
},
logs: {
tag: "ha-config-logs",
load: () =>
import(
/* webpackChunkName: "panel-config-logs" */ "./logs/ha-config-logs"
),
},
info: {
tag: "ha-config-info",
load: () =>
import(
/* webpackChunkName: "panel-config-info" */ "./info/ha-config-info"
),
},
customize: {
tag: "ha-config-customize",
load: () =>
@@ -313,21 +278,14 @@ class HaPanelConfig extends HassRouterPage {
tag: "zha-config-dashboard-router",
load: () =>
import(
/* webpackChunkName: "panel-config-zha" */ "./integrations/integration-panels/zha/zha-config-dashboard-router"
/* webpackChunkName: "panel-config-zha" */ "./zha/zha-config-dashboard-router"
),
},
zwave: {
tag: "ha-config-zwave",
load: () =>
import(
/* webpackChunkName: "panel-config-zwave" */ "./integrations/integration-panels/zwave/ha-config-zwave"
),
},
mqtt: {
tag: "mqtt-config-panel",
load: () =>
import(
/* webpackChunkName: "panel-config-mqtt" */ "./integrations/integration-panels/mqtt/mqtt-config-panel"
/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave"
),
},
},
-209
View File
@@ -1,209 +0,0 @@
import {
css,
CSSResult,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import "./integrations-card";
import "./system-health-card";
import { configSections } from "../ha-panel-config";
import "../../../layouts/hass-tabs-subpage";
const JS_TYPE = __BUILD__;
const JS_VERSION = __VERSION__;
class HaConfigInfo extends LitElement {
@property() public hass!: HomeAssistant;
@property() public narrow!: boolean;
@property() public isWide!: boolean;
@property() public showAdvanced!: boolean;
@property() public route!: Route;
protected render(): TemplateResult {
const hass = this.hass;
const customUiList: Array<{ name: string; url: string; version: string }> =
(window as any).CUSTOM_UI_LIST || [];
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.general}
>
<div class="about">
<a
href="https://www.home-assistant.io"
target="_blank"
rel="noreferrer"
><img
src="/static/icons/favicon-192x192.png"
height="192"
alt="${this.hass.localize(
"ui.panel.config.info.home_assistant_logo"
)}"
/></a>
<br />
<h2>Home Assistant ${hass.connection.haVersion}</h2>
<p>
${this.hass.localize(
"ui.panel.config.info.path_configuration",
"path",
hass.config.config_dir
)}
</p>
<p class="develop">
<a
href="https://www.home-assistant.io/developers/credits/"
target="_blank"
rel="noreferrer"
>
${this.hass.localize("ui.panel.config.info.developed_by")}
</a>
</p>
<p>
${this.hass.localize("ui.panel.config.info.license")}<br />
${this.hass.localize("ui.panel.config.info.source")}
<a
href="https://github.com/home-assistant/core"
target="_blank"
rel="noreferrer"
>${this.hass.localize("ui.panel.config.info.server")}</a
>
&mdash;
<a
href="https://github.com/home-assistant/frontend"
target="_blank"
rel="noreferrer"
>${this.hass.localize("ui.panel.config.info.frontend")}</a
>
</p>
<p>
${this.hass.localize("ui.panel.config.info.built_using")}
<a href="https://www.python.org" target="_blank" rel="noreferrer"
>Python 3</a
>,
<a
href="https://www.polymer-project.org"
target="_blank"
rel="noreferrer"
>Polymer</a
>, ${this.hass.localize("ui.panel.config.info.icons_by")}
<a
href="https://www.google.com/design/icons/"
target="_blank"
rel="noreferrer"
>Google</a
>
${this.hass.localize("ui.common.and")}
<a
href="https://MaterialDesignIcons.com"
target="_blank"
rel="noreferrer"
>MaterialDesignIcons.com</a
>.
</p>
<p>
${this.hass.localize(
"ui.panel.config.info.frontend_version",
"version",
JS_VERSION,
"type",
JS_TYPE
)}
${customUiList.length > 0
? html`
<div>
${this.hass.localize("ui.panel.config.info.custom_uis")}
${customUiList.map(
(item) => html`
<div>
<a href="${item.url}" target="_blank"> ${item.name}</a
>: ${item.version}
</div>
`
)}
</div>
`
: ""}
</p>
</div>
<div class="content">
<system-health-card .hass=${this.hass}></system-health-card>
<integrations-card .hass=${this.hass}></integrations-card>
</div>
</hass-tabs-subpage>
`;
}
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
// Legacy custom UI can be slow to register, give them time.
const customUI = ((window as any).CUSTOM_UI_LIST || []).length;
setTimeout(() => {
if (((window as any).CUSTOM_UI_LIST || []).length !== customUI.length) {
this.requestUpdate();
}
}, 1000);
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
:host {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
.content {
direction: ltr;
}
.about {
text-align: center;
line-height: 2em;
}
.version {
@apply --paper-font-headline;
}
.develop {
@apply --paper-font-subhead;
}
.about a {
color: var(--primary-color);
}
system-health-card,
integrations-card {
display: block;
max-width: 600px;
margin: 0 auto;
padding-bottom: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-info": HaConfigInfo;
}
}
customElements.define("ha-config-info", HaConfigInfo);
@@ -299,7 +299,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
${this._showIgnored
? ignoredConfigEntries.map(
(item: ConfigEntryExtended) => html`
<ha-card outlined class="ignored">
<ha-card class="ignored">
<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.ignore.ignored"
@@ -335,7 +335,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
${configEntriesInProgress.length
? configEntriesInProgress.map(
(flow: DataEntryFlowProgressExtended) => html`
<ha-card outlined class="discovered">
<ha-card class="discovered">
<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.discovered"
@@ -396,7 +396,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
)
: !this._configEntries.length
? html`
<ha-card outlined>
<ha-card>
<div class="card-content">
<h1>
${this.hass.localize("ui.panel.config.integrations.none")}
@@ -613,7 +613,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
justify-content: space-between;
}
.discovered {
--ha-card-border-color: var(--primary-color);
border: 1px solid var(--primary-color);
}
.discovered .header {
background: var(--primary-color);
@@ -622,7 +622,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
text-align: center;
}
.ignored {
--ha-card-border-color: var(--light-theme-disabled-color);
border: 1px solid var(--light-theme-disabled-color);
}
.ignored img {
filter: grayscale(1);
@@ -45,21 +45,6 @@ declare global {
}
}
const integrationsWithPanel = {
mqtt: {
buttonLocalizeKey: "ui.panel.config.mqtt.button",
path: "/config/mqtt",
},
zha: {
buttonLocalizeKey: "ui.panel.config.zha.button",
path: "/config/zha/dashboard",
},
zwave: {
buttonLocalizeKey: "ui.panel.config.zwave.button",
path: "/config/zwave",
},
};
@customElement("ha-integration-card")
export class HaIntegrationCard extends LitElement {
@property() public hass!: HomeAssistant;
@@ -91,7 +76,7 @@ export class HaIntegrationCard extends LitElement {
private _renderGroupedIntegration(): TemplateResult {
return html`
<ha-card outlined class="group">
<ha-card class="group">
<div class="group-header">
<img
src="https://brands.home-assistant.io/${this.domain}/icon.png"
@@ -127,7 +112,6 @@ export class HaIntegrationCard extends LitElement {
const entities = this._getEntities(item);
return html`
<ha-card
outlined
class="single integration"
.configEntry=${item}
.id=${item.entry_id}
@@ -195,24 +179,13 @@ export class HaIntegrationCard extends LitElement {
"ui.panel.config.integrations.config_entry.rename"
)}</mwc-button
>
${item.domain in integrationsWithPanel
? html`<a
href=${`${
integrationsWithPanel[item.domain].path
}?config_entry=${item.entry_id}`}
><mwc-button>
${this.hass.localize(
integrationsWithPanel[item.domain].buttonLocalizeKey
)}
</mwc-button></a
>`
: item.supports_options
${item.supports_options
? html`
<mwc-button @click=${this._showOptions}>
${this.hass.localize(
<mwc-button @click=${this._showOptions}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.options"
)}
</mwc-button>
)}</mwc-button
>
`
: ""}
</div>
@@ -1,143 +0,0 @@
import {
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
PropertyValues,
} from "lit-element";
import "../../../../../components/ha-code-editor";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { ZHADeviceZigbeeInfoDialogParams } from "./show-dialog-zha-device-zigbee-info";
import {
ZHADevice,
Cluster,
ZHAGroup,
fetchBindableDevices,
fetchGroups,
} from "../../../../../data/zha";
import { ZHAClusterSelectedParams } from "./types";
import "./zha-cluster-attributes";
import "./zha-cluster-commands";
import "./zha-clusters";
import "./zha-device-binding";
import "./zha-group-binding";
import { HASSDomEvent } from "../../../../../common/dom/fire_event";
import { sortZHADevices, sortZHAGroups } from "./functions";
@customElement("dialog-zha-cluster")
class DialogZHACluster extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _device?: ZHADevice;
@property() private _selectedCluster?: Cluster;
@property() private _bindableDevices: ZHADevice[] = [];
@property() private _groups: ZHAGroup[] = [];
public async showDialog(
params: ZHADeviceZigbeeInfoDialogParams
): Promise<void> {
this._device = params.device;
}
protected updated(changedProperties: PropertyValues): void {
super.update(changedProperties);
if (changedProperties.has("_device")) {
this._fetchData();
}
}
protected render(): TemplateResult {
if (!this._device) {
return html``;
}
return html`
<ha-dialog
open
hideActions
@closing="${this._close}"
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.zha.clusters.header")
)}
>
<zha-clusters
.hass=${this.hass}
.selectedDevice="${this._device}"
@zha-cluster-selected="${this._onClusterSelected}"
></zha-clusters>
${this._selectedCluster
? html`
<zha-cluster-attributes
.hass=${this.hass}
.selectedNode="${this._device}"
.selectedCluster="${this._selectedCluster}"
></zha-cluster-attributes>
<zha-cluster-commands
.hass=${this.hass}
.selectedNode="${this._device}"
.selectedCluster="${this._selectedCluster}"
></zha-cluster-commands>
`
: ""}
${this._bindableDevices.length > 0
? html`
<zha-device-binding-control
.hass=${this.hass}
.selectedDevice="${this._device}"
.bindableDevices="${this._bindableDevices}"
></zha-device-binding-control>
`
: ""}
${this._device && this._groups.length > 0
? html`
<zha-group-binding-control
.hass=${this.hass}
.selectedDevice="${this._device}"
.groups="${this._groups}"
></zha-group-binding-control>
`
: ""}
</ha-dialog>
`;
}
private _onClusterSelected(
selectedClusterEvent: HASSDomEvent<ZHAClusterSelectedParams>
): void {
this._selectedCluster = selectedClusterEvent.detail.cluster;
}
private _close(): void {
this._device = undefined;
}
private async _fetchData(): Promise<void> {
if (this._device && this.hass) {
this._bindableDevices =
this._device && this._device.device_type !== "Coordinator"
? (await fetchBindableDevices(this.hass, this._device.ieee)).sort(
sortZHADevices
)
: [];
this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups);
}
}
static get styles(): CSSResult {
return haStyleDialog;
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-zha-cluster": DialogZHACluster;
}
}
@@ -1,22 +0,0 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
import { ZHADevice } from "../../../../../data/zha";
export interface ZHAClusterDialogParams {
device: ZHADevice;
}
export const loadZHAClusterDialog = () =>
import(
/* webpackChunkName: "dialog-zha-device-zigbee-info" */ "./dialog-zha-cluster"
);
export const showZHAClusterDialog = (
element: HTMLElement,
params: ZHAClusterDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-zha-cluster",
dialogImport: loadZHAClusterDialog,
dialogParams: params,
});
};
@@ -1,132 +0,0 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@material/mwc-fab";
import {
css,
CSSResultArray,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
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 { mdiNetwork, mdiFolderMultipleOutline, mdiPlus } from "@mdi/js";
import "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { computeRTL } from "../../../../../common/util/compute_rtl";
export const zhaTabs: PageNavigation[] = [
{
translationKey: "ui.panel.config.zha.network.caption",
path: `/config/zha/dashboard`,
iconPath: mdiNetwork,
},
{
translationKey: "ui.panel.config.zha.groups.caption",
path: `/config/zha/groups`,
iconPath: mdiFolderMultipleOutline,
},
];
@customElement("zha-config-dashboard")
class ZHAConfigDashboard 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;
protected render(): TemplateResult {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${zhaTabs}
back-path="/config/integrations"
>
<ha-card header="Zigbee Network">
<div class="card-content">
Network info/settings for specific config entry
</div>
${this.configEntryId
? html`<div class="card-actions">
<a
href="${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`}"
>
<mwc-button>Devices</mwc-button>
</a>
<a
href="${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`}"
>
<mwc-button>Entities</mwc-button>
</a>
</div>`
: ""}
</ha-card>
<a href="/config/zha/add">
<mwc-fab
?is-wide=${this.isWide}
?narrow=${this.narrow}
title=${this.hass.localize("ui.panel.config.zha.add_device")}
?rtl=${computeRTL(this.hass)}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</a>
</hass-tabs-subpage>
`;
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
ha-card {
margin: auto;
margin-top: 16px;
max-width: 500px;
}
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[rtl][is-wide] {
bottom: 24px;
right: auto;
left: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-config-dashboard": ZHAConfigDashboard;
}
}
@@ -1,243 +0,0 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-listbox/paper-listbox";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeStateName } from "../../../../../common/entity/compute_state_name";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/entity/state-badge";
import "../../../../../components/ha-card";
import "../../../../../components/ha-service-description";
import { updateDeviceRegistryEntry } from "../../../../../data/device_registry";
import { ZHADevice } from "../../../../../data/zha";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-area-picker";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import {
subscribeEntityRegistry,
EntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../../../data/entity_registry";
import { createValidEntityId } from "../../../../../common/entity/valid_entity_id";
import memoizeOne from "memoize-one";
import { EntityRegistryStateEntry } from "../../../devices/ha-config-device-page";
import { compare } from "../../../../../common/string/compare";
import { getIeeeTail } from "./functions";
@customElement("zha-device-card")
class ZHADeviceCard extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public device?: ZHADevice;
@property({ type: Boolean }) public narrow?: boolean;
@property() private _entities: EntityRegistryEntry[] = [];
private _deviceEntities = memoizeOne(
(
deviceId: string,
entities: EntityRegistryEntry[]
): EntityRegistryStateEntry[] =>
entities
.filter((entity) => entity.device_id === deviceId)
.map((entity) => {
return { ...entity, stateName: this._computeEntityName(entity) };
})
.sort((ent1, ent2) =>
compare(
ent1.stateName || `zzz${ent1.entity_id}`,
ent2.stateName || `zzz${ent2.entity_id}`
)
)
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection, (entities) => {
this._entities = entities;
}),
];
}
protected render(): TemplateResult {
if (!this.hass || !this.device) {
return html``;
}
const entities = this._deviceEntities(
this.device.device_reg_id,
this._entities
);
return html`
<ha-card .header=${this.device.user_given_name || this.device.name}>
<div class="card-content">
<div class="info">
<div class="model">${this.device.model}</div>
<div class="manuf">
${this.hass.localize(
"ui.dialogs.zha_device_info.manuf",
"manufacturer",
this.device.manufacturer
)}
</div>
</div>
<div class="device-entities">
${entities.map(
(entity) => html`
<state-badge
@click="${this._openMoreInfo}"
.title=${entity.stateName!}
.stateObj="${this.hass!.states[entity.entity_id]}"
slot="item-icon"
></state-badge>
`
)}
</div>
<paper-input
type="string"
@change=${this._rename}
.value=${this.device.user_given_name || this.device.name}
.label=${this.hass.localize(
"ui.dialogs.zha_device_info.zha_device_card.device_name_placeholder"
)}
></paper-input>
<ha-area-picker
.hass=${this.hass}
.device=${this.device.device_reg_id}
@value-changed=${this._areaPicked}
></ha-area-picker>
</div>
</ha-card>
`;
}
private async _rename(event): Promise<void> {
if (!this.hass || !this.device) {
return;
}
const device = this.device;
const oldDeviceName = device.user_given_name || device.name;
const newDeviceName = event.target.value;
this.device.user_given_name = newDeviceName;
await updateDeviceRegistryEntry(this.hass, device.device_reg_id, {
name_by_user: newDeviceName,
});
if (!oldDeviceName || !newDeviceName || oldDeviceName === newDeviceName) {
return;
}
const entities = this._deviceEntities(device.device_reg_id, this._entities);
const oldDeviceEntityId = createValidEntityId(oldDeviceName);
const newDeviceEntityId = createValidEntityId(newDeviceName);
const ieeeTail = getIeeeTail(device.ieee);
const updateProms = entities.map((entity) => {
const name = entity.name || entity.stateName;
let newEntityId: string | null = null;
let newName: string | null = null;
if (name && name.includes(oldDeviceName)) {
newName = name.replace(` ${ieeeTail}`, "");
newName = newName.replace(oldDeviceName, newDeviceName);
newEntityId = entity.entity_id.replace(`_${ieeeTail}`, "");
newEntityId = newEntityId.replace(oldDeviceEntityId, newDeviceEntityId);
}
if (!newName && !newEntityId) {
return new Promise((resolve) => resolve());
}
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
name: newName || name,
disabled_by: entity.disabled_by,
new_entity_id: newEntityId || entity.entity_id,
});
});
await Promise.all(updateProms);
}
private _openMoreInfo(ev: MouseEvent): void {
fireEvent(this, "hass-more-info", {
entityId: (ev.currentTarget as any).stateObj.entity_id,
});
}
private _computeEntityName(entity: EntityRegistryEntry): string {
if (this.hass.states[entity.entity_id]) {
return computeStateName(this.hass.states[entity.entity_id]);
}
return entity.name;
}
private async _areaPicked(ev: CustomEvent) {
const picker = ev.currentTarget as any;
const area = ev.detail.value;
try {
await updateDeviceRegistryEntry(this.hass, this.device!.device_reg_id, {
area_id: area,
});
this.device!.area_id = area;
} catch (err) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error_saving_area",
"error",
err.message
),
});
picker.value = null;
}
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.device-entities {
display: flex;
flex-wrap: wrap;
padding: 4px;
justify-content: left;
min-height: 48px;
}
.device {
width: 30%;
}
.device .name {
font-weight: bold;
}
.device .manuf {
color: var(--secondary-text-color);
margin-bottom: 20px;
}
.extra-info {
margin-top: 8px;
}
state-badge {
cursor: pointer;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-device-card": ZHADeviceCard;
}
}
@@ -1,193 +0,0 @@
import "@material/mwc-button";
import "@material/mwc-fab";
import "../../../../../components/ha-icon-button";
import memoizeOne from "memoize-one";
import {
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
CSSResultArray,
css,
} from "lit-element";
import { HASSDomEvent } from "../../../../../common/dom/fire_event";
import { navigate } from "../../../../../common/navigate";
import {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../../../components/data-table/ha-data-table";
import { fetchGroups, ZHAGroup, ZHADevice } from "../../../../../data/zha";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../../../types";
import { sortZHAGroups, formatAsPaddedHex } from "./functions";
import { zhaTabs } from "./zha-config-dashboard";
import { computeRTL } from "../../../../../common/util/compute_rtl";
import { mdiPlus } from "@mdi/js";
import { haStyle } from "../../../../../resources/styles";
export interface GroupRowData extends ZHAGroup {
group?: GroupRowData;
id?: string;
}
@customElement("zha-groups-dashboard")
export class ZHAGroupsDashboard extends LitElement {
@property() public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public _groups: ZHAGroup[] = [];
private _firstUpdatedCalled = false;
public connectedCallback(): void {
super.connectedCallback();
if (this.hass && this._firstUpdatedCalled) {
this._fetchGroups();
}
}
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.hass) {
this._fetchGroups();
}
this._firstUpdatedCalled = true;
}
private _formattedGroups = memoizeOne((groups: ZHAGroup[]) => {
let outputGroups: GroupRowData[] = groups;
outputGroups = outputGroups.map((group) => {
return {
...group,
id: String(group.group_id),
};
});
return outputGroups;
});
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer =>
narrow
? {
name: {
title: "Group",
sortable: true,
filterable: true,
direction: "asc",
grows: true,
},
}
: {
name: {
title: this.hass.localize("ui.panel.config.zha.groups.groups"),
sortable: true,
filterable: true,
direction: "asc",
grows: true,
},
group_id: {
title: this.hass.localize("ui.panel.config.zha.groups.group_id"),
type: "numeric",
width: "15%",
template: (groupId: number) => {
return html` ${formatAsPaddedHex(groupId)} `;
},
sortable: true,
},
members: {
title: this.hass.localize("ui.panel.config.zha.groups.members"),
type: "numeric",
width: "15%",
template: (members: ZHADevice[]) => {
return html` ${members.length} `;
},
sortable: true,
},
}
);
protected render(): TemplateResult {
return html`
<hass-tabs-subpage-data-table
.tabs=${zhaTabs}
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.columns=${this._columns(this.narrow)}
.data=${this._formattedGroups(this._groups)}
@row-click=${this._handleRowClicked}
>
</hass-tabs-subpage-data-table>
<a href="/config/zha/group-add">
<mwc-fab
?is-wide=${this.isWide}
?narrow=${this.narrow}
title=${this.hass!.localize("ui.panel.config.zha.groups.add_group")}
?rtl=${computeRTL(this.hass)}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</a>
`;
}
private async _fetchGroups() {
this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups);
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const groupId = ev.detail.id;
navigate(this, `/config/zha/group/${groupId}`);
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[rtl][is-wide] {
bottom: 24px;
right: auto;
left: 24px;
}
a {
color: var(--primary-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-groups-dashboard": ZHAGroupsDashboard;
}
}
+2 -1
View File
@@ -53,7 +53,8 @@ class HaConfigScene extends HassRouterPage {
private _getScenes = memoizeOne((states: HassEntities): SceneEntity[] => {
return Object.values(states).filter(
(entity) => computeStateDomain(entity) === "scene"
(entity) =>
computeStateDomain(entity) === "scene" && !entity.attributes.hidden
) as SceneEntity[];
});
+2 -1
View File
@@ -53,7 +53,8 @@ class HaConfigScript extends HassRouterPage {
private _getScripts = memoizeOne((states: HassEntities): ScriptEntity[] => {
return Object.values(states).filter(
(entity) => computeStateDomain(entity) === "script"
(entity) =>
computeStateDomain(entity) === "script" && !entity.attributes.hidden
) as ScriptEntity[];
});
@@ -0,0 +1,267 @@
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
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 "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../styles/polymer-ha-style";
import "../ha-config-section";
/*
* @appliesMixin LocalizeMixin
*/
class HaConfigSectionServerControl extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style">
.validate-container {
@apply --layout-vertical;
@apply --layout-center-center;
height: 140px;
}
.validate-result {
color: var(--google-green-500);
font-weight: 500;
margin-bottom: 1em;
}
.config-invalid {
margin: 1em 0;
}
.config-invalid .text {
color: var(--google-red-500);
font-weight: 500;
}
.config-invalid mwc-button {
float: right;
}
.validate-log {
white-space: pre-wrap;
direction: ltr;
}
</style>
<ha-config-section is-wide="[[isWide]]">
<span slot="header"
>[[localize('ui.panel.config.server_control.caption')]]</span
>
<span slot="introduction"
>[[localize('ui.panel.config.server_control.description')]]</span
>
<template is="dom-if" if="[[showAdvanced]]">
<ha-card
header="[[localize('ui.panel.config.server_control.section.validation.heading')]]"
>
<div class="card-content">
[[localize('ui.panel.config.server_control.section.validation.introduction')]]
<template is="dom-if" if="[[!validateLog]]">
<div class="validate-container">
<template is="dom-if" if="[[!validating]]">
<template is="dom-if" if="[[isValid]]">
<div class="validate-result" id="result">
[[localize('ui.panel.config.server_control.section.validation.valid')]]
</div>
</template>
<mwc-button raised="" on-click="validateConfig">
[[localize('ui.panel.config.server_control.section.validation.check_config')]]
</mwc-button>
</template>
<template is="dom-if" if="[[validating]]">
<paper-spinner active=""></paper-spinner>
</template>
</div>
</template>
<template is="dom-if" if="[[validateLog]]">
<div class="config-invalid">
<span class="text">
[[localize('ui.panel.config.server_control.section.validation.invalid')]]
</span>
<mwc-button raised="" on-click="validateConfig">
[[localize('ui.panel.config.server_control.section.validation.check_config')]]
</mwc-button>
</div>
<div id="configLog" class="validate-log">[[validateLog]]</div>
</template>
</div>
</ha-card>
<ha-card
header="[[localize('ui.panel.config.server_control.section.reloading.heading')]]"
>
<div class="card-content">
[[localize('ui.panel.config.server_control.section.reloading.introduction')]]
</div>
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="homeassistant"
service="reload_core_config"
>[[localize('ui.panel.config.server_control.section.reloading.core')]]
</ha-call-service-button>
</div>
<template is="dom-if" if="[[groupLoaded(hass)]]">
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="group"
service="reload"
>[[localize('ui.panel.config.server_control.section.reloading.group')]]
</ha-call-service-button>
</div>
</template>
<template is="dom-if" if="[[automationLoaded(hass)]]">
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="automation"
service="reload"
>[[localize('ui.panel.config.server_control.section.reloading.automation')]]
</ha-call-service-button>
</div>
</template>
<template is="dom-if" if="[[scriptLoaded(hass)]]">
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="script"
service="reload"
>[[localize('ui.panel.config.server_control.section.reloading.script')]]
</ha-call-service-button>
</div>
</template>
<template is="dom-if" if="[[sceneLoaded(hass)]]">
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="scene"
service="reload"
>[[localize('ui.panel.config.server_control.section.reloading.scene')]]
</ha-call-service-button>
</div>
</template>
<template is="dom-if" if="[[personLoaded(hass)]]">
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="person"
service="reload"
>[[localize('ui.panel.config.server_control.section.reloading.person')]]
</ha-call-service-button>
</div>
</template>
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="zone"
service="reload"
>[[localize('ui.panel.config.server_control.section.reloading.zone')]]
</ha-call-service-button>
</div>
</ha-card>
</template>
<ha-card
header="[[localize('ui.panel.config.server_control.section.server_management.heading')]]"
>
<div class="card-content">
[[localize('ui.panel.config.server_control.section.server_management.introduction')]]
</div>
<div class="card-actions warning">
<ha-call-service-button
class="warning"
hass="[[hass]]"
domain="homeassistant"
service="restart"
confirmation="[[localize('ui.panel.config.server_control.section.server_management.confirm_restart')]]"
>[[localize('ui.panel.config.server_control.section.server_management.restart')]]
</ha-call-service-button>
<ha-call-service-button
class="warning"
hass="[[hass]]"
domain="homeassistant"
service="stop"
confirmation="[[localize('ui.panel.config.server_control.section.server_management.confirm_stop')]]"
>[[localize('ui.panel.config.server_control.section.server_management.stop')]]
</ha-call-service-button>
</div>
</ha-card>
</ha-config-section>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
isWide: {
type: Boolean,
value: false,
},
validating: {
type: Boolean,
value: false,
},
isValid: {
type: Boolean,
value: null,
},
validateLog: {
type: String,
value: "",
},
showAdvanced: Boolean,
};
}
groupLoaded(hass) {
return isComponentLoaded(hass, "group");
}
automationLoaded(hass) {
return isComponentLoaded(hass, "automation");
}
scriptLoaded(hass) {
return isComponentLoaded(hass, "script");
}
sceneLoaded(hass) {
return isComponentLoaded(hass, "scene");
}
personLoaded(hass) {
return isComponentLoaded(hass, "person");
}
validateConfig() {
this.validating = true;
this.validateLog = "";
this.isValid = null;
this.hass.callApi("POST", "config/core/check_config").then((result) => {
this.validating = false;
this.isValid = result.result === "valid";
if (!this.isValid) {
this.validateLog = result.errors;
}
});
}
}
customElements.define(
"ha-config-section-server-control",
HaConfigSectionServerControl
);
@@ -0,0 +1,72 @@
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../layouts/hass-tabs-subpage";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../styles/polymer-ha-style";
import { configSections } from "../ha-panel-config";
import "./ha-config-section-server-control";
/*
* @appliesMixin LocalizeMixin
*/
class HaConfigServerControl extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style">
.content {
padding-bottom: 32px;
}
.border {
margin: 32px auto 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
max-width: 1040px;
}
.narrow .border {
max-width: 640px;
}
</style>
<hass-tabs-subpage
hass="[[hass]]"
narrow="[[narrow]]"
route="[[route]]"
back-path="/config"
tabs="[[_computeTabs()]]"
show-advanced="[[showAdvanced]]"
>
<div class$="[[computeClasses(isWide)]]">
<ha-config-section-server-control
is-wide="[[isWide]]"
show-advanced="[[showAdvanced]]"
hass="[[hass]]"
></ha-config-section-server-control>
</div>
</hass-tabs-subpage>
`;
}
static get properties() {
return {
hass: Object,
isWide: Boolean,
narrow: Boolean,
route: Object,
showAdvanced: Boolean,
};
}
_computeTabs() {
return configSections.general;
}
computeClasses(isWide) {
return isWide ? "content" : "content narrow";
}
}
customElements.define("ha-config-server-control", HaConfigServerControl);
@@ -1,269 +0,0 @@
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "../../../layouts/hass-tabs-subpage";
import { configSections } from "../ha-panel-config";
import {
LitElement,
property,
customElement,
html,
css,
CSSResult,
TemplateResult,
} from "lit-element";
import { HomeAssistant, Route } from "../../../types";
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import "../ha-config-section";
import { haStyle } from "../../../resources/styles";
import { checkCoreConfig } from "../../../data/core";
const reloadableDomains = [
"group",
"automation",
"script",
"scene",
"person",
"zone",
"input_boolean",
"input_text",
"input_number",
"input_datetime",
"input_select",
];
@customElement("ha-config-server-control")
export class HaConfigServerControl extends LitElement {
@property() public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
@property() public route!: Route;
@property() public showAdvanced!: boolean;
@property() private _validating = false;
private _validateLog = "";
private _isValid: boolean | null = null;
protected render(): TemplateResult {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config"
.tabs=${configSections.general}
.showAdvanced=${this.showAdvanced}
>
<ha-config-section .isWide=${this.isWide}>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.server_control.caption"
)}</span
>
<span slot="introduction"
>${this.hass.localize(
"ui.panel.config.server_control.description"
)}</span
>
${this.showAdvanced
? html` <ha-card
header=${this.hass.localize(
"ui.panel.config.server_control.section.validation.heading"
)}
>
<div class="card-content">
${this.hass.localize(
"ui.panel.config.server_control.section.validation.introduction"
)}
${!this._validateLog
? html`
<div
class="validate-container layout vertical center-center"
>
${!this._validating
? html`
${this._isValid
? html` <div
class="validate-result"
id="result"
>
${this.hass.localize(
"ui.panel.config.server_control.section.validation.valid"
)}
</div>`
: ""}
<mwc-button
raised
@click=${this._validateConfig}
>
${this.hass.localize(
"ui.panel.config.server_control.section.validation.check_config"
)}
</mwc-button>
`
: html` <paper-spinner active></paper-spinner> `}
</div>
`
: html`
<div class="config-invalid">
<span class="text">
${this.hass.localize(
"ui.panel.config.server_control.section.validation.invalid"
)}
</span>
<mwc-button raised @click=${this._validateConfig}>
${this.hass.localize(
"ui.panel.config.server_control.section.validation.check_config"
)}
</mwc-button>
</div>
<div id="configLog" class="validate-log">
${this._validateLog}
</div>
`}
</div>
</ha-card>`
: ""}
<ha-card
header=${this.hass.localize(
"ui.panel.config.server_control.section.server_management.heading"
)}
>
<div class="card-content">
${this.hass.localize(
"ui.panel.config.server_control.section.server_management.introduction"
)}
</div>
<div class="card-actions warning">
<ha-call-service-button
class="warning"
.hass=${this.hass}
domain="homeassistant"
service="restart"
.confirmation=${this.hass.localize(
"ui.panel.config.server_control.section.server_management.confirm_restart"
)}
>${this.hass.localize(
"ui.panel.config.server_control.section.server_management.restart"
)}
</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>
${this.showAdvanced
? html`
<ha-card
header=${this.hass.localize(
"ui.panel.config.server_control.section.reloading.heading"
)}
>
<div class="card-content">
${this.hass.localize(
"ui.panel.config.server_control.section.reloading.introduction"
)}
</div>
<div class="card-actions">
<ha-call-service-button
.hass=${this.hass}
domain="homeassistant"
service="reload_core_config"
>${this.hass.localize(
"ui.panel.config.server_control.section.reloading.core"
)}
</ha-call-service-button>
</div>
${reloadableDomains.map((domain) =>
isComponentLoaded(this.hass, domain)
? html`<div class="card-actions">
<ha-call-service-button
.hass=${this.hass}
.domain=${domain}
service="reload"
>${this.hass.localize(
`ui.panel.config.server_control.section.reloading.${domain}`
)}
</ha-call-service-button>
</div>`
: ""
)}
</ha-card>
`
: ""}
</ha-config-section>
</hass-tabs-subpage>
`;
}
private async _validateConfig() {
this._validating = true;
this._validateLog = "";
this._isValid = null;
const configCheck = await checkCoreConfig(this.hass);
this._validating = false;
this._isValid = configCheck.result === "valid";
if (configCheck.errors) {
this._validateLog = configCheck.errors;
}
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.validate-container {
height: 140px;
}
.validate-result {
color: var(--google-green-500);
font-weight: 500;
margin-bottom: 1em;
}
.config-invalid {
margin: 1em 0;
}
.config-invalid .text {
color: var(--google-red-500);
font-weight: 500;
}
.config-invalid mwc-button {
float: right;
}
.validate-log {
white-space: pre-wrap;
direction: ltr;
}
`,
];
}
}
@@ -1,4 +1,4 @@
import { Cluster, ZHADevice, ZHAGroup } from "../../../../../data/zha";
import { Cluster, ZHADevice, ZHAGroup } from "../../../data/zha";
export const formatAsPaddedHex = (value: string | number): string => {
let hex = value;
@@ -8,9 +8,6 @@ export const formatAsPaddedHex = (value: string | number): string => {
return "0x" + hex.toString(16).padStart(4, "0");
};
export const getIeeeTail = (ieee: string) =>
ieee.split(":").slice(-4).reverse().join("");
export const sortZHADevices = (a: ZHADevice, b: ZHADevice): number => {
const nameA = a.user_given_name ? a.user_given_name : a.name;
const nameb = b.user_given_name ? b.user_given_name : b.name;
@@ -1,4 +1,4 @@
import { Cluster, ZHADevice } from "../../../../../data/zha";
import { Cluster, ZHADevice } from "../../../data/zha";
export interface PickerTarget extends EventTarget {
selected: number;
@@ -1,5 +1,5 @@
import "@material/mwc-button";
import "../../../../../components/ha-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-spinner/paper-spinner";
import {
css,
@@ -9,24 +9,19 @@ import {
LitElement,
property,
TemplateResult,
PropertyValues,
} from "lit-element";
import "../../../../../components/ha-service-description";
import "@polymer/paper-input/paper-textarea";
import { ZHADevice } from "../../../../../data/zha";
import "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant, Route } from "../../../../../types";
import "../../../components/ha-service-description";
import "../../../components/ha-textarea";
import { ZHADevice } from "../../../data/zha";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import "./zha-device-card";
import { zhaTabs } from "./zha-config-dashboard";
import { IronAutogrowTextareaElement } from "@polymer/iron-autogrow-textarea";
@customElement("zha-add-devices-page")
class ZHAAddDevicesPage extends LitElement {
@property() public hass!: HomeAssistant;
@property() public narrow?: boolean;
@property() public isWide?: boolean;
@property() public route?: Route;
@@ -41,8 +36,6 @@ class ZHAAddDevicesPage extends LitElement {
@property() private _showHelp = false;
@property() private _showLogs = false;
private _ieeeAddress?: string;
private _addDevicesTimeoutHandle: any = undefined;
@@ -67,63 +60,58 @@ class ZHAAddDevicesPage extends LitElement {
this._formattedEvents = "";
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (
changedProps.has("hass") &&
!this._active &&
!changedProps.get("hass")
) {
this._subscribe();
}
}
protected render(): TemplateResult {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${zhaTabs}
<hass-subpage
header="${this.hass!.localize(
"ui.panel.config.zha.add_device_page.header"
)}"
>
<mwc-button slot="toolbar-icon" @click=${this._toggleLogs}
>${this._showLogs ? "Hide logs" : "Show logs"}</mwc-button
>
<div class="searching">
${this._active
? html`
<h1>
${this._active
? html`
<h2>
<paper-spinner
?active="${this._active}"
alt="Searching"
></paper-spinner>
${this.hass!.localize(
"ui.panel.config.zha.add_device_page.spinner"
)}
</h2>
`
: html`
<div class="card-actions">
<mwc-button @click=${this._subscribe} class="search-button">
${this.hass!.localize(
"ui.panel.config.zha.add_device_page.spinner"
"ui.panel.config.zha.add_device_page.search_again"
)}
</h1>
<paper-spinner active alt="Searching"></paper-spinner>
`
: html`
<div>
<mwc-button @click=${this._subscribe} class="search-button">
${this.hass!.localize(
"ui.panel.config.zha.add_device_page.search_again"
)}
</mwc-button>
</div>
`}
</div>
</mwc-button>
<ha-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
></ha-icon-button>
${this._showHelp
? html`
<ha-service-description
.hass=${this.hass}
domain="zha"
service="permit"
class="help-text"
></ha-service-description>
`
: ""}
</div>
`}
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<div class="content-header"></div>
<div class="content">
${this._discoveredDevices.length < 1
? html`
<div class="discovery-text">
<h4>
${this.hass!.localize(
"ui.panel.config.zha.add_device_page.pairing_mode"
)}
</h4>
<h4>
${this.hass!.localize(
this._active
? "ui.panel.config.zha.add_device_page.discovered_text"
: "ui.panel.config.zha.add_device_page.no_devices_found"
"ui.panel.config.zha.add_device_page.discovery_text"
)}
</h4>
</div>
@@ -135,38 +123,27 @@ class ZHAAddDevicesPage extends LitElement {
class="card"
.hass=${this.hass}
.device=${device}
.narrow=${this.narrow}
.narrow=${!this.isWide}
.showHelp=${this._showHelp}
.showActions=${!this._active}
.showEntityDetail=${false}
></zha-device-card>
`
)}
`}
</div>
${this._showLogs
? html`<paper-textarea
readonly
max-rows="10"
class="log"
value="${this._formattedEvents}"
>
</paper-textarea>`
: ""}
</hass-tabs-subpage>
<ha-textarea class="events" value="${this._formattedEvents}">
</ha-textarea>
</hass-subpage>
`;
}
private _toggleLogs() {
this._showLogs = !this._showLogs;
}
private _handleMessage(message: any): void {
if (message.type === "log_output") {
this._formattedEvents += message.log_entry.message + "\n";
if (this.shadowRoot) {
const paperTextArea = this.shadowRoot.querySelector("paper-textarea");
if (paperTextArea) {
const textArea = (paperTextArea.inputElement as IronAutogrowTextareaElement)
.textarea;
const textArea = this.shadowRoot.querySelector("ha-textarea");
if (textArea) {
textArea.scrollTop = textArea.scrollHeight;
}
}
@@ -188,58 +165,69 @@ class ZHAAddDevicesPage extends LitElement {
}
private _subscribe(): void {
if (!this.hass) {
return;
}
this._active = true;
const data: any = { type: "zha/devices/permit" };
if (this._ieeeAddress) {
data.ieee = this._ieeeAddress;
}
this._subscribed = this.hass.connection.subscribeMessage(
this._subscribed = this.hass!.connection.subscribeMessage(
(message) => this._handleMessage(message),
data
);
this._active = true;
this._addDevicesTimeoutHandle = setTimeout(
() => this._unsubscribe(),
120000
);
}
private _onHelpTap(): void {
this._showHelp = !this._showHelp;
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.discovery-text {
width: 100%;
padding: 16px;
display: flex;
flex-direction: column;
align-items: center;
.discovery-text,
.content-header {
margin: 16px;
}
.content {
border-top: 1px solid var(--light-primary-color);
min-height: 500px;
display: flex;
flex-wrap: wrap;
padding: 4px;
justify-content: center;
justify-content: left;
overflow: scroll;
}
.error {
color: var(--google-red-500);
}
paper-spinner {
padding: 20px;
display: none;
margin-right: 20px;
margin-left: 16px;
}
.searching {
margin-top: 20px;
display: flex;
flex-direction: column;
align-items: center;
paper-spinner[active] {
display: block;
float: left;
margin-right: 20px;
margin-left: 16px;
}
.card {
margin: 8px;
margin-left: 16px;
margin-right: 16px;
margin-bottom: 0px;
margin-top: 10px;
}
.log {
padding: 16px;
.events {
margin: 16px;
border-top: 1px solid var(--light-primary-color);
padding-top: 16px;
min-height: 200px;
max-height: 200px;
overflow: scroll;
}
.toggle-help-icon {
position: absolute;
@@ -12,20 +12,20 @@ import {
PropertyValues,
query,
} from "lit-element";
import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
import { navigate } from "../../../../../common/navigate";
import type { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import type { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
import {
addGroup,
fetchGroupableDevices,
ZHAGroup,
ZHADeviceEndpoint,
} from "../../../../../data/zha";
import "../../../../../layouts/hass-error-screen";
import "../../../../../layouts/hass-subpage";
import type { PolymerChangedEvent } from "../../../../../polymer-types";
import type { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
} from "../../../data/zha";
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-subpage";
import type { PolymerChangedEvent } from "../../../polymer-types";
import type { HomeAssistant } from "../../../types";
import "../ha-config-section";
import "./zha-device-endpoint-data-table";
import type { ZHADeviceEndpointDataTable } from "./zha-device-endpoint-data-table";
@@ -1,6 +1,6 @@
import "@material/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "../../../../../components/ha-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
@@ -13,9 +13,9 @@ import {
PropertyValues,
TemplateResult,
} from "lit-element";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-service-description";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import "../../../components/ha-service-description";
import {
Attribute,
Cluster,
@@ -23,10 +23,10 @@ import {
ReadAttributeServiceData,
readAttributeValue,
ZHADevice,
} from "../../../../../data/zha";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
} from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { formatAsPaddedHex } from "./functions";
import {
ChangeEvent,
@@ -1,5 +1,5 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "../../../../../components/ha-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
@@ -12,18 +12,18 @@ import {
PropertyValues,
TemplateResult,
} from "lit-element";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-service-description";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import "../../../components/ha-service-description";
import {
Cluster,
Command,
fetchCommandsForCluster,
ZHADevice,
} from "../../../../../data/zha";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
} from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { formatAsPaddedHex } from "./functions";
import {
ChangeEvent,
@@ -7,14 +7,14 @@ import {
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import "../../../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
HaDataTable,
} from "../../../../../components/data-table/ha-data-table";
import "../../../../../components/entity/ha-state-icon";
import type { Cluster } from "../../../../../data/zha";
import type { HomeAssistant } from "../../../../../types";
} from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-state-icon";
import type { Cluster } from "../../../data/zha";
import type { HomeAssistant } from "../../../types";
import { formatAsPaddedHex } from "./functions";
export interface ClusterRowData extends Cluster {
@@ -1,5 +1,5 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "../../../../../components/ha-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
@@ -11,18 +11,14 @@ import {
PropertyValues,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-service-description";
import {
Cluster,
fetchClustersForZhaNode,
ZHADevice,
} from "../../../../../data/zha";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import "../../../components/ha-service-description";
import { Cluster, fetchClustersForZhaNode, ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { computeClusterKey } from "./functions";
import { ItemSelectedEvent } from "./types";
@@ -64,6 +60,9 @@ export class ZHAClusters extends LitElement {
return html`
<ha-config-section .isWide="${this.isWide}">
<div class="header" slot="header">
<span>
${this.hass!.localize("ui.panel.config.zha.clusters.header")}
</span>
<ha-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
@@ -2,9 +2,8 @@ import { customElement, property } from "lit-element";
import {
HassRouterPage,
RouterOptions,
} from "../../../../../layouts/hass-router-page";
import { HomeAssistant } from "../../../../../types";
import { navigate } from "../../../../../common/navigate";
} from "../../../layouts/hass-router-page";
import { HomeAssistant } from "../../../types";
@customElement("zha-config-dashboard-router")
class ZHAConfigDashboardRouter extends HassRouterPage {
@@ -14,10 +13,6 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
@property() public narrow!: boolean;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
@@ -29,6 +24,13 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
/* webpackChunkName: "zha-config-dashboard" */ "./zha-config-dashboard"
),
},
device: {
tag: "zha-device-page",
load: () =>
import(
/* webpackChunkName: "zha-devices-page" */ "./zha-device-page"
),
},
add: {
tag: "zha-add-devices-page",
load: () =>
@@ -63,24 +65,11 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
if (this._currentPage === "group") {
el.groupId = this.routeTail.path.substr(1);
} else if (this._currentPage === "device") {
el.ieee = 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
);
}
}
}
@@ -0,0 +1,188 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import {
css,
CSSResultArray,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { navigate } from "../../../common/navigate";
import "../../../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
RowClickedEvent,
DataTableRowData,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import { fetchDevices } from "../../../data/zha";
import type { ZHADevice } from "../../../data/zha";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
import "../ha-config-section";
import { formatAsPaddedHex, sortZHADevices } from "./functions";
export interface DeviceRowData extends DataTableRowData {
device?: DeviceRowData;
}
@customElement("zha-config-dashboard")
class ZHAConfigDashboard 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() private _devices: ZHADevice[] = [];
private pages: string[] = ["add", "groups"];
private _firstUpdatedCalled = false;
private _memoizeDevices = memoizeOne((devices: ZHADevice[]) => {
let outputDevices: DeviceRowData[] = devices;
outputDevices = outputDevices.map((device) => {
return {
...device,
name: device.user_given_name ? device.user_given_name : device.name,
nwk: formatAsPaddedHex(device.nwk),
};
});
return outputDevices;
});
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer =>
narrow
? {
name: {
title: "Devices",
sortable: true,
filterable: true,
direction: "asc",
grows: true,
},
}
: {
name: {
title: "Name",
sortable: true,
filterable: true,
direction: "asc",
grows: true,
},
nwk: {
title: "Nwk",
sortable: true,
filterable: true,
width: "15%",
},
ieee: {
title: "IEEE",
sortable: true,
filterable: true,
width: "30%",
},
}
);
public connectedCallback(): void {
super.connectedCallback();
if (this.hass && this._firstUpdatedCalled) {
this._fetchDevices();
}
}
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.hass) {
this._fetchDevices();
}
this._firstUpdatedCalled = true;
}
protected render(): TemplateResult {
return html`
<hass-subpage .header=${this.hass.localize("component.zha.title")}>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize("ui.panel.config.zha.header")}
</div>
<div slot="introduction">
${this.hass.localize("ui.panel.config.zha.introduction")}
</div>
<ha-card>
${this.pages.map((page) => {
return html`
<a href=${`/config/zha/${page}`}>
<paper-item>
<paper-item-body two-line="">
${this.hass.localize(
`ui.panel.config.zha.${page}.caption`
)}
<div secondary>
${this.hass.localize(
`ui.panel.config.zha.${page}.description`
)}
</div>
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
`;
})}
</ha-card>
<ha-card>
<ha-data-table
.columns=${this._columns(this.narrow)}
.data=${this._memoizeDevices(this._devices)}
@row-click=${this._handleDeviceClicked}
.id=${"ieee"}
auto-height
></ha-data-table>
</ha-card>
</ha-config-section>
</hass-subpage>
`;
}
private async _fetchDevices() {
this._devices = (await fetchDevices(this.hass!)).sort(sortZHADevices);
}
private async _handleDeviceClicked(ev: CustomEvent) {
const deviceId = (ev.detail as RowClickedEvent).id;
navigate(this, `/config/zha/device/${deviceId}`);
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
a {
text-decoration: none;
color: var(--primary-text-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-config-dashboard": ZHAConfigDashboard;
}
}
@@ -1,6 +1,6 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "../../../../../components/ha-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
@@ -13,13 +13,13 @@ import {
PropertyValues,
TemplateResult,
} from "lit-element";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-service-description";
import { bindDevices, unbindDevices, ZHADevice } from "../../../../../data/zha";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import "../../../components/ha-service-description";
import { bindDevices, unbindDevices, ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { ItemSelectedEvent } from "./types";
@customElement("zha-device-binding-control")
+566
View File
@@ -0,0 +1,566 @@
import "@material/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-listbox/paper-listbox";
import { HassEvent, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/entity/state-badge";
import "../../../components/ha-card";
import "../../../components/ha-service-description";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../../data/area_registry";
import {
DeviceRegistryEntryMutableParams,
updateDeviceRegistryEntry,
} from "../../../data/device_registry";
import {
reconfigureNode,
ZHADevice,
ZHAEntityReference,
} from "../../../data/zha";
import { showZHADeviceZigbeeInfoDialog } from "../../../dialogs/zha-device-zigbee-signature-dialog/show-dialog-zha-device-zigbee-info";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { addEntitiesToLovelaceView } from "../../lovelace/editor/add-entities-to-view";
import { formatAsPaddedHex } from "./functions";
import { ItemSelectedEvent, NodeServiceData } from "./types";
declare global {
// for fire event
interface HASSDomEvents {
"zha-device-removed": {
device?: ZHADevice;
};
}
}
@customElement("zha-device-card")
class ZHADeviceCard extends LitElement {
@property() public hass!: HomeAssistant;
@property() public device?: ZHADevice;
@property({ type: Boolean }) public narrow?: boolean;
@property({ type: Boolean }) public showHelp?: boolean = false;
@property({ type: Boolean }) public showActions?: boolean = true;
@property({ type: Boolean }) public showName?: boolean = true;
@property({ type: Boolean }) public showEntityDetail?: boolean = true;
@property({ type: Boolean }) public showModelInfo?: boolean = true;
@property({ type: Boolean }) public showEditableInfo?: boolean = true;
@property() private _serviceData?: NodeServiceData;
@property() private _areas: AreaRegistryEntry[] = [];
@property() private _selectedAreaIndex = -1;
@property() private _userGivenName?: string;
private _unsubAreas?: UnsubscribeFunc;
private _unsubEntities?: UnsubscribeFunc;
public disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubAreas) {
this._unsubAreas();
}
if (this._unsubEntities) {
this._unsubEntities();
}
}
public connectedCallback() {
super.connectedCallback();
this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
if (this.device) {
this._selectedAreaIndex =
this._areas.findIndex(
(area) => area.area_id === this.device!.area_id
) + 1; // account for the no area selected index
}
});
this.hass.connection
.subscribeEvents((event: HassEvent) => {
if (this.device) {
this.device!.entities.forEach((deviceEntity) => {
if (event.data.old_entity_id === deviceEntity.entity_id) {
deviceEntity.entity_id = event.data.entity_id;
}
});
}
}, "entity_registry_updated")
.then((unsub) => {
this._unsubEntities = unsub;
});
}
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.addEventListener("hass-service-called", (ev) =>
this.serviceCalled(ev)
);
}
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("device")) {
if (!this._areas || !this.device || !this.device.area_id) {
this._selectedAreaIndex = 0;
} else {
this._selectedAreaIndex =
this._areas.findIndex(
(area) => area.area_id === this.device!.area_id
) + 1;
}
this._userGivenName = this.device!.user_given_name;
this._serviceData = {
ieee_address: this.device!.ieee,
};
}
super.update(changedProperties);
}
protected serviceCalled(ev): void {
// Check if this is for us
if (ev.detail.success && ev.detail.service === "remove") {
fireEvent(this, "zha-device-removed", {
device: this.device,
});
}
}
protected render(): TemplateResult {
return html`
<ha-card header="${this.showName ? this.device!.name : ""}">
${
this.showModelInfo
? html`
<div class="info">
<div class="model">${this.device!.model}</div>
<div class="manuf">
${this.hass!.localize(
"ui.dialogs.zha_device_info.manuf",
"manufacturer",
this.device!.manufacturer
)}
</div>
</div>
`
: ""
}
<div class="card-content">
<dl>
<dt>IEEE:</dt>
<dd class="zha-info">${this.device!.ieee}</dd>
<dt>Nwk:</dt>
<dd class="zha-info">${formatAsPaddedHex(this.device!.nwk)}</dd>
<dt>Device Type:</dt>
<dd class="zha-info">${this.device!.device_type}</dd>
<dt>LQI:</dt>
<dd class="zha-info">${
this.device!.lqi ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")
}</dd>
<dt>RSSI:</dt>
<dd class="zha-info">${
this.device!.rssi ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")
}</dd>
<dt>${this.hass!.localize(
"ui.dialogs.zha_device_info.last_seen"
)}:</dt>
<dd class="zha-info">${
this.device!.last_seen ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")
}</dd>
<dt>${this.hass!.localize(
"ui.dialogs.zha_device_info.power_source"
)}:</dt>
<dd class="zha-info">${
this.device!.power_source ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")
}</dd>
${
this.device!.quirk_applied
? html`
<dt>
${this.hass!.localize(
"ui.dialogs.zha_device_info.quirk"
)}:
</dt>
<dd class="zha-info">${this.device!.quirk_class}</dd>
`
: ""
}
</dl>
</div>
<div class="device-entities">
${this.device!.entities.map(
(entity) => html`
<paper-icon-item
@click="${this._openMoreInfo}"
.entity="${entity}"
>
<state-badge
.stateObj="${this.hass!.states[entity.entity_id]}"
slot="item-icon"
></state-badge>
${this.showEntityDetail
? html`
<paper-item-body>
<div class="name">
${this._computeEntityName(entity)}
</div>
<div class="secondary entity-id">
${entity.entity_id}
</div>
</paper-item-body>
`
: ""}
</paper-icon-item>
`
)}
</div>
${
this.device!.entities && this.device!.entities.length > 0
? html`
<div class="card-actions">
<mwc-button @click=${this._addToLovelaceView}>
${this.hass.localize(
"ui.panel.config.devices.entities.add_entities_lovelace"
)}
</mwc-button>
</div>
`
: ""
}
${
this.showEditableInfo
? html`
<div class="editable">
<paper-input
type="string"
@change="${this._saveCustomName}"
.value="${this._userGivenName || ""}"
.placeholder="${this.hass!.localize(
"ui.dialogs.zha_device_info.zha_device_card.device_name_placeholder"
)}"
></paper-input>
</div>
<div class="node-picker">
<paper-dropdown-menu
.label="${this.hass!.localize(
"ui.dialogs.zha_device_info.zha_device_card.area_picker_label"
)}"
class="menu"
>
<paper-listbox
slot="dropdown-content"
.selected="${this._selectedAreaIndex}"
@iron-select="${this._selectedAreaChanged}"
>
<paper-item>
${this.hass!.localize(
"ui.dialogs.zha_device_info.no_area"
)}
</paper-item>
${this._areas.map(
(entry) => html`
<paper-item>${entry.name}</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
</div>
`
: ""
}
${
this.showActions
? html`
<div class="card-actions">
${this.device!.device_type !== "Coordinator"
? html`
<mwc-button @click=${this._onReconfigureNodeClick}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.reconfigure"
)}
</mwc-button>
${this.showHelp
? html`
<div class="help-text">
${this.hass!.localize(
"ui.dialogs.zha_device_info.services.reconfigure"
)}
</div>
`
: ""}
<ha-call-service-button
.hass=${this.hass}
domain="zha"
service="remove"
.confirmation=${this.hass!.localize(
"ui.dialogs.zha_device_info.confirmations.remove"
)}
.serviceData=${this._serviceData}
>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.remove"
)}
</ha-call-service-button>
${this.showHelp
? html`
<div class="help-text">
${this.hass!.localize(
"ui.dialogs.zha_device_info.services.remove"
)}
</div>
`
: ""}
`
: ""}
${this.device!.power_source === "Mains" &&
(this.device!.device_type === "Router" ||
this.device!.device_type === "Coordinator")
? html`
<mwc-button @click=${this._onAddDevicesClick}>
${this.hass!.localize(
"ui.panel.config.zha.common.add_devices"
)}
</mwc-button>
${this.showHelp
? html`
<ha-service-description
.hass=${this.hass}
domain="zha"
service="permit"
class="help-text2"
></ha-service-description>
`
: ""}
`
: ""}
${this.device!.device_type !== "Coordinator"
? html`
<mwc-button @click=${this._handleZigbeeInfoClicked}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.zigbee_information"
)}
</mwc-button>
${this.showHelp
? html`
<div class="help-text">
${this.hass!.localize(
"ui.dialogs.zha_device_info.services.zigbee_information"
)}
</div>
`
: ""}
`
: ""}
</div>
`
: ""
}
</div>
</ha-card>
`;
}
private async _onReconfigureNodeClick(): Promise<void> {
if (this.hass) {
await reconfigureNode(this.hass, this.device!.ieee);
}
}
private _computeEntityName(entity: ZHAEntityReference): string {
if (this.hass.states[entity.entity_id]) {
return computeStateName(this.hass.states[entity.entity_id]);
}
return entity.name;
}
private async _saveCustomName(event): Promise<void> {
if (this.hass) {
const values: DeviceRegistryEntryMutableParams = {
name_by_user: event.target.value,
area_id: this.device!.area_id ? this.device!.area_id : undefined,
};
await updateDeviceRegistryEntry(
this.hass,
this.device!.device_reg_id,
values
);
this.device!.user_given_name = event.target.value;
}
}
private _openMoreInfo(ev: MouseEvent): void {
fireEvent(this, "hass-more-info", {
entityId: (ev.currentTarget as any).entity.entity_id,
});
}
private async _selectedAreaChanged(event: ItemSelectedEvent) {
if (!this.device || !this._areas) {
return;
}
this._selectedAreaIndex = event!.target!.selected;
const area = this._areas[this._selectedAreaIndex - 1]; // account for No Area
if (
(!area && !this.device.area_id) ||
(area && area.area_id === this.device.area_id)
) {
return;
}
const newAreaId = area ? area.area_id : undefined;
await updateDeviceRegistryEntry(this.hass!, this.device.device_reg_id, {
area_id: newAreaId,
name_by_user: this.device!.user_given_name,
});
this.device!.area_id = newAreaId;
}
private _onAddDevicesClick() {
navigate(this, "/config/zha/add/" + this.device!.ieee);
}
private async _handleZigbeeInfoClicked() {
showZHADeviceZigbeeInfoDialog(this, { device: this.device! });
}
private _addToLovelaceView(): void {
addEntitiesToLovelaceView(
this,
this.hass,
this.device!.entities.map((entity) => entity.entity_id)
);
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
:host(:not([narrow])) .device-entities {
max-height: 225px;
overflow-y: auto;
display: flex;
flex-wrap: wrap;
padding: 4px;
justify-content: left;
}
ha-card {
flex: 1 0 100%;
padding-bottom: 10px;
min-width: 300px;
}
.device {
width: 30%;
}
.device .name {
font-weight: bold;
}
.device .manuf {
color: var(--secondary-text-color);
margin-bottom: 20px;
}
.extra-info {
margin-top: 8px;
}
.manuf,
.zha-info,
.name {
text-overflow: ellipsis;
}
.entity-id {
text-overflow: ellipsis;
color: var(--secondary-text-color);
}
.info {
margin-left: 16px;
}
dl {
display: flex;
flex-wrap: wrap;
width: 100%;
}
dl dt {
display: inline-block;
width: 30%;
padding-left: 12px;
float: left;
text-align: left;
}
dl dd {
width: 60%;
overflow-wrap: break-word;
margin-inline-start: 20px;
}
paper-icon-item {
overflow-x: hidden;
cursor: pointer;
padding-top: 4px;
padding-bottom: 4px;
}
.editable {
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
}
.help-text {
color: grey;
padding: 16px;
}
.menu {
width: 100%;
}
.node-picker {
align-items: center;
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
}
.buttons .icon {
margin-right: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-device-card": ZHADeviceCard;
}
}
@@ -9,18 +9,16 @@ import {
CSSResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import "../../../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
HaDataTable,
DataTableRowData,
} from "../../../../../components/data-table/ha-data-table";
import "../../../../../components/entity/ha-state-icon";
import type {
ZHADeviceEndpoint,
ZHAEntityReference,
} from "../../../../../data/zha";
import type { HomeAssistant } from "../../../../../types";
} from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-state-icon";
import type { ZHADeviceEndpoint, ZHAEntityReference } from "../../../data/zha";
import { showZHADeviceInfoDialog } from "../../../dialogs/zha-device-info-dialog/show-dialog-zha-device-info";
import type { HomeAssistant } from "../../../types";
export interface DeviceEndpointRowData extends DataTableRowData {
id: string;
@@ -57,7 +55,6 @@ export class ZHADeviceEndpointDataTable extends LitElement {
ieee: deviceEndpoint.device.ieee,
endpoint_id: deviceEndpoint.endpoint_id,
entities: deviceEndpoint.entities,
dev_id: deviceEndpoint.device.device_reg_id,
});
});
@@ -75,10 +72,14 @@ export class ZHADeviceEndpointDataTable extends LitElement {
filterable: true,
direction: "asc",
grows: true,
template: (name, device: any) => html`
<a href="${`/config/devices/device/${device.dev_id}`}">
template: (name) => html`
<div
class="mdc-data-table__cell table-cell-text"
@click=${this._handleClicked}
style="cursor: pointer;"
>
${name}
</a>
</div>
`,
},
endpoint_id: {
@@ -94,10 +95,14 @@ export class ZHADeviceEndpointDataTable extends LitElement {
filterable: true,
direction: "asc",
grows: true,
template: (name, device: any) => html`
<a href="${`/config/devices/device/${device.dev_id}`}">
template: (name) => html`
<div
class="mdc-data-table__cell table-cell-text"
@click=${this._handleClicked}
style="cursor: pointer;"
>
${name}
</a>
</div>
`,
},
endpoint_id: {
@@ -151,6 +156,14 @@ export class ZHADeviceEndpointDataTable extends LitElement {
`;
}
private async _handleClicked(ev: CustomEvent) {
const rowId = ((ev.target as HTMLElement).closest(
".mdc-data-table__row"
) as any).rowId;
const ieee = rowId.substring(0, rowId.indexOf("_"));
showZHADeviceInfoDialog(this, { ieee });
}
static get styles(): CSSResult[] {
return [
css`
+157
View File
@@ -0,0 +1,157 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import {
Cluster,
fetchBindableDevices,
fetchGroups,
fetchZHADevice,
ZHADevice,
ZHAGroup,
} from "../../../data/zha";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { sortZHADevices, sortZHAGroups } from "./functions";
import { ZHAClusterSelectedParams } from "./types";
import "./zha-cluster-attributes";
import "./zha-cluster-commands";
import "./zha-clusters";
import "./zha-device-binding";
import "./zha-group-binding";
import "./zha-node";
@customElement("zha-device-page")
export class ZHADevicePage extends LitElement {
@property() public hass?: HomeAssistant;
@property() public isWide?: boolean;
@property() public ieee?: string;
@property() public device?: ZHADevice;
@property() public narrow?: boolean;
@property() private _selectedCluster?: Cluster;
@property() private _bindableDevices: ZHADevice[] = [];
@property() private _groups: ZHAGroup[] = [];
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("ieee")) {
this._fetchData();
}
super.update(changedProperties);
}
protected render(): TemplateResult {
return html`
<hass-subpage
.header=${this.hass!.localize("ui.panel.config.zha.devices.header")}
.back=${!this.isWide}
>
<zha-node
.isWide="${this.isWide}"
.hass=${this.hass}
.device=${this.device}
></zha-node>
${this.device && this.device.device_type !== "Coordinator"
? html`
<zha-clusters
.hass=${this.hass}
.isWide="${this.isWide}"
.selectedDevice="${this.device}"
@zha-cluster-selected="${this._onClusterSelected}"
></zha-clusters>
${this._selectedCluster
? html`
<zha-cluster-attributes
.isWide="${this.isWide}"
.hass=${this.hass}
.selectedNode="${this.device}"
.selectedCluster="${this._selectedCluster}"
></zha-cluster-attributes>
<zha-cluster-commands
.isWide="${this.isWide}"
.hass=${this.hass}
.selectedNode="${this.device}"
.selectedCluster="${this._selectedCluster}"
></zha-cluster-commands>
`
: ""}
${this._bindableDevices.length > 0
? html`
<zha-device-binding-control
.isWide="${this.isWide}"
.hass=${this.hass}
.selectedDevice="${this.device}"
.bindableDevices="${this._bindableDevices}"
></zha-device-binding-control>
`
: ""}
${this.device && this._groups.length > 0
? html`
<zha-group-binding-control
.isWide="${this.isWide}"
.narrow="${this.narrow}"
.hass=${this.hass}
.selectedDevice="${this.device}"
.groups="${this._groups}"
></zha-group-binding-control>
`
: ""}
`
: ""}
<div class="spacer"></div>
</hass-subpage>
`;
}
private _onClusterSelected(
selectedClusterEvent: HASSDomEvent<ZHAClusterSelectedParams>
): void {
this._selectedCluster = selectedClusterEvent.detail.cluster;
}
private async _fetchData(): Promise<void> {
if (this.ieee && this.hass) {
this.device = await fetchZHADevice(this.hass, this.ieee);
this._bindableDevices =
this.device && this.device.device_type !== "Coordinator"
? (await fetchBindableDevices(this.hass, this.ieee)).sort(
sortZHADevices
)
: [];
this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups);
}
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.spacer {
height: 50px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-device-page": ZHADevicePage;
}
}
@@ -1,6 +1,6 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "../../../../../components/ha-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
@@ -14,11 +14,11 @@ import {
query,
TemplateResult,
} from "lit-element";
import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/buttons/ha-call-service-button";
import { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table";
import "../../../../../components/ha-card";
import "../../../../../components/ha-service-description";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import "../../../components/buttons/ha-call-service-button";
import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
import "../../../components/ha-card";
import "../../../components/ha-service-description";
import {
bindDeviceToGroup,
Cluster,
@@ -26,10 +26,10 @@ import {
unbindDeviceFromGroup,
ZHADevice,
ZHAGroup,
} from "../../../../../data/zha";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
} from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { ItemSelectedEvent } from "./types";
import "./zha-clusters-data-table";
import type { ZHAClustersDataTable } from "./zha-clusters-data-table";
@@ -1,5 +1,5 @@
import "@material/mwc-button";
import "../../../../../components/ha-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-spinner/paper-spinner";
import {
css,
@@ -11,9 +11,9 @@ import {
PropertyValues,
query,
} from "lit-element";
import { HASSDomEvent } from "../../../../../common/dom/fire_event";
import { navigate } from "../../../../../common/navigate";
import { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
import {
addMembersToGroup,
fetchGroup,
@@ -22,12 +22,13 @@ import {
removeMembersFromGroup,
ZHAGroup,
ZHADeviceEndpoint,
} from "../../../../../data/zha";
import "../../../../../layouts/hass-error-screen";
import "../../../../../layouts/hass-subpage";
import { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
} from "../../../data/zha";
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-subpage";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { formatAsPaddedHex } from "./functions";
import "./zha-device-card";
import "./zha-device-endpoint-data-table";
import type { ZHADeviceEndpointDataTable } from "./zha-device-endpoint-data-table";
@@ -121,26 +122,25 @@ export class ZHAGroupPage extends LitElement {
<div class="header">
${this.hass.localize("ui.panel.config.zha.groups.members")}
</div>
<ha-card>
${this.group.members.length
? this.group.members.map(
(member) =>
html`<a
href="/config/devices/device/${member.device
.device_reg_id}"
>
<paper-item
>${member.device.user_given_name ||
member.device.name}</paper-item
>
</a>`
)
: html`
<paper-item>
This group has no members
</paper-item>
`}
</ha-card>
${this.group.members.length
? this.group.members.map(
(member) => html`
<zha-device-card
class="card"
.hass=${this.hass}
.device=${member.device}
.narrow=${this.narrow}
.showActions=${false}
.showEditableInfo=${false}
></zha-device-card>
`
)
: html`
<p>
This group has no members
</p>
`}
${this.group.members.length
? html`
<div class="header">
@@ -285,9 +285,6 @@ export class ZHAGroupPage extends LitElement {
static get styles(): CSSResult[] {
return [
css`
hass-subpage {
--app-header-text-color: var(--sidebar-icon-color);
}
.header {
font-family: var(--paper-font-display1_-_font-family);
-webkit-font-smoothing: var(
@@ -300,13 +297,12 @@ export class ZHAGroupPage extends LitElement {
opacity: var(--dark-primary-opacity);
}
.button {
float: right;
ha-config-section *:last-child {
padding-bottom: 24px;
}
a {
color: var(--primary-color);
text-decoration: none;
.button {
float: right;
}
mwc-button paper-spinner {
@@ -0,0 +1,172 @@
import "@material/mwc-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-spinner/paper-spinner";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
import { fetchGroups, removeGroups, ZHAGroup } from "../../../data/zha";
import "../../../layouts/hass-subpage";
import { HomeAssistant } from "../../../types";
import { sortZHAGroups } from "./functions";
import "./zha-groups-data-table";
@customElement("zha-groups-dashboard")
export class ZHAGroupsDashboard extends LitElement {
@property() public hass!: HomeAssistant;
@property() public narrow = false;
@property() public _groups?: ZHAGroup[];
@property() private _processingRemove = false;
@property() private _selectedGroupsToRemove: number[] = [];
private _firstUpdatedCalled = false;
public connectedCallback(): void {
super.connectedCallback();
if (this.hass && this._firstUpdatedCalled) {
this._fetchGroups();
}
}
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.hass) {
this._fetchGroups();
}
this._firstUpdatedCalled = true;
}
protected render(): TemplateResult {
return html`
<hass-subpage
.header=${this.hass!.localize(
"ui.panel.config.zha.groups.groups-header"
)}
>
<ha-icon-button
slot="toolbar-icon"
icon="hass:plus"
@click=${this._addGroup}
></ha-icon-button>
<div class="content">
${this._groups
? html`
<zha-groups-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.groups=${this._groups}
.selectable=${true}
@selection-changed=${this._handleRemoveSelectionChanged}
class="table"
></zha-groups-data-table>
`
: html`
<paper-spinner
active
alt=${this.hass!.localize("ui.common.loading")}
></paper-spinner>
`}
</div>
<div class="paper-dialog-buttons">
<mwc-button
?disabled="${!this._selectedGroupsToRemove.length ||
this._processingRemove}"
@click="${this._removeGroup}"
class="button"
>
<paper-spinner
?active="${this._processingRemove}"
alt=${this.hass!.localize(
"ui.panel.config.zha.groups.removing_groups"
)}
></paper-spinner>
${this.hass!.localize(
"ui.panel.config.zha.groups.remove_groups"
)}</mwc-button
>
</div>
</hass-subpage>
`;
}
private async _fetchGroups() {
this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups);
}
private _handleRemoveSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedGroupsToRemove = ev.detail.value.map((value) =>
Number(value)
);
}
private async _removeGroup(): Promise<void> {
this._processingRemove = true;
this._groups = await removeGroups(this.hass, this._selectedGroupsToRemove);
this._selectedGroupsToRemove = [];
this._processingRemove = false;
}
private async _addGroup(): Promise<void> {
navigate(this, `/config/zha/group-add`);
}
static get styles(): CSSResult[] {
return [
css`
.content {
padding: 4px;
}
zha-groups-data-table {
width: 100%;
}
.button {
float: right;
}
.table {
height: 200px;
overflow: auto;
}
mwc-button paper-spinner {
width: 14px;
height: 14px;
margin-right: 20px;
}
paper-spinner {
display: none;
}
paper-spinner[active] {
display: block;
}
.paper-dialog-buttons {
align-items: flex-end;
padding: 8px;
}
.paper-dialog-buttons .warning {
--mdc-theme-primary: var(--google-red-500);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-groups-dashboard": ZHAGroupsDashboard;
}
}
@@ -0,0 +1,129 @@
import {
customElement,
html,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { navigate } from "../../../common/navigate";
import "../../../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
HaDataTable,
} from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-state-icon";
import type { ZHADevice, ZHAGroup } from "../../../data/zha";
import type { HomeAssistant } from "../../../types";
import { formatAsPaddedHex } from "./functions";
export interface GroupRowData extends ZHAGroup {
group?: GroupRowData;
id?: string;
}
@customElement("zha-groups-data-table")
export class ZHAGroupsDataTable extends LitElement {
@property() public hass!: HomeAssistant;
@property() public narrow = false;
@property() public groups: ZHAGroup[] = [];
@property() public selectable = false;
@query("ha-data-table") private _dataTable!: HaDataTable;
private _groups = memoizeOne((groups: ZHAGroup[]) => {
let outputGroups: GroupRowData[] = groups;
outputGroups = outputGroups.map((group) => {
return {
...group,
id: String(group.group_id),
};
});
return outputGroups;
});
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer =>
narrow
? {
name: {
title: "Group",
sortable: true,
filterable: true,
direction: "asc",
grows: true,
template: (name) => html`
<div @click=${this._handleRowClicked} style="cursor: pointer;">
${name}
</div>
`,
},
}
: {
name: {
title: this.hass.localize("ui.panel.config.zha.groups.groups"),
sortable: true,
filterable: true,
direction: "asc",
grows: true,
template: (name) => html`
<div @click=${this._handleRowClicked} style="cursor: pointer;">
${name}
</div>
`,
},
group_id: {
title: this.hass.localize("ui.panel.config.zha.groups.group_id"),
type: "numeric",
width: "15%",
template: (groupId: number) => {
return html` ${formatAsPaddedHex(groupId)} `;
},
sortable: true,
},
members: {
title: this.hass.localize("ui.panel.config.zha.groups.members"),
type: "numeric",
width: "15%",
template: (members: ZHADevice[]) => {
return html` ${members.length} `;
},
sortable: true,
},
}
);
public clearSelection() {
this._dataTable.clearSelection();
}
protected render(): TemplateResult {
return html`
<ha-data-table
.columns=${this._columns(this.narrow)}
.data=${this._groups(this.groups)}
.selectable=${this.selectable}
auto-height
></ha-data-table>
`;
}
private _handleRowClicked(ev: CustomEvent) {
const groupId = ((ev.target as HTMLElement).closest(
".mdc-data-table__row"
) as any).rowId;
navigate(this, `/config/zha/group/${groupId}`);
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-groups-data-table": ZHAGroupsDataTable;
}
}
+148
View File
@@ -0,0 +1,148 @@
import "../../../components/ha-icon-button";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { navigate } from "../../../common/navigate";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import "../../../components/ha-service-description";
import { ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import "./zha-device-card";
@customElement("zha-node")
export class ZHANode extends LitElement {
@property() public hass?: HomeAssistant;
@property() public isWide?: boolean;
@property() public device?: ZHADevice;
@property() private _showHelp = false;
protected render(): TemplateResult {
return html`
<ha-config-section .isWide="${this.isWide}">
<div class="header" slot="header">
<span
>${this.hass!.localize(
"ui.panel.config.zha.node_management.header"
)}</span
>
<ha-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
></ha-icon-button>
</div>
<span slot="introduction">
${this.hass!.localize(
"ui.panel.config.zha.node_management.introduction"
)}
<br /><br />
${this.hass!.localize(
"ui.panel.config.zha.node_management.hint_battery_devices"
)}
<br /><br />
${this.hass!.localize(
"ui.panel.config.zha.node_management.hint_wakeup"
)}
</span>
<div class="content">
${this.device
? html`
<zha-device-card
class="card"
.hass=${this.hass}
.device=${this.device}
.narrow=${!this.isWide}
.showHelp=${this._showHelp}
showName
showModelInfo
.showEntityDetail=${false}
showActions
@zha-device-removed=${this._onDeviceRemoved}
></zha-device-card>
`
: html`
<paper-spinner
active
alt=${this.hass!.localize("ui.common.loading")}
></paper-spinner>
`}
</div>
</ha-config-section>
`;
}
private _onHelpTap(): void {
this._showHelp = !this._showHelp;
}
private _onDeviceRemoved(): void {
this.device = undefined;
navigate(this, `/config/zha`, true);
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.node-info {
margin-left: 16px;
}
.help-text {
color: grey;
padding-left: 28px;
padding-right: 28px;
padding-bottom: 16px;
}
.content {
max-width: 680px;
}
.card {
padding: 28px 20px 0;
margin-top: 24px;
}
ha-service-description {
display: block;
color: grey;
}
[hidden] {
display: none;
}
.header {
flex-grow: 1;
}
.toggle-help-icon {
float: right;
top: 6px;
right: 0;
padding-right: 0px;
color: var(--primary-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-node": ZHANode;
}
}
@@ -1,27 +1,27 @@
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "../../../../../components/ha-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-input/paper-input";
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 { computeStateDomain } from "../../../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../../../common/entity/compute_state_name";
import { sortStatesByName } from "../../../../../common/entity/states_sort_by_name";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-menu-button";
import "../../../../../components/ha-icon-button-arrow-prev";
import "../../../../../components/ha-service-description";
import "../../../../../layouts/ha-app-layout";
import { EventsMixin } from "../../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../../mixins/localize-mixin";
import "../../../../../styles/polymer-ha-style";
import "../../../ha-config-section";
import "../../../ha-form-style";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { sortStatesByName } from "../../../common/entity/states_sort_by_name";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import "../../../components/ha-menu-button";
import "../../../components/ha-icon-button-arrow-prev";
import "../../../components/ha-service-description";
import "../../../layouts/ha-app-layout";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../styles/polymer-ha-style";
import "../ha-config-section";
import "../ha-form-style";
import "./zwave-groups";
import "./zwave-log";
import "./zwave-network";
@@ -38,10 +38,6 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex ha-style ha-form-style">
app-toolbar {
border-bottom: 1px solid var(--divider-color);
}
.content {
margin-top: 24px;
}
@@ -4,10 +4,10 @@ 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 { computeStateName } from "../../../../../common/entity/compute_state_name";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../styles/polymer-ha-style";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import "../../../styles/polymer-ha-style";
class ZwaveGroups extends PolymerElement {
static get template() {
@@ -2,9 +2,9 @@ import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../../../components/dialog/ha-paper-dialog";
import { EventsMixin } from "../../../../../mixins/events-mixin";
import "../../../../../styles/polymer-ha-style-dialog";
import "../../../components/dialog/ha-paper-dialog";
import { EventsMixin } from "../../../mixins/events-mixin";
import "../../../styles/polymer-ha-style-dialog";
class ZwaveLogDialog extends EventsMixin(PolymerElement) {
static get template() {
@@ -4,12 +4,12 @@ import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import isPwa from "../../../../../common/config/is_pwa";
import "../../../../../components/ha-card";
import { EventsMixin } from "../../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../../mixins/localize-mixin";
import "../../../ha-config-section";
import "../../../../../styles/polymer-ha-style";
import isPwa from "../../../common/config/is_pwa";
import "../../../components/ha-card";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../ha-config-section";
import "../../../styles/polymer-ha-style";
let registeredDialog = false;

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