Compare commits
111 Commits
20200313.0
...
card-helpe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fbfdec546 | ||
|
|
6692fa439a | ||
|
|
0535247bb3 | ||
|
|
a0a4fcaf5f | ||
|
|
454d81facc | ||
|
|
0bfa8260fa | ||
|
|
f2124f1c95 | ||
|
|
451bc2370a | ||
|
|
0b17642c31 | ||
|
|
214dc25576 | ||
|
|
158eddfd44 | ||
|
|
5daa6dbd25 | ||
|
|
645ef3e61f | ||
|
|
5dcea51712 | ||
|
|
6995968d50 | ||
|
|
c4cb42f3c2 | ||
|
|
0d4a7a2b3e | ||
|
|
20cc9c9b42 | ||
|
|
8a6bd04543 | ||
|
|
263138a388 | ||
|
|
e645342131 | ||
|
|
6e4c707f9e | ||
|
|
fca286d6c0 | ||
|
|
5a2e08647f | ||
|
|
f6dac98abd | ||
|
|
ddb525f6cd | ||
|
|
f7ee712456 | ||
|
|
ff873e2f71 | ||
|
|
54b57e6222 | ||
|
|
375abfb95e | ||
|
|
30a38fa6d1 | ||
|
|
554c0b692d | ||
|
|
61ac831882 | ||
|
|
59e89a0daf | ||
|
|
1e3950cd1d | ||
|
|
f514ea453c | ||
|
|
cc046478e5 | ||
|
|
a17c1052cd | ||
|
|
2408f9b8fa | ||
|
|
6aae1b3378 | ||
|
|
ed51223226 | ||
|
|
013808b7f5 | ||
|
|
af584e1d12 | ||
|
|
3763d7a1d0 | ||
|
|
ce92add096 | ||
|
|
40c94b6596 | ||
|
|
c894bc2e40 | ||
|
|
415a4fa1af | ||
|
|
b9367a33a8 | ||
|
|
7170f06c08 | ||
|
|
f2578a58b4 | ||
|
|
1950656bd5 | ||
|
|
eed3263c70 | ||
|
|
02e01626f5 | ||
|
|
41a2d9604e | ||
|
|
7d6f188bfc | ||
|
|
15a144f17a | ||
|
|
c54f2b66da | ||
|
|
685a0807d8 | ||
|
|
0d404e0e37 | ||
|
|
39bb859f57 | ||
|
|
90e32b7e45 | ||
|
|
63a2d9dd18 | ||
|
|
4982693883 | ||
|
|
f4211e3fa3 | ||
|
|
eacf58b5a5 | ||
|
|
f9349bc731 | ||
|
|
3840671764 | ||
|
|
fd62cf02d6 | ||
|
|
254744cd7d | ||
|
|
595d04c922 | ||
|
|
a3969fe2c8 | ||
|
|
220e4134b7 | ||
|
|
56e176a6f1 | ||
|
|
780c15d6b3 | ||
|
|
55ff848b78 | ||
|
|
dbe829bc7d | ||
|
|
8d0508f320 | ||
|
|
2741bb8b38 | ||
|
|
16cadd53cf | ||
|
|
a8d21c6112 | ||
|
|
6b2e707653 | ||
|
|
205b7451fa | ||
|
|
1d3aeec0de | ||
|
|
ac911dcd31 | ||
|
|
89a94b3efc | ||
|
|
71793dcfa5 | ||
|
|
1e527a8350 | ||
|
|
262b12eb93 | ||
|
|
7fb1e699ae | ||
|
|
9ee647329b | ||
|
|
cd6dcec644 | ||
|
|
d8f248c60e | ||
|
|
2925b930ad | ||
|
|
127aaba47b | ||
|
|
8bc8761af6 | ||
|
|
a88321d243 | ||
|
|
4e19232960 | ||
|
|
5b95bdb6b7 | ||
|
|
0fc59ccb16 | ||
|
|
a9daf9835a | ||
|
|
2110d9c3b9 | ||
|
|
b77e0b8125 | ||
|
|
5197f102ea | ||
|
|
447d4604c6 | ||
|
|
5a84e34f93 | ||
|
|
fb6d3cccdc | ||
|
|
fad3cb185b | ||
|
|
f61ce395f5 | ||
|
|
17f3299152 | ||
|
|
17e04589d0 |
@@ -8,7 +8,7 @@ trigger:
|
||||
pr: none
|
||||
variables:
|
||||
- name: versionWheels
|
||||
value: '1.3-3.7-alpine3.10'
|
||||
value: '1.10.1-3.7-alpine3.11'
|
||||
- name: versionNode
|
||||
value: '12.1'
|
||||
- group: twine
|
||||
@@ -50,15 +50,8 @@ stages:
|
||||
- template: templates/azp-job-wheels.yaml@azure
|
||||
parameters:
|
||||
builderVersion: '$(versionWheels)'
|
||||
builderApk: 'build-base'
|
||||
wheelsLocal: true
|
||||
wheelsRequirement: 'requirement.txt'
|
||||
preBuild:
|
||||
- task: NodeTool@0
|
||||
displayName: "Use Node $(versionNode)"
|
||||
inputs:
|
||||
versionSpec: "$(versionNode)"
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
yarn install
|
||||
script/build_frontend
|
||||
sleep 240
|
||||
echo "home-assistant-frontend==$(Build.SourceBranchName)" > requirement.txt
|
||||
|
||||
@@ -172,6 +172,9 @@ export class HcMain extends HassElement {
|
||||
return;
|
||||
}
|
||||
if (!this._unsubLovelace || this._urlPath !== msg.urlPath) {
|
||||
if (msg.urlPath === "lovelace") {
|
||||
msg.urlPath = null;
|
||||
}
|
||||
this._urlPath = msg.urlPath;
|
||||
if (this._unsubLovelace) {
|
||||
this._unsubLovelace();
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 805 B After Width: | Height: | Size: 803 B |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 781 B After Width: | Height: | Size: 375 B |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 126 KiB |
BIN
gallery/public/images/frenck.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
gallery/public/images/netflix.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
@@ -34,6 +34,7 @@ export const createMediaPlayerEntities = () => [
|
||||
media_content_type: "movie",
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
entity_picture: "/images/frenck.jpg",
|
||||
supported_features: 33,
|
||||
}),
|
||||
getEntity("media_player", "living_room", "playing", {
|
||||
@@ -42,6 +43,7 @@ export const createMediaPlayerEntities = () => [
|
||||
media_title: "Chapter 1",
|
||||
media_series_title: "House of Cards",
|
||||
app_name: "Netflix",
|
||||
entity_picture: "/images/netflix.jpg",
|
||||
supported_features: 1,
|
||||
}),
|
||||
getEntity("media_player", "sonos_idle", "idle", {
|
||||
|
||||
@@ -93,7 +93,7 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
? "not_available"
|
||||
: ""}
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.connection.haVersion,
|
||||
this.hass.config.version,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
|
||||
@@ -107,7 +107,7 @@ class HassioAddonInfo extends LitElement {
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title="${this.addon.name} ${this.addon
|
||||
.last_version} is available"
|
||||
.version_latest} is available"
|
||||
.description="You are currently running version ${this.addon
|
||||
.version}"
|
||||
icon="hassio:arrow-up-bold-circle"
|
||||
@@ -179,7 +179,7 @@ class HassioAddonInfo extends LitElement {
|
||||
`}
|
||||
`
|
||||
: html`
|
||||
${this.addon.last_version}
|
||||
${this.addon.version_latest}
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
@@ -636,7 +636,7 @@ class HassioAddonInfo extends LitElement {
|
||||
this.addon &&
|
||||
!this.addon.detached &&
|
||||
this.addon.version &&
|
||||
this.addon.version !== this.addon.last_version
|
||||
this.addon.version !== this.addon.version_latest
|
||||
);
|
||||
}
|
||||
|
||||
@@ -661,8 +661,7 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
private get _computeCannotIngressSidebar(): boolean {
|
||||
return (
|
||||
!this.addon.ingress ||
|
||||
!atLeastVersion(this.hass.connection.haVersion, 0, 92)
|
||||
!this.addon.ingress || !atLeastVersion(this.hass.config.version, 0, 92)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ class HassioAddons extends LitElement {
|
||||
? "running"
|
||||
: "stopped"}
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.connection.haVersion,
|
||||
this.hass.config.version,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
|
||||
@@ -40,8 +40,8 @@ export class HassioUpdate extends LitElement {
|
||||
].filter((value) => {
|
||||
return (
|
||||
!!value &&
|
||||
(value.last_version
|
||||
? value.version !== value.last_version
|
||||
(value.version_latest
|
||||
? value.version !== value.version_latest
|
||||
: value.version_latest
|
||||
? value.version !== value.version_latest
|
||||
: false)
|
||||
@@ -68,26 +68,26 @@ export class HassioUpdate extends LitElement {
|
||||
${this._renderUpdateCard(
|
||||
"Home Assistant Core",
|
||||
this.hassInfo.version,
|
||||
this.hassInfo.last_version,
|
||||
this.hassInfo.version_latest,
|
||||
"hassio/homeassistant/update",
|
||||
`https://${
|
||||
this.hassInfo.last_version.includes("b") ? "rc" : "www"
|
||||
this.hassInfo.version_latest.includes("b") ? "rc" : "www"
|
||||
}.home-assistant.io/latest-release-notes/`,
|
||||
"hassio:home-assistant"
|
||||
)}
|
||||
${this._renderUpdateCard(
|
||||
"Supervisor",
|
||||
this.supervisorInfo.version,
|
||||
this.supervisorInfo.last_version,
|
||||
this.supervisorInfo.version_latest,
|
||||
"hassio/supervisor/update",
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.last_version}`
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.version_latest}`
|
||||
)}
|
||||
${this.hassOsInfo
|
||||
? this._renderUpdateCard(
|
||||
"Operating System",
|
||||
this.hassOsInfo.version,
|
||||
this.hassOsInfo.version_latest,
|
||||
"hassio/hassos/update",
|
||||
"hassio/os/update",
|
||||
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}`
|
||||
)
|
||||
: ""}
|
||||
|
||||
@@ -87,8 +87,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
applyThemesOnElement(
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
this.hass.selectedTheme,
|
||||
true
|
||||
this.hass.selectedTheme || this.hass.themes.default_theme
|
||||
);
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
// Paulus - March 17, 2019
|
||||
|
||||
@@ -100,7 +100,7 @@ class HassioHostInfo extends LitElement {
|
||||
<ha-call-api-button
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
path="hassio/hassos/config/sync"
|
||||
path="hassio/os/config/sync"
|
||||
title="Load HassOS configs or updates from USB"
|
||||
>Import from USB</ha-call-api-button
|
||||
>
|
||||
@@ -108,9 +108,7 @@ class HassioHostInfo extends LitElement {
|
||||
: ""}
|
||||
${this.hostInfo.version !== this.hostInfo.version_latest
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
.hass=${this.hass}
|
||||
path="hassio/hassos/update"
|
||||
<ha-call-api-button .hass=${this.hass} path="hassio/os/update"
|
||||
>Update</ha-call-api-button
|
||||
>
|
||||
`
|
||||
|
||||
@@ -41,7 +41,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Latest version</td>
|
||||
<td>${this.supervisorInfo.last_version}</td>
|
||||
<td>${this.supervisorInfo.version_latest}</td>
|
||||
</tr>
|
||||
${this.supervisorInfo.channel !== "stable"
|
||||
? html`
|
||||
@@ -63,7 +63,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
<ha-call-api-button .hass=${this.hass} path="hassio/supervisor/reload"
|
||||
>Reload</ha-call-api-button
|
||||
>
|
||||
${this.supervisorInfo.version !== this.supervisorInfo.last_version
|
||||
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="36" height="36" viewBox="0 0 24 24"><path fill="#505050" d="M19,3A2,2 0 0,1 21,5V11H19V13H19L17,13V15H15V17H13V19H11V21H5C3.89,21 3,20.1 3,19V5A2,2 0 0,1 5,3H19M21,15V19A2,2 0 0,1 19,21H19L15,21V19H17V17H19V15H21M19,8.5A0.5,0.5 0 0,0 18.5,8H5.5A0.5,0.5 0 0,0 5,8.5V15.5A0.5,0.5 0 0,0 5.5,16H11V15H13V13H15V11H17V9H19V8.5Z" /></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="36" height="36" version="1.1" viewBox="0 0 24 24"><path fill="#505050" d="M19,3A2,2 0 0,1 21,5V11H19V13H19L17,13V15H15V17H13V19H11V21H5C3.89,21 3,20.1 3,19V5A2,2 0 0,1 5,3H19M21,15V19A2,2 0 0,1 19,21H19L15,21V19H17V17H19V15H21M19,8.5A0.5,0.5 0 0,0 18.5,8H5.5A0.5,0.5 0 0,0 5,8.5V15.5A0.5,0.5 0 0,0 5.5,16H11V15H13V13H15V11H17V9H19V8.5Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 571 B After Width: | Height: | Size: 434 B |
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20200313.0",
|
||||
version="20200330.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
/** Icon to use when no icon specified for domain. */
|
||||
export const DEFAULT_DOMAIN_ICON = "hass:bookmark";
|
||||
|
||||
/** Panel to show when no panel is picked. */
|
||||
export const DEFAULT_PANEL = "lovelace";
|
||||
|
||||
/** Domains that have a state card. */
|
||||
export const DOMAINS_WITH_CARD = [
|
||||
"climate",
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { derivedStyles } from "../../resources/styles";
|
||||
import { HomeAssistant, Theme } from "../../types";
|
||||
|
||||
interface ProcessedTheme {
|
||||
keys: { [key: string]: "" };
|
||||
styles: { [key: string]: string };
|
||||
}
|
||||
|
||||
const hexToRgb = (hex: string): string | null => {
|
||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||
@@ -15,67 +21,82 @@ const hexToRgb = (hex: string): string | null => {
|
||||
: null;
|
||||
};
|
||||
|
||||
let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
|
||||
|
||||
/**
|
||||
* Apply a theme to an element by setting the CSS variables on it.
|
||||
*
|
||||
* element: Element to apply theme on.
|
||||
* themes: HASS Theme information
|
||||
* localTheme: selected theme.
|
||||
* updateMeta: boolean if we should update the theme-color meta element.
|
||||
* selectedTheme: selected theme.
|
||||
*/
|
||||
export const applyThemesOnElement = (
|
||||
element,
|
||||
themes,
|
||||
localTheme,
|
||||
updateMeta = false
|
||||
themes: HomeAssistant["themes"],
|
||||
selectedTheme?: string
|
||||
) => {
|
||||
if (!element._themes) {
|
||||
element._themes = {};
|
||||
}
|
||||
let themeName = themes.default_theme;
|
||||
if (localTheme === "default" || (localTheme && themes.themes[localTheme])) {
|
||||
themeName = localTheme;
|
||||
}
|
||||
const styles = { ...element._themes };
|
||||
if (themeName !== "default") {
|
||||
const theme = { ...derivedStyles, ...themes.themes[themeName] };
|
||||
Object.keys(theme).forEach((key) => {
|
||||
const prefixedKey = `--${key}`;
|
||||
element._themes[prefixedKey] = "";
|
||||
styles[prefixedKey] = theme[key];
|
||||
if (key.startsWith("rgb")) {
|
||||
return;
|
||||
}
|
||||
const rgbKey = `rgb-${key}`;
|
||||
if (theme[rgbKey] !== undefined) {
|
||||
return;
|
||||
}
|
||||
const prefixedRgbKey = `--${rgbKey}`;
|
||||
element._themes[prefixedRgbKey] = "";
|
||||
const rgbValue = hexToRgb(theme[key]);
|
||||
if (rgbValue !== null) {
|
||||
styles[prefixedRgbKey] = rgbValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (element.updateStyles) {
|
||||
element.updateStyles(styles);
|
||||
} else if (window.ShadyCSS) {
|
||||
// implement updateStyles() method of Polymer elements
|
||||
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles);
|
||||
}
|
||||
const newTheme = selectedTheme
|
||||
? PROCESSED_THEMES[selectedTheme] || processTheme(selectedTheme, themes)
|
||||
: undefined;
|
||||
|
||||
if (!updateMeta) {
|
||||
if (!element._themes && !newTheme) {
|
||||
// No styles to reset, and no styles to set
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = document.querySelector("meta[name=theme-color]");
|
||||
if (meta) {
|
||||
if (!meta.hasAttribute("default-content")) {
|
||||
meta.setAttribute("default-content", meta.getAttribute("content")!);
|
||||
}
|
||||
const themeColor =
|
||||
styles["--primary-color"] || meta.getAttribute("default-content");
|
||||
meta.setAttribute("content", themeColor);
|
||||
// Add previous set keys to reset them, and new theme
|
||||
const styles = { ...element._themes, ...newTheme?.styles };
|
||||
element._themes = newTheme?.keys;
|
||||
|
||||
// Set and/or reset styles
|
||||
if (element.updateStyles) {
|
||||
element.updateStyles(styles);
|
||||
} else if (window.ShadyCSS) {
|
||||
// Implement updateStyles() method of Polymer elements
|
||||
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles);
|
||||
}
|
||||
};
|
||||
|
||||
const processTheme = (
|
||||
themeName: string,
|
||||
themes: HomeAssistant["themes"]
|
||||
): ProcessedTheme | undefined => {
|
||||
if (!themes.themes[themeName]) {
|
||||
return;
|
||||
}
|
||||
const theme: Theme = {
|
||||
...derivedStyles,
|
||||
...themes.themes[themeName],
|
||||
};
|
||||
const styles = {};
|
||||
const keys = {};
|
||||
for (const key of Object.keys(theme)) {
|
||||
const prefixedKey = `--${key}`;
|
||||
const value = theme[key];
|
||||
styles[prefixedKey] = value;
|
||||
keys[prefixedKey] = "";
|
||||
|
||||
// Try to create a rgb value for this key if it is a hex color
|
||||
if (!value.startsWith("#")) {
|
||||
// Not a hex color
|
||||
continue;
|
||||
}
|
||||
const rgbKey = `rgb-${key}`;
|
||||
if (theme[rgbKey] !== undefined) {
|
||||
// Theme has it's own rgb value
|
||||
continue;
|
||||
}
|
||||
const rgbValue = hexToRgb(value);
|
||||
if (rgbValue !== null) {
|
||||
const prefixedRgbKey = `--${rgbKey}`;
|
||||
styles[prefixedRgbKey] = rgbValue;
|
||||
keys[prefixedRgbKey] = "";
|
||||
}
|
||||
}
|
||||
PROCESSED_THEMES[themeName] = { styles, keys };
|
||||
return { styles, keys };
|
||||
};
|
||||
|
||||
export const invalidateThemeCache = () => {
|
||||
PROCESSED_THEMES = {};
|
||||
};
|
||||
|
||||
@@ -4,17 +4,58 @@ import { domainIcon } from "./domain_icon";
|
||||
|
||||
export const coverIcon = (state: HassEntity): string => {
|
||||
const open = state.state !== "closed";
|
||||
|
||||
switch (state.attributes.device_class) {
|
||||
case "garage":
|
||||
return open ? "hass:garage-open" : "hass:garage";
|
||||
switch (state.state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
case "closing":
|
||||
return "hass:arrow-down-box";
|
||||
case "closed":
|
||||
return "hass:garage";
|
||||
default:
|
||||
return "hass:garage-open";
|
||||
}
|
||||
case "gate":
|
||||
switch (state.state) {
|
||||
case "opening":
|
||||
case "closing":
|
||||
return "hass:gate-arrow-right";
|
||||
case "closed":
|
||||
return "hass:gate";
|
||||
default:
|
||||
return "hass:gate-open";
|
||||
}
|
||||
case "door":
|
||||
return open ? "hass:door-open" : "hass:door-closed";
|
||||
case "damper":
|
||||
return open ? "hass:circle" : "hass:circle-slice-8";
|
||||
case "shutter":
|
||||
return open ? "hass:window-shutter-open" : "hass:window-shutter";
|
||||
case "blind":
|
||||
return open ? "hass:blinds-open" : "hass:blinds";
|
||||
case "curtain":
|
||||
switch (state.state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
case "closing":
|
||||
return "hass:arrow-down-box";
|
||||
case "closed":
|
||||
return "hass:blinds";
|
||||
default:
|
||||
return "hass:blinds-open";
|
||||
}
|
||||
case "window":
|
||||
return open ? "hass:window-open" : "hass:window-closed";
|
||||
switch (state.state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
case "closing":
|
||||
return "hass:arrow-down-box";
|
||||
case "closed":
|
||||
return "hass:window-closed";
|
||||
default:
|
||||
return "hass:window-open";
|
||||
}
|
||||
default:
|
||||
return domainIcon("cover", state.state);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,16 @@ export const domainIcon = (domain: string, state?: string): string => {
|
||||
: "hass:checkbox-marked-circle";
|
||||
|
||||
case "cover":
|
||||
return state === "closed" ? "hass:window-closed" : "hass:window-open";
|
||||
switch (state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
case "closing":
|
||||
return "hass:arrow-down-box";
|
||||
case "closed":
|
||||
return "hass:window-closed";
|
||||
default:
|
||||
return "hass:window-open";
|
||||
}
|
||||
|
||||
case "lock":
|
||||
return state && state === "unlocked" ? "hass:lock-open" : "hass:lock";
|
||||
|
||||
@@ -69,9 +69,10 @@ export interface DataTableSortColumnData {
|
||||
|
||||
export interface DataTableColumnData extends DataTableSortColumnData {
|
||||
title: string;
|
||||
type?: "numeric" | "icon";
|
||||
type?: "numeric" | "icon" | "icon-button";
|
||||
template?: <T>(data: any, row: T) => TemplateResult | string;
|
||||
width?: string;
|
||||
maxWidth?: string;
|
||||
grows?: boolean;
|
||||
}
|
||||
|
||||
@@ -227,10 +228,13 @@ export class HaDataTable extends LitElement {
|
||||
const sorted = key === this._sortColumn;
|
||||
const classes = {
|
||||
"mdc-data-table__header-cell--numeric": Boolean(
|
||||
column.type && column.type === "numeric"
|
||||
column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__header-cell--icon": Boolean(
|
||||
column.type && column.type === "icon"
|
||||
column.type === "icon"
|
||||
),
|
||||
"mdc-data-table__header-cell--icon-button": Boolean(
|
||||
column.type === "icon-button"
|
||||
),
|
||||
sortable: Boolean(column.sortable),
|
||||
"not-sorted": Boolean(column.sortable && !sorted),
|
||||
@@ -241,9 +245,8 @@ export class HaDataTable extends LitElement {
|
||||
class="mdc-data-table__header-cell ${classMap(classes)}"
|
||||
style=${column.width
|
||||
? styleMap({
|
||||
[column.grows ? "minWidth" : "width"]: String(
|
||||
column.width
|
||||
),
|
||||
[column.grows ? "minWidth" : "width"]: column.width,
|
||||
maxWidth: column.maxWidth || "",
|
||||
})
|
||||
: ""}
|
||||
role="columnheader"
|
||||
@@ -318,10 +321,13 @@ export class HaDataTable extends LitElement {
|
||||
<div
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"mdc-data-table__cell--numeric": Boolean(
|
||||
column.type && column.type === "numeric"
|
||||
column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__cell--icon": Boolean(
|
||||
column.type && column.type === "icon"
|
||||
column.type === "icon"
|
||||
),
|
||||
"mdc-data-table__cell--icon-button": Boolean(
|
||||
column.type === "icon-button"
|
||||
),
|
||||
grows: Boolean(column.grows),
|
||||
})}"
|
||||
@@ -329,7 +335,10 @@ export class HaDataTable extends LitElement {
|
||||
? styleMap({
|
||||
[column.grows
|
||||
? "minWidth"
|
||||
: "width"]: String(column.width),
|
||||
: "width"]: column.width,
|
||||
maxWidth: column.maxWidth
|
||||
? column.maxWidth
|
||||
: "",
|
||||
})
|
||||
: ""}
|
||||
>
|
||||
@@ -532,6 +541,7 @@ export class HaDataTable extends LitElement {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell.mdc-data-table__cell--icon {
|
||||
@@ -544,7 +554,7 @@ export class HaDataTable extends LitElement {
|
||||
padding-left: 16px;
|
||||
/* @noflip */
|
||||
padding-right: 0;
|
||||
width: 40px;
|
||||
width: 56px;
|
||||
}
|
||||
[dir="rtl"] .mdc-data-table__header-cell--checkbox,
|
||||
.mdc-data-table__header-cell--checkbox[dir="rtl"],
|
||||
@@ -591,7 +601,7 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
.mdc-data-table__header-cell--icon,
|
||||
.mdc-data-table__cell--icon {
|
||||
width: 24px;
|
||||
width: 54px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell.mdc-data-table__header-cell--icon {
|
||||
@@ -610,6 +620,28 @@ export class HaDataTable extends LitElement {
|
||||
margin-right: -8px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon-button,
|
||||
.mdc-data-table__cell--icon-button {
|
||||
width: 56px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon-button:first-child,
|
||||
.mdc-data-table__cell--icon-button:first-child {
|
||||
width: 64px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon-button:last-child,
|
||||
.mdc-data-table__cell--icon-button:last-child {
|
||||
width: 64px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--icon-button a {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell {
|
||||
font-family: Roboto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -695,6 +727,9 @@ export class HaDataTable extends LitElement {
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.scroller {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
@@ -246,7 +246,7 @@ class HaEntityPicker extends LitElement {
|
||||
paper-input > paper-icon-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 2px;
|
||||
padding: 0px 2px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
[hidden] {
|
||||
|
||||
@@ -19,7 +19,7 @@ class HaCoverControls extends PolymerElement {
|
||||
<div class="state">
|
||||
<paper-icon-button
|
||||
aria-label="Open cover"
|
||||
icon="hass:arrow-up"
|
||||
icon="[[computeOpenIcon(stateObj)]]"
|
||||
on-click="onOpenTap"
|
||||
invisible$="[[!entityObj.supportsOpen]]"
|
||||
disabled="[[computeOpenDisabled(stateObj, entityObj)]]"
|
||||
@@ -32,7 +32,7 @@ class HaCoverControls extends PolymerElement {
|
||||
></paper-icon-button>
|
||||
<paper-icon-button
|
||||
aria-label="Close cover"
|
||||
icon="hass:arrow-down"
|
||||
icon="[[computeCloseIcon(stateObj)]]"
|
||||
on-click="onCloseTap"
|
||||
invisible$="[[!entityObj.supportsClose]]"
|
||||
disabled="[[computeClosedDisabled(stateObj, entityObj)]]"
|
||||
@@ -60,6 +60,26 @@ class HaCoverControls extends PolymerElement {
|
||||
return new CoverEntity(hass, stateObj);
|
||||
}
|
||||
|
||||
computeOpenIcon(stateObj) {
|
||||
switch (stateObj.attributes.device_class) {
|
||||
case "awning":
|
||||
case "gate":
|
||||
return "hass:arrow-expand-horizontal";
|
||||
default:
|
||||
return "hass:arrow-up";
|
||||
}
|
||||
}
|
||||
|
||||
computeCloseIcon(stateObj) {
|
||||
switch (stateObj.attributes.device_class) {
|
||||
case "awning":
|
||||
case "gate":
|
||||
return "hass:arrow-collapse-horizontal";
|
||||
default:
|
||||
return "hass:arrow-down";
|
||||
}
|
||||
}
|
||||
|
||||
computeOpenDisabled(stateObj, entityObj) {
|
||||
var assumedState = stateObj.attributes.assumed_state === true;
|
||||
return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState;
|
||||
|
||||
@@ -18,7 +18,6 @@ import "../components/user/ha-user-badge";
|
||||
import "../components/ha-menu-button";
|
||||
import { HomeAssistant, PanelInfo } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { DEFAULT_PANEL } from "../common/const";
|
||||
import {
|
||||
getExternalConfig,
|
||||
ExternalConfig,
|
||||
@@ -33,6 +32,7 @@ import { classMap } from "lit-html/directives/class-map";
|
||||
import { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { compare } from "../common/string/compare";
|
||||
import { getDefaultPanel } from "../data/panel";
|
||||
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
|
||||
|
||||
@@ -77,7 +77,6 @@ const panelSorter = (a: PanelInfo, b: PanelInfo) => {
|
||||
// both not built in, sort by title
|
||||
return compare(a.title!, b.title!);
|
||||
};
|
||||
const DEFAULT_PAGE = localStorage.defaultPage || DEFAULT_PANEL;
|
||||
|
||||
const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
|
||||
const panels = hass.panels;
|
||||
@@ -89,7 +88,7 @@ const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
|
||||
const afterSpacer: PanelInfo[] = [];
|
||||
|
||||
Object.values(panels).forEach((panel) => {
|
||||
if (!panel.title || panel.url_path === DEFAULT_PAGE) {
|
||||
if (!panel.title || panel.url_path === hass.defaultPanel) {
|
||||
return;
|
||||
}
|
||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||
@@ -142,8 +141,7 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
const defaultPanel =
|
||||
this.hass.panels[DEFAULT_PAGE] || this.hass.panels[DEFAULT_PANEL];
|
||||
const defaultPanel = getDefaultPanel(hass);
|
||||
|
||||
return html`
|
||||
<div class="menu">
|
||||
@@ -297,7 +295,8 @@ class HaSidebar extends LitElement {
|
||||
hass.panelUrl !== oldHass.panelUrl ||
|
||||
hass.user !== oldHass.user ||
|
||||
hass.localize !== oldHass.localize ||
|
||||
hass.states !== oldHass.states
|
||||
hass.states !== oldHass.states ||
|
||||
hass.defaultPanel !== oldHass.defaultPanel
|
||||
);
|
||||
}
|
||||
|
||||
@@ -530,6 +529,7 @@ class HaSidebar extends LitElement {
|
||||
overflow-x: hidden;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
background: none;
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
@@ -12,9 +12,9 @@ export const callAlarmAction = (
|
||||
| "arm_night"
|
||||
| "arm_custom_bypass"
|
||||
| "disarm",
|
||||
code: string
|
||||
code?: string
|
||||
) => {
|
||||
hass!.callService("alarm_control_panel", "alarm_" + action, {
|
||||
hass!.callService("alarm_control_panel", `alarm_${action}`, {
|
||||
entity_id: entity,
|
||||
code,
|
||||
});
|
||||
|
||||
@@ -25,3 +25,16 @@ export const fetchAuthProviders = () =>
|
||||
fetch("/auth/providers", {
|
||||
credentials: "same-origin",
|
||||
});
|
||||
|
||||
export const createAuthForUser = async (
|
||||
hass: HomeAssistant,
|
||||
userId: string,
|
||||
username: string,
|
||||
password: string
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config/auth_provider/homeassistant/create",
|
||||
user_id: userId,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ export const fetchDeviceTriggerCapabilities = (
|
||||
const whitelist = [
|
||||
"above",
|
||||
"below",
|
||||
"brightness",
|
||||
"brightness_pct",
|
||||
"code",
|
||||
"for",
|
||||
"position",
|
||||
|
||||
@@ -53,6 +53,9 @@ export const fallbackDeviceName = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const devicesInArea = (devices: DeviceRegistryEntry[], areaId: string) =>
|
||||
devices.filter((device) => device.area_id === areaId);
|
||||
|
||||
export const updateDeviceRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
deviceId: string,
|
||||
|
||||
@@ -27,6 +27,16 @@ export interface EntityRegistryEntryUpdateParams {
|
||||
new_entity_id?: string;
|
||||
}
|
||||
|
||||
export const findBatteryEntity = (
|
||||
hass: HomeAssistant,
|
||||
entities: EntityRegistryEntry[]
|
||||
): EntityRegistryEntry | undefined =>
|
||||
entities.find(
|
||||
(entity) =>
|
||||
hass.states[entity.entity_id] &&
|
||||
hass.states[entity.entity_id].attributes.device_class === "battery"
|
||||
);
|
||||
|
||||
export const computeEntityRegistryName = (
|
||||
hass: HomeAssistant,
|
||||
entry: EntityRegistryEntry
|
||||
|
||||
1
src/data/graph.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const strokeWidth = 5;
|
||||
@@ -29,7 +29,7 @@ export interface HassioAddonDetails extends HassioAddonInfo {
|
||||
arch: "armhf" | "aarch64" | "i386" | "amd64";
|
||||
machine: any;
|
||||
homeassistant: string;
|
||||
last_version: string;
|
||||
version_latest: string;
|
||||
boot: "auto" | "manual";
|
||||
build: boolean;
|
||||
options: object;
|
||||
|
||||
@@ -23,7 +23,7 @@ export const fetchHassioHassOsInfo = async (hass: HomeAssistant) => {
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HassioHassOSInfo>>(
|
||||
"GET",
|
||||
"hassio/hassos/info"
|
||||
"hassio/os/info"
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => {
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HassioHomeAssistantInfo>>(
|
||||
"GET",
|
||||
"hassio/homeassistant/info"
|
||||
"hassio/core/info"
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
23
src/data/lovelace_custom_cards.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface CustomCardEntry {
|
||||
type: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
preview?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomCardsWindow {
|
||||
customCards?: CustomCardEntry[];
|
||||
}
|
||||
|
||||
export const CUSTOM_TYPE_PREFIX = "custom:";
|
||||
|
||||
const customCardsWindow = window as CustomCardsWindow;
|
||||
|
||||
if (!("customCards" in customCardsWindow)) {
|
||||
customCardsWindow.customCards = [];
|
||||
}
|
||||
|
||||
export const customCards = customCardsWindow.customCards!;
|
||||
|
||||
export const getCustomCardEntry = (type: string) =>
|
||||
customCards.find((card) => card.type === type);
|
||||
@@ -17,3 +17,12 @@ export const subscribeMQTTTopic = (
|
||||
topic,
|
||||
});
|
||||
};
|
||||
|
||||
export const removeMQTTDeviceEntry = (
|
||||
hass: HomeAssistant,
|
||||
deviceId: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "mqtt/device/remove",
|
||||
device_id: deviceId,
|
||||
});
|
||||
|
||||
41
src/data/panel.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { HomeAssistant, PanelInfo } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
/** Panel to show when no panel is picked. */
|
||||
export const DEFAULT_PANEL = "lovelace";
|
||||
|
||||
export const getStorageDefaultPanelUrlPath = () =>
|
||||
localStorage.defaultPanel
|
||||
? JSON.parse(localStorage.defaultPanel)
|
||||
: DEFAULT_PANEL;
|
||||
|
||||
export const setDefaultPanel = (element: HTMLElement, urlPath: string) => {
|
||||
fireEvent(element, "hass-default-panel", { defaultPanel: urlPath });
|
||||
};
|
||||
|
||||
export const getDefaultPanel = (hass: HomeAssistant) =>
|
||||
hass.panels[hass.defaultPanel];
|
||||
|
||||
export const getPanelTitle = (hass: HomeAssistant): string | undefined => {
|
||||
if (!hass.panels) {
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = Object.values(hass.panels).find(
|
||||
(p: PanelInfo): boolean => p.url_path === hass.panelUrl
|
||||
);
|
||||
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (panel.url_path === "lovelace") {
|
||||
return hass.localize("panel.states");
|
||||
}
|
||||
|
||||
if (panel.url_path === "profile") {
|
||||
return hass.localize("panel.profile");
|
||||
}
|
||||
|
||||
return hass.localize(`panel.${panel.title}`) || panel.title || undefined;
|
||||
};
|
||||
@@ -10,7 +10,7 @@ export interface LoggedError {
|
||||
exception: string;
|
||||
count: number;
|
||||
// unix timestamp in seconds
|
||||
first_occured: number;
|
||||
first_occurred: number;
|
||||
}
|
||||
|
||||
export const fetchSystemLog = (hass: HomeAssistant) =>
|
||||
|
||||
@@ -5,6 +5,8 @@ export const SYSTEM_GROUP_ID_ADMIN = "system-admin";
|
||||
export const SYSTEM_GROUP_ID_USER = "system-users";
|
||||
export const SYSTEM_GROUP_ID_READ_ONLY = "system-read-only";
|
||||
|
||||
export const GROUPS = [SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_ADMIN];
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -15,7 +17,7 @@ export interface User {
|
||||
credentials: Credential[];
|
||||
}
|
||||
|
||||
interface UpdateUserParams {
|
||||
export interface UpdateUserParams {
|
||||
name?: User["name"];
|
||||
group_ids?: User["group_ids"];
|
||||
}
|
||||
@@ -25,10 +27,16 @@ export const fetchUsers = async (hass: HomeAssistant) =>
|
||||
type: "config/auth/list",
|
||||
});
|
||||
|
||||
export const createUser = async (hass: HomeAssistant, name: string) =>
|
||||
export const createUser = async (
|
||||
hass: HomeAssistant,
|
||||
name: string,
|
||||
// tslint:disable-next-line: variable-name
|
||||
group_ids?: User["group_ids"]
|
||||
) =>
|
||||
hass.callWS<{ user: User }>({
|
||||
type: "config/auth/create",
|
||||
name,
|
||||
group_ids,
|
||||
});
|
||||
|
||||
export const updateUser = async (
|
||||
|
||||
@@ -130,7 +130,11 @@ class DataEntryFlowDialog extends LitElement {
|
||||
>
|
||||
${this._loading || (this._step === null && this._handlers === undefined)
|
||||
? html`
|
||||
<step-flow-loading></step-flow-loading>
|
||||
<step-flow-loading
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.loading_first_time"
|
||||
)}
|
||||
></step-flow-loading>
|
||||
`
|
||||
: this._step === undefined
|
||||
? // When we are going to next step, we render 1 round of empty
|
||||
|
||||
@@ -5,14 +5,22 @@ import {
|
||||
css,
|
||||
customElement,
|
||||
CSSResult,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-spinner/paper-spinner-lite";
|
||||
|
||||
@customElement("step-flow-loading")
|
||||
class StepFlowLoading extends LitElement {
|
||||
@property() public label?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="init-spinner">
|
||||
${this.label
|
||||
? html`
|
||||
<div>${this.label}</div>
|
||||
`
|
||||
: ""}
|
||||
<paper-spinner-lite active></paper-spinner-lite>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -142,6 +142,9 @@ class DialogBox extends LitElement {
|
||||
min-width: initial;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
padding-top: 6px;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
interface BaseDialogParams {
|
||||
confirmText?: string;
|
||||
text?: string;
|
||||
text?: string | TemplateResult;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,12 +92,12 @@ window.hassConnection.then(({ conn }) => {
|
||||
subscribeFrontendUserData(conn, "core", noop);
|
||||
|
||||
if (location.pathname === "/" || location.pathname.startsWith("/lovelace/")) {
|
||||
(window as WindowWithLovelaceProm).llConfProm = fetchConfig(
|
||||
conn,
|
||||
null,
|
||||
false
|
||||
);
|
||||
(window as WindowWithLovelaceProm).llResProm = fetchResources(conn);
|
||||
const llWindow = window as WindowWithLovelaceProm;
|
||||
llWindow.llConfProm = fetchConfig(conn, null, false);
|
||||
llWindow.llConfProm.catch(() => {
|
||||
// Ignore it, it is handled by Lovelace panel.
|
||||
});
|
||||
llWindow.llResProm = fetchResources(conn);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export const demoConfig: HassConfig = {
|
||||
temperature: "°C",
|
||||
volume: "L",
|
||||
},
|
||||
components: ["notify.html5", "history"],
|
||||
components: ["notify.html5", "history", "shopping_list"],
|
||||
time_zone: "America/Los_Angeles",
|
||||
config_dir: "/config",
|
||||
version: "DEMO",
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
import {
|
||||
applyThemesOnElement,
|
||||
invalidateThemeCache,
|
||||
} from "../common/dom/apply_themes_on_element";
|
||||
|
||||
import { demoConfig } from "./demo_config";
|
||||
import { demoServices } from "./demo_services";
|
||||
@@ -8,6 +11,7 @@ import { HomeAssistant } from "../types";
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import { getLocalLanguage } from "../util/hass-translation";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import { DEFAULT_PANEL } from "../data/panel";
|
||||
|
||||
const ensureArray = <T>(val: T | T[]): T[] =>
|
||||
Array.isArray(val) ? val : [val];
|
||||
@@ -169,6 +173,7 @@ export const provideHass = (
|
||||
name: "Demo User",
|
||||
},
|
||||
panelUrl: "lovelace",
|
||||
defaultPanel: DEFAULT_PANEL,
|
||||
|
||||
language: localLanguage,
|
||||
selectedLanguage: localLanguage,
|
||||
@@ -224,6 +229,7 @@ export const provideHass = (
|
||||
(eventListeners[event] || []).forEach((fn) => fn(event));
|
||||
},
|
||||
mockTheme(theme) {
|
||||
invalidateThemeCache();
|
||||
hass().updateHass({
|
||||
selectedTheme: theme ? "mock" : "default",
|
||||
themes: {
|
||||
@@ -237,8 +243,7 @@ export const provideHass = (
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
themes,
|
||||
selectedTheme,
|
||||
true
|
||||
selectedTheme as string
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
.route=${this.route}
|
||||
.tabs=${this.tabs}
|
||||
>
|
||||
<div slot="toolbar-icon"><slot name="toolbar-icon"></slot></div>
|
||||
${this.narrow
|
||||
? html`
|
||||
<div slot="header">
|
||||
|
||||
@@ -37,12 +37,12 @@ class HassTabsSubpage extends LitElement {
|
||||
@property() public route!: Route;
|
||||
@property() public tabs!: PageNavigation[];
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
@property() private _activeTab: number = -1;
|
||||
@property() private _activeTab?: PageNavigation;
|
||||
|
||||
private _getTabs = memoizeOne(
|
||||
(
|
||||
tabs: PageNavigation[],
|
||||
activeTab: number,
|
||||
activeTab: PageNavigation | undefined,
|
||||
showAdvanced: boolean | undefined,
|
||||
_components,
|
||||
_language
|
||||
@@ -56,31 +56,32 @@ class HassTabsSubpage extends LitElement {
|
||||
);
|
||||
|
||||
return shownTabs.map(
|
||||
(page, index) => html`
|
||||
<div
|
||||
class="tab ${classMap({
|
||||
active: index === activeTab,
|
||||
})}"
|
||||
@click=${this._tabTapped}
|
||||
.path=${page.path}
|
||||
>
|
||||
${this.narrow
|
||||
? html`
|
||||
<ha-icon .icon=${page.icon}></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
${!this.narrow || index === activeTab
|
||||
? html`
|
||||
<span class="name"
|
||||
>${page.translationKey
|
||||
? this.hass.localize(page.translationKey)
|
||||
: name}</span
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
<mwc-ripple></mwc-ripple>
|
||||
</div>
|
||||
`
|
||||
(page) =>
|
||||
html`
|
||||
<div
|
||||
class="tab ${classMap({
|
||||
active: page === activeTab,
|
||||
})}"
|
||||
@click=${this._tabTapped}
|
||||
.path=${page.path}
|
||||
>
|
||||
${this.narrow
|
||||
? html`
|
||||
<ha-icon .icon=${page.icon}></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
${!this.narrow || page === activeTab
|
||||
? html`
|
||||
<span class="name"
|
||||
>${page.translationKey
|
||||
? this.hass.localize(page.translationKey)
|
||||
: name}</span
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
<mwc-ripple></mwc-ripple>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -88,7 +89,7 @@ class HassTabsSubpage extends LitElement {
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("route")) {
|
||||
this._activeTab = this.tabs.findIndex((tab) =>
|
||||
this._activeTab = this.tabs.find((tab) =>
|
||||
this.route.prefix.includes(tab.path)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import "./ha-init-page";
|
||||
import "../resources/ha-style";
|
||||
import "../resources/custom-card-support";
|
||||
import { registerServiceWorker } from "../util/register-service-worker";
|
||||
import { DEFAULT_PANEL } from "../common/const";
|
||||
|
||||
import { Route, HomeAssistant } from "../types";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { HassElement } from "../state/hass-element";
|
||||
import { getStorageDefaultPanelUrlPath } from "../data/panel";
|
||||
|
||||
export class HomeAssistantAppEl extends HassElement {
|
||||
@property() private _route?: Route;
|
||||
@@ -86,7 +86,7 @@ export class HomeAssistantAppEl extends HassElement {
|
||||
this._route === undefined &&
|
||||
(route.path === "" || route.path === "/")
|
||||
) {
|
||||
navigate(window, `/${localStorage.defaultPage || DEFAULT_PANEL}`, true);
|
||||
navigate(window, `/${getStorageDefaultPanelUrlPath()}`, true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -109,9 +109,9 @@ class DialogAreaDetail extends LitElement {
|
||||
name: this._name.trim(),
|
||||
};
|
||||
if (this._params!.entry) {
|
||||
await this._params!.updateEntry(values);
|
||||
await this._params!.updateEntry!(values);
|
||||
} else {
|
||||
await this._params!.createEntry(values);
|
||||
await this._params!.createEntry!(values);
|
||||
}
|
||||
this._params = undefined;
|
||||
} catch (err) {
|
||||
@@ -124,7 +124,7 @@ class DialogAreaDetail extends LitElement {
|
||||
private async _deleteEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.removeEntry()) {
|
||||
if (await this._params!.removeEntry!()) {
|
||||
this._params = undefined;
|
||||
}
|
||||
} finally {
|
||||
|
||||
397
src/panels/config/areas/ha-config-area-page.ts
Normal file
@@ -0,0 +1,397 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../components/dialog/ha-paper-dialog";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
updateAreaRegistryEntry,
|
||||
deleteAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
devicesInArea,
|
||||
computeDeviceName,
|
||||
} from "../../../data/device_registry";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import {
|
||||
showAreaRegistryDetailDialog,
|
||||
loadAreaRegistryDetailDialog,
|
||||
} from "./show-dialog-area-registry-detail";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { RelatedResult, findRelated } from "../../../data/search";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
@customElement("ha-config-area-page")
|
||||
class HaConfigAreaPage extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public areaId!: string;
|
||||
@property() public areas!: AreaRegistryEntry[];
|
||||
@property() public devices!: DeviceRegistryEntry[];
|
||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||
@property() public isWide!: boolean;
|
||||
@property() public showAdvanced!: boolean;
|
||||
@property() public route!: Route;
|
||||
@property() private _related?: RelatedResult;
|
||||
|
||||
private _area = memoizeOne((areaId: string, areas: AreaRegistryEntry[]):
|
||||
| AreaRegistryEntry
|
||||
| undefined => areas.find((area) => area.area_id === areaId));
|
||||
|
||||
private _devices = memoizeOne(
|
||||
(areaId: string, devices: DeviceRegistryEntry[]): DeviceRegistryEntry[] =>
|
||||
devicesInArea(devices, areaId)
|
||||
);
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
loadAreaRegistryDetailDialog();
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("areaId")) {
|
||||
this._findRelated();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const area = this._area(this.areaId, this.areas);
|
||||
|
||||
if (!area) {
|
||||
return html`
|
||||
<hass-error-screen
|
||||
error="${this.hass.localize("ui.panel.config.areas.area_not_found")}"
|
||||
></hass-error-screen>
|
||||
`;
|
||||
}
|
||||
|
||||
const devices = this._devices(this.areaId, this.devices);
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.tabs=${configSections.integrations}
|
||||
.route=${this.route}
|
||||
>
|
||||
${this.narrow
|
||||
? html`
|
||||
<span slot="header">
|
||||
${area.name}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<paper-icon-button
|
||||
slot="toolbar-icon"
|
||||
icon="hass:settings"
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
></paper-icon-button>
|
||||
|
||||
<div class="container">
|
||||
${!this.narrow
|
||||
? html`
|
||||
<div class="fullwidth">
|
||||
<h1>${area.name}</h1>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="column">
|
||||
<ha-card
|
||||
.header=${this.hass.localize("ui.panel.config.devices.caption")}
|
||||
>${devices.length
|
||||
? devices.map(
|
||||
(device) =>
|
||||
html`
|
||||
<a href="/config/devices/device/${device.id}">
|
||||
<paper-item>
|
||||
<paper-item-body>
|
||||
${computeDeviceName(device, this.hass)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
`
|
||||
)
|
||||
: html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.no_devices"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
</ha-card>
|
||||
</div>
|
||||
<div class="column">
|
||||
${isComponentLoaded(this.hass, "automation")
|
||||
? html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations"
|
||||
)}
|
||||
>${this._related?.automation?.length
|
||||
? this._related.automation.map((automation) => {
|
||||
const state = this.hass.states[automation];
|
||||
return state
|
||||
? html`
|
||||
<div>
|
||||
<a
|
||||
href=${ifDefined(
|
||||
state.attributes.id
|
||||
? `/config/automation/edit/${state.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-item
|
||||
.disabled=${!state.attributes.id}
|
||||
>
|
||||
<paper-item-body>
|
||||
${computeStateName(state)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
${!state.attributes.id
|
||||
? html`
|
||||
<paper-tooltip
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.cant_edit"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: "";
|
||||
})
|
||||
: html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.no_automations"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="column">
|
||||
${isComponentLoaded(this.hass, "scene")
|
||||
? html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes"
|
||||
)}
|
||||
>${this._related?.scene?.length
|
||||
? this._related.scene.map((scene) => {
|
||||
const state = this.hass.states[scene];
|
||||
return state
|
||||
? html`
|
||||
<div>
|
||||
<a
|
||||
href=${ifDefined(
|
||||
state.attributes.id
|
||||
? `/config/scene/edit/${state.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-item
|
||||
.disabled=${!state.attributes.id}
|
||||
>
|
||||
<paper-item-body>
|
||||
${computeStateName(state)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
${!state.attributes.id
|
||||
? html`
|
||||
<paper-tooltip
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.cant_edit"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: "";
|
||||
})
|
||||
: html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.no_scenes"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
${isComponentLoaded(this.hass, "script")
|
||||
? html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts"
|
||||
)}
|
||||
>${this._related?.script?.length
|
||||
? this._related.script.map((script) => {
|
||||
const state = this.hass.states[script];
|
||||
return state
|
||||
? html`
|
||||
<a
|
||||
href=${ifDefined(
|
||||
state.attributes.id
|
||||
? `/config/script/edit/${state.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-item>
|
||||
<paper-item-body>
|
||||
${computeStateName(state)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
`
|
||||
: "";
|
||||
})
|
||||
: html`
|
||||
<paper-item class="no-link">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.script.no_scripts"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _findRelated() {
|
||||
this._related = await findRelated(this.hass, "area", this.areaId);
|
||||
}
|
||||
|
||||
private _showSettings(ev: MouseEvent) {
|
||||
const entry: AreaRegistryEntry = (ev.currentTarget! as any).entry;
|
||||
this._openDialog(entry);
|
||||
}
|
||||
|
||||
private _openDialog(entry?: AreaRegistryEntry) {
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
entry,
|
||||
updateEntry: async (values) =>
|
||||
updateAreaRegistryEntry(this.hass!, entry!.area_id, values),
|
||||
removeEntry: async () => {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.delete.confirmation_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.areas.delete.confirmation_text"
|
||||
),
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
confirmText: this.hass.localize("ui.common.yes"),
|
||||
}))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteAreaRegistryEntry(this.hass!, entry!.area_id);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-family: var(--paper-font-headline_-_font-family);
|
||||
-webkit-font-smoothing: var(
|
||||
--paper-font-headline_-_-webkit-font-smoothing
|
||||
);
|
||||
font-size: var(--paper-font-headline_-_font-size);
|
||||
font-weight: var(--paper-font-headline_-_font-weight);
|
||||
letter-spacing: var(--paper-font-headline_-_letter-spacing);
|
||||
line-height: var(--paper-font-headline_-_line-height);
|
||||
opacity: var(--dark-primary-opacity);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: auto;
|
||||
max-width: 1000px;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.column {
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
width: 33%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.fullwidth {
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.column > *:not(:first-child) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:host([narrow]) .column {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host([narrow]) .container {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
paper-item.no-link {
|
||||
cursor: default;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-area-page": HaConfigAreaPage;
|
||||
}
|
||||
}
|
||||
200
src/panels/config/areas/ha-config-areas-dashboard.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
css,
|
||||
CSSResult,
|
||||
property,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
createAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import "../ha-config-section";
|
||||
import {
|
||||
showAreaRegistryDetailDialog,
|
||||
loadAreaRegistryDetailDialog,
|
||||
} from "./show-dialog-area-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import {
|
||||
devicesInArea,
|
||||
DeviceRegistryEntry,
|
||||
} from "../../../data/device_registry";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
@customElement("ha-config-areas-dashboard")
|
||||
export class HaConfigAreasDashboard extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public isWide?: boolean;
|
||||
@property() public narrow!: boolean;
|
||||
@property() public route!: Route;
|
||||
@property() public areas!: AreaRegistryEntry[];
|
||||
@property() public devices!: DeviceRegistryEntry[];
|
||||
|
||||
private _areas = memoizeOne(
|
||||
(areas: AreaRegistryEntry[], devices: DeviceRegistryEntry[]) => {
|
||||
return areas.map((area) => {
|
||||
return {
|
||||
...area,
|
||||
devices: devicesInArea(devices, area.area_id).length,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer =>
|
||||
narrow
|
||||
? {
|
||||
name: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.data_table.area"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.data_table.area"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
},
|
||||
devices: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.data_table.devices"
|
||||
),
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.integrations}
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.data=${this._areas(this.areas, this.devices)}
|
||||
@row-click=${this._handleRowClicked}
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.no_areas"
|
||||
)}
|
||||
id="area_id"
|
||||
>
|
||||
<paper-icon-button
|
||||
slot="toolbar-icon"
|
||||
icon="hass:help-circle"
|
||||
@click=${this._showHelp}
|
||||
></paper-icon-button>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-fab
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
icon="hass:plus"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.create_area"
|
||||
)}"
|
||||
@click=${this._createArea}
|
||||
></ha-fab>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
loadAreaRegistryDetailDialog();
|
||||
}
|
||||
|
||||
private _createArea() {
|
||||
this._openDialog();
|
||||
}
|
||||
|
||||
private _showHelp() {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.areas.caption"),
|
||||
text: html`
|
||||
${this.hass.localize("ui.panel.config.areas.picker.introduction")}
|
||||
<p>
|
||||
${this.hass.localize("ui.panel.config.areas.picker.introduction2")}
|
||||
</p>
|
||||
<a href="/config/integrations/dashboard">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.integrations_page"
|
||||
)}
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const areaId = ev.detail.id;
|
||||
navigate(this, `/config/areas/area/${areaId}`);
|
||||
}
|
||||
|
||||
private _openDialog(entry?: AreaRegistryEntry) {
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
entry,
|
||||
createEntry: async (values) =>
|
||||
createAreaRegistryEntry(this.hass!, values),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
hass-loading-screen {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
ha-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
ha-fab[narrow] {
|
||||
bottom: 84px;
|
||||
}
|
||||
ha-fab.rtl {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
ha-fab[is-wide].rtl {
|
||||
bottom: 24px;
|
||||
right: auto;
|
||||
left: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,226 +1,120 @@
|
||||
import "./ha-config-areas-dashboard";
|
||||
import "./ha-config-area-page";
|
||||
import { compare } from "../../../common/string/compare";
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
css,
|
||||
CSSResult,
|
||||
property,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
updateAreaRegistryEntry,
|
||||
deleteAreaRegistryEntry,
|
||||
createAreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
AreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../ha-config-section";
|
||||
import {
|
||||
showAreaRegistryDetailDialog,
|
||||
loadAreaRegistryDetailDialog,
|
||||
} from "./show-dialog-area-registry-detail";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../../layouts/hass-router-page";
|
||||
import { property, customElement, PropertyValues } from "lit-element";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../data/device_registry";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
@customElement("ha-config-areas")
|
||||
export class HaConfigAreas extends LitElement {
|
||||
class HaConfigAreas extends HassRouterPage {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public isWide?: boolean;
|
||||
@property() public narrow!: boolean;
|
||||
@property() public route!: Route;
|
||||
@property() private _areas?: AreaRegistryEntry[];
|
||||
private _unsubAreas?: UnsubscribeFunc;
|
||||
@property() public isWide!: boolean;
|
||||
@property() public showAdvanced!: boolean;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboard",
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "ha-config-areas-dashboard",
|
||||
cache: true,
|
||||
},
|
||||
area: {
|
||||
tag: "ha-config-area-page",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@property() private _configEntries: ConfigEntry[] = [];
|
||||
@property() private _deviceRegistryEntries: DeviceRegistryEntry[] = [];
|
||||
@property() private _areas: AreaRegistryEntry[] = [];
|
||||
|
||||
private _unsubs?: UnsubscribeFunc[];
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsubAreas) {
|
||||
this._unsubAreas();
|
||||
if (this._unsubs) {
|
||||
while (this._unsubs.length) {
|
||||
this._unsubs.pop()!();
|
||||
}
|
||||
this._unsubs = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || this._areas === undefined) {
|
||||
return html`
|
||||
<hass-loading-screen></hass-loading-screen>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.integrations}
|
||||
>
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="header">
|
||||
${this.hass.localize("ui.panel.config.areas.picker.header")}
|
||||
</span>
|
||||
<span slot="introduction">
|
||||
${this.hass.localize("ui.panel.config.areas.picker.introduction")}
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.introduction2"
|
||||
)}
|
||||
</p>
|
||||
<a href="/config/integrations/dashboard">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.integrations_page"
|
||||
)}
|
||||
</a>
|
||||
</span>
|
||||
<ha-card>
|
||||
${this._areas.map((entry) => {
|
||||
return html`
|
||||
<paper-item @click=${this._openEditEntry} .entry=${entry}>
|
||||
<paper-item-body>
|
||||
${entry.name}
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
`;
|
||||
})}
|
||||
${this._areas.length === 0
|
||||
? html`
|
||||
<div class="empty">
|
||||
${this.hass.localize("ui.panel.config.areas.no_areas")}
|
||||
<mwc-button @click=${this._createArea}>
|
||||
${this.hass.localize("ui.panel.config.areas.create_area")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
</hass-tabs-subpage>
|
||||
|
||||
<ha-fab
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
icon="hass:plus"
|
||||
title="${this.hass.localize("ui.panel.config.areas.create_area")}"
|
||||
@click=${this._createArea}
|
||||
class="${classMap({
|
||||
rtl: computeRTL(this.hass),
|
||||
})}"
|
||||
></ha-fab>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
loadAreaRegistryDetailDialog();
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
if (!this._unsubAreas) {
|
||||
this._unsubAreas = subscribeAreaRegistry(
|
||||
this.hass.connection,
|
||||
(areas) => {
|
||||
this._areas = areas;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _createArea() {
|
||||
this._openDialog();
|
||||
}
|
||||
|
||||
private _openEditEntry(ev: MouseEvent) {
|
||||
const entry: AreaRegistryEntry = (ev.currentTarget! as any).entry;
|
||||
this._openDialog(entry);
|
||||
}
|
||||
private _openDialog(entry?: AreaRegistryEntry) {
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
entry,
|
||||
createEntry: async (values) =>
|
||||
createAreaRegistryEntry(this.hass!, values),
|
||||
updateEntry: async (values) =>
|
||||
updateAreaRegistryEntry(this.hass!, entry!.area_id, values),
|
||||
removeEntry: async () => {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.delete.confirmation_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.areas.delete.confirmation_text"
|
||||
),
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
confirmText: this.hass.localize("ui.common.yes"),
|
||||
}))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteAreaRegistryEntry(this.hass!, entry!.area_id);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
this.addEventListener("hass-reload-entries", () => {
|
||||
this._loadData();
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
hass-loading-screen {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 16px auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
.empty {
|
||||
text-align: center;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (!this._unsubs && changedProps.has("hass")) {
|
||||
this._loadData();
|
||||
}
|
||||
}
|
||||
|
||||
ha-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
ha-fab[narrow] {
|
||||
bottom: 84px;
|
||||
}
|
||||
ha-fab.rtl {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
protected updatePageEl(pageEl) {
|
||||
pageEl.hass = this.hass;
|
||||
|
||||
ha-fab[is-wide].rtl {
|
||||
bottom: 24px;
|
||||
right: auto;
|
||||
left: 24px;
|
||||
}
|
||||
`;
|
||||
if (this._currentPage === "area") {
|
||||
pageEl.areaId = this.routeTail.path.substr(1);
|
||||
}
|
||||
|
||||
pageEl.entries = this._configEntries;
|
||||
pageEl.devices = this._deviceRegistryEntries;
|
||||
pageEl.areas = this._areas;
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.isWide = this.isWide;
|
||||
pageEl.showAdvanced = this.showAdvanced;
|
||||
pageEl.route = this.routeTail;
|
||||
}
|
||||
|
||||
private _loadData() {
|
||||
getConfigEntries(this.hass).then((configEntries) => {
|
||||
this._configEntries = configEntries.sort((conf1, conf2) =>
|
||||
compare(conf1.title, conf2.title)
|
||||
);
|
||||
});
|
||||
if (this._unsubs) {
|
||||
return;
|
||||
}
|
||||
this._unsubs = [
|
||||
subscribeAreaRegistry(this.hass.connection, (areas) => {
|
||||
this._areas = areas;
|
||||
}),
|
||||
subscribeDeviceRegistry(this.hass.connection, (entries) => {
|
||||
this._deviceRegistryEntries = entries;
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-areas": HaConfigAreas;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
|
||||
export interface AreaRegistryDetailDialogParams {
|
||||
entry?: AreaRegistryEntry;
|
||||
createEntry: (values: AreaRegistryEntryMutableParams) => Promise<unknown>;
|
||||
updateEntry: (
|
||||
createEntry?: (values: AreaRegistryEntryMutableParams) => Promise<unknown>;
|
||||
updateEntry?: (
|
||||
updates: Partial<AreaRegistryEntryMutableParams>
|
||||
) => Promise<unknown>;
|
||||
removeEntry: () => Promise<boolean>;
|
||||
removeEntry?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const loadAreaRegistryDetailDialog = () =>
|
||||
|
||||