mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-12 18:59:26 +00:00
Compare commits
129 Commits
Better-row
...
multiple-a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e87accdfb4 | ||
![]() |
4dcfe3031d | ||
![]() |
1538fbb102 | ||
![]() |
b9259b87eb | ||
![]() |
30997dbc88 | ||
![]() |
607eb6d130 | ||
![]() |
f2e9b3577d | ||
![]() |
cb2c6d8560 | ||
![]() |
bfe8346ced | ||
![]() |
88da9bb91b | ||
![]() |
5e2ee1a16c | ||
![]() |
2fdc746392 | ||
![]() |
1667973a66 | ||
![]() |
42f0101440 | ||
![]() |
13b69bff1b | ||
![]() |
2c2226dfd6 | ||
![]() |
a3fdfe0e15 | ||
![]() |
a0de209a55 | ||
![]() |
0208b50ac7 | ||
![]() |
4a61779aba | ||
![]() |
05057ade05 | ||
![]() |
6fb206853c | ||
![]() |
fab68055bf | ||
![]() |
d5a77ef3cd | ||
![]() |
dcb2605de4 | ||
![]() |
e6d38f4539 | ||
![]() |
293b56cfa6 | ||
![]() |
775e93d54b | ||
![]() |
7f840e75df | ||
![]() |
2113ea675e | ||
![]() |
916a5c1a6b | ||
![]() |
f684531315 | ||
![]() |
fe8dda8996 | ||
![]() |
4cd4b328c8 | ||
![]() |
d844c89b94 | ||
![]() |
177ea2b85a | ||
![]() |
50c5c15f49 | ||
![]() |
1810760dc7 | ||
![]() |
4635b92e3f | ||
![]() |
1c652626eb | ||
![]() |
2000cfb1db | ||
![]() |
f4d07828e7 | ||
![]() |
95b552671c | ||
![]() |
ef3bc3efe1 | ||
![]() |
371ad899f5 | ||
![]() |
2c54158d84 | ||
![]() |
5d9e30bbdc | ||
![]() |
e477fd567d | ||
![]() |
6a6c2937fe | ||
![]() |
09e17c4da8 | ||
![]() |
fd00469d11 | ||
![]() |
cbbeb795f3 | ||
![]() |
bba40e0da8 | ||
![]() |
d23165d06a | ||
![]() |
405fef6f03 | ||
![]() |
588f217826 | ||
![]() |
3d8b7cf80e | ||
![]() |
c0ef923ad3 | ||
![]() |
3df44fc71e | ||
![]() |
c1965492d9 | ||
![]() |
1f56ffde80 | ||
![]() |
f335fdc002 | ||
![]() |
0c914b5ec8 | ||
![]() |
d767b06858 | ||
![]() |
d4e49f3944 | ||
![]() |
7dfc5b3faf | ||
![]() |
8a88033ab9 | ||
![]() |
7b06b38c94 | ||
![]() |
5409752817 | ||
![]() |
909f3a3005 | ||
![]() |
4930532c7b | ||
![]() |
8a42e65c6a | ||
![]() |
5d4121a9b4 | ||
![]() |
a70e6c49a1 | ||
![]() |
3d83d5f4b5 | ||
![]() |
f9dece0743 | ||
![]() |
ac0871d0e8 | ||
![]() |
ffc19e591d | ||
![]() |
c53380ca3d | ||
![]() |
7c74a2026a | ||
![]() |
adaed438d9 | ||
![]() |
baf38305cb | ||
![]() |
8254712521 | ||
![]() |
53214781e3 | ||
![]() |
88cbbbdf65 | ||
![]() |
c10dca9c7b | ||
![]() |
7f2ebb4bde | ||
![]() |
f1abb60e4a | ||
![]() |
e014c7aff6 | ||
![]() |
b79c03433e | ||
![]() |
34eb4d974d | ||
![]() |
3264be3c5e | ||
![]() |
655f4f75fb | ||
![]() |
4383f31696 | ||
![]() |
99eb15d15e | ||
![]() |
2682c6e150 | ||
![]() |
3a5d854e6d | ||
![]() |
1e90c6387c | ||
![]() |
2cca25f4d0 | ||
![]() |
565724d201 | ||
![]() |
3e4955becd | ||
![]() |
7b560c727f | ||
![]() |
35abd9dfdb | ||
![]() |
b7ccf3e0e5 | ||
![]() |
0d9ab8fdd0 | ||
![]() |
303f9290a8 | ||
![]() |
e0c4dc08a1 | ||
![]() |
8c655883fe | ||
![]() |
ba90785115 | ||
![]() |
7b392b626b | ||
![]() |
8e4ceb7d48 | ||
![]() |
2ab1c6e9a9 | ||
![]() |
dbdced0971 | ||
![]() |
5e481880bd | ||
![]() |
faec063f34 | ||
![]() |
bbea38d227 | ||
![]() |
a0ef60de49 | ||
![]() |
3313572606 | ||
![]() |
c4f850cb14 | ||
![]() |
3bdab738c6 | ||
![]() |
faaef31b9f | ||
![]() |
ca7b8b8b4c | ||
![]() |
9ca84e0694 | ||
![]() |
daaf2b1796 | ||
![]() |
25f7cbea5a | ||
![]() |
c485ea9d7b | ||
![]() |
295390c8e9 | ||
![]() |
3ebf816ce2 | ||
![]() |
50e7410002 |
10
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
10
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
@@ -74,12 +74,12 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
|
||||
```
|
||||
|
||||
## Problem-relevant configuration
|
||||
## Problem-relevant frontend configuration
|
||||
|
||||
<!--
|
||||
An example configuration that caused the problem for you. Fill this out even
|
||||
if it seems unimportant to you. Please be sure to remove personal information
|
||||
like passwords, private URLs and other credentials.
|
||||
An example configuration that caused the problem for you, e.g. the YAML configuration
|
||||
of the used cards. Fill this out even if it seems unimportant to you. Please be sure
|
||||
to remove personal information like passwords, private URLs and other credentials.
|
||||
-->
|
||||
|
||||
```yaml
|
||||
@@ -89,7 +89,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
## Javascript errors shown in your browser console/inspector
|
||||
|
||||
<!--
|
||||
If you come across any javascript or other error logs, e.g., in your browser
|
||||
If you come across any Javascript or other error logs, e.g. in your browser
|
||||
console/inspector please provide them.
|
||||
-->
|
||||
|
||||
|
@@ -14,7 +14,7 @@ This is the repository for the official [Home Assistant](https://home-assistant.
|
||||
- Development: [Instructions](https://developers.home-assistant.io/docs/frontend/development/)
|
||||
- Production build: `script/build_frontend`
|
||||
- Gallery: `cd gallery && script/develop_gallery`
|
||||
- Hass.io: [Instructions](https://developers.home-assistant.io/docs/en/hassio_hass.html)
|
||||
- Supervisor: [Instructions](https://developers.home-assistant.io/docs/supervisor/developing)
|
||||
|
||||
## Frontend development
|
||||
|
||||
|
@@ -36,6 +36,7 @@ const createWebpackConfig = ({
|
||||
const ignorePackages = bundle.ignorePackages({ latestBuild });
|
||||
return {
|
||||
mode: isProdBuild ? "production" : "development",
|
||||
target: ["web", latestBuild ? "es2017" : "es5"],
|
||||
devtool: isProdBuild
|
||||
? "cheap-module-source-map"
|
||||
: "eval-cheap-module-source-map",
|
||||
@@ -131,22 +132,6 @@ const createWebpackConfig = ({
|
||||
}
|
||||
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
|
||||
},
|
||||
environment: {
|
||||
// The environment supports arrow functions ('() => { ... }').
|
||||
arrowFunction: latestBuild,
|
||||
// The environment supports BigInt as literal (123n).
|
||||
bigIntLiteral: false,
|
||||
// The environment supports const and let for variable declarations.
|
||||
const: latestBuild,
|
||||
// The environment supports destructuring ('{ a, b } = obj').
|
||||
destructuring: latestBuild,
|
||||
// The environment supports an async import() function to import EcmaScript modules.
|
||||
dynamicImport: latestBuild,
|
||||
// The environment supports 'for of' iteration ('for (const x of array) { ... }').
|
||||
forOf: latestBuild,
|
||||
// The environment supports ECMAScript Module syntax to import ECMAScript modules (import ... from '...').
|
||||
module: latestBuild,
|
||||
},
|
||||
chunkFilename:
|
||||
isProdBuild && !isStatsBuild
|
||||
? "chunk.[chunkhash].js"
|
||||
|
@@ -9,30 +9,29 @@ class DemoMoreInfo extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
#card {
|
||||
max-width: 400px;
|
||||
width: 100vw;
|
||||
}
|
||||
ha-card {
|
||||
width: 333px;
|
||||
width: 352px;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
state-card-content {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 400px;
|
||||
margin: 0 16px;
|
||||
overflow: auto;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
:host {
|
||||
.root {
|
||||
flex-direction: column;
|
||||
}
|
||||
pre {
|
||||
@@ -40,21 +39,25 @@ class DemoMoreInfo extends PolymerElement {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<ha-card>
|
||||
<state-card-content
|
||||
state-obj="[[_stateObj]]"
|
||||
hass="[[hass]]"
|
||||
in-dialog
|
||||
></state-card-content>
|
||||
<div class="root">
|
||||
<div id="card">
|
||||
<ha-card>
|
||||
<state-card-content
|
||||
state-obj="[[_stateObj]]"
|
||||
hass="[[hass]]"
|
||||
in-dialog
|
||||
></state-card-content>
|
||||
|
||||
<more-info-content
|
||||
hass="[[hass]]"
|
||||
state-obj="[[_stateObj]]"
|
||||
></more-info-content>
|
||||
</ha-card>
|
||||
<template is="dom-if" if="[[showConfig]]">
|
||||
<pre>[[_jsonEntity(_stateObj)]]</pre>
|
||||
</template>
|
||||
<more-info-content
|
||||
hass="[[hass]]"
|
||||
state-obj="[[_stateObj]]"
|
||||
></more-info-content>
|
||||
</ha-card>
|
||||
</div>
|
||||
<template is="dom-if" if="[[showConfig]]">
|
||||
<pre>[[_jsonEntity(_stateObj)]]</pre>
|
||||
</template>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -3,12 +3,18 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../../src/components/ha-switch";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "./demo-more-info";
|
||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||
|
||||
class DemoMoreInfos extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
#container {
|
||||
min-height: calc(100vh - 128px);
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -23,20 +29,31 @@ class DemoMoreInfos extends PolymerElement {
|
||||
.filters {
|
||||
margin-left: 60px;
|
||||
}
|
||||
ha-formfield {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
||||
<app-toolbar>
|
||||
<div class="filters">
|
||||
<ha-switch checked="{{_showConfig}}">Show entity</ha-switch>
|
||||
<ha-formfield label="Show entities">
|
||||
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Dark theme">
|
||||
<ha-switch on-change="_darkThemeToggled"> </ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
</app-toolbar>
|
||||
<div class="cards">
|
||||
<template is="dom-repeat" items="[[entities]]">
|
||||
<demo-more-info
|
||||
entity-id="[[item]]"
|
||||
show-config="[[_showConfig]]"
|
||||
hass="[[hass]]"
|
||||
></demo-more-info>
|
||||
</template>
|
||||
<div id="container">
|
||||
<div class="cards">
|
||||
<template is="dom-repeat" items="[[entities]]">
|
||||
<demo-more-info
|
||||
entity-id="[[item]]"
|
||||
show-config="[[_showConfig]]"
|
||||
hass="[[hass]]"
|
||||
></demo-more-info>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -51,6 +68,16 @@ class DemoMoreInfos extends PolymerElement {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_showConfigToggled(ev) {
|
||||
this._showConfig = ev.target.checked;
|
||||
}
|
||||
|
||||
_darkThemeToggled(ev) {
|
||||
applyThemesOnElement(this.$.container, { themes: {} }, "default", {
|
||||
dark: ev.target.checked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-more-infos", DemoMoreInfos);
|
||||
|
@@ -7,8 +7,8 @@ export const createMediaPlayerEntities = () => [
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
|
||||
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
|
||||
supported_features: 195135,
|
||||
// Select Source + Stop + Clear + Play + Shuffle Set
|
||||
supported_features: 64063,
|
||||
entity_picture: "/images/album_cover_2.jpg",
|
||||
media_duration: 300,
|
||||
media_position: 50,
|
||||
@@ -24,8 +24,8 @@ export const createMediaPlayerEntities = () => [
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
|
||||
// Select Source + Stop + Clear + Play + Shuffle Set
|
||||
supported_features: 64063,
|
||||
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
|
||||
supported_features: 195135,
|
||||
entity_picture: "/images/album_cover.jpg",
|
||||
media_duration: 300,
|
||||
media_position: 0,
|
||||
|
72
gallery/src/data/plants.ts
Normal file
72
gallery/src/data/plants.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
|
||||
export const createPlantEntities = () => [
|
||||
getEntity("plant", "lemon_tree", "ok", {
|
||||
problem: "none",
|
||||
sensors: {
|
||||
moisture: "sensor.lemon_tree_moisture",
|
||||
battery: "sensor.lemon_tree_battery",
|
||||
temperature: "sensor.lemon_tree_temperature",
|
||||
conductivity: "sensor.lemon_tree_conductivity",
|
||||
brightness: "sensor.lemon_tree_brightness",
|
||||
},
|
||||
unit_of_measurement_dict: {
|
||||
temperature: "°C",
|
||||
moisture: "%",
|
||||
brightness: "lx",
|
||||
battery: "%",
|
||||
conductivity: "μS/cm",
|
||||
},
|
||||
moisture: 54,
|
||||
battery: 95,
|
||||
temperature: 15.6,
|
||||
conductivity: 1,
|
||||
brightness: 12,
|
||||
max_brightness: 20,
|
||||
friendly_name: "Lemon Tree",
|
||||
}),
|
||||
getEntity("plant", "apple_tree", "ok", {
|
||||
problem: "brightness",
|
||||
sensors: {
|
||||
moisture: "sensor.apple_tree_moisture",
|
||||
battery: "sensor.apple_tree_battery",
|
||||
temperature: "sensor.apple_tree_temperature",
|
||||
conductivity: "sensor.apple_tree_conductivity",
|
||||
brightness: "sensor.apple_tree_brightness",
|
||||
},
|
||||
unit_of_measurement_dict: {
|
||||
temperature: "°C",
|
||||
moisture: "%",
|
||||
brightness: "lx",
|
||||
battery: "%",
|
||||
conductivity: "μS/cm",
|
||||
},
|
||||
moisture: 54,
|
||||
battery: 2,
|
||||
temperature: 15.6,
|
||||
conductivity: 1,
|
||||
brightness: 25,
|
||||
max_brightness: 20,
|
||||
friendly_name: "Apple Tree",
|
||||
}),
|
||||
getEntity("plant", "sunflowers", "ok", {
|
||||
problem: "moisture, temperature, conductivity",
|
||||
sensors: {
|
||||
moisture: "sensor.sunflowers_moisture",
|
||||
temperature: "sensor.sunflowers_temperature",
|
||||
conductivity: "sensor.sunflowers_conductivity",
|
||||
brightness: "sensor.sunflowers_brightness",
|
||||
},
|
||||
unit_of_measurement_dict: {
|
||||
temperature: "°C",
|
||||
moisture: "%",
|
||||
brightness: "lx",
|
||||
conductivity: "μS/cm",
|
||||
},
|
||||
moisture: 54,
|
||||
temperature: 15.6,
|
||||
conductivity: 1,
|
||||
brightness: 25,
|
||||
entity_picture: "/images/sunflowers.jpg",
|
||||
}),
|
||||
];
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -71,28 +76,19 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoAlarmPanelEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-alarm-panel-card")
|
||||
class DemoAlarmPanelEntity extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
this._setupDemo();
|
||||
}
|
||||
|
||||
private async _setupDemo() {
|
||||
const hass = provideHass(this.$.demos);
|
||||
await hass.updateTranslations(null, "en");
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -53,24 +58,19 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoConditional extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-conditional-card")
|
||||
class DemoConditional extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -217,24 +222,19 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoEntities extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-entities-card")
|
||||
class DemoEntities extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -69,24 +74,19 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoButtonEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-entity-button-card")
|
||||
class DemoButtonEntity extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -89,26 +94,21 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoFilter extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-entity-filter-card")
|
||||
class DemoEntityFilter extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-entity-filter-card", DemoFilter);
|
||||
customElements.define("demo-hui-entity-filter-card", DemoEntityFilter);
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -107,24 +112,19 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoGaugeEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-gauge-card")
|
||||
class DemoGaugeEntity extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -218,26 +223,21 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoPicEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-glance-card")
|
||||
class DemoGlanceEntity extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-glance-card", DemoPicEntity);
|
||||
customElements.define("demo-hui-glance-card", DemoGlanceEntity);
|
||||
|
@@ -1,6 +1,4 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { html, LitElement, customElement, TemplateResult } from "lit-element";
|
||||
import "../components/demo-cards";
|
||||
|
||||
const CONFIGS = [
|
||||
@@ -37,18 +35,10 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoIframe extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards configs="[[_configs]]"></demo-cards> `;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
@customElement("demo-hui-iframe-card")
|
||||
class DemoIframe extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -63,24 +68,19 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoLightEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-light-card")
|
||||
class DemoLightEntity extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -161,24 +166,19 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoMap extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-map-card")
|
||||
class DemoMap extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { mockTemplate } from "../../../demo/src/stubs/template";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -254,23 +259,19 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoMarkdown extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-markdown-card")
|
||||
class DemoMarkdown extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
mockTemplate(hass);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
import { createMediaPlayerEntities } from "../data/media_players";
|
||||
@@ -146,28 +151,33 @@ const CONFIGS = [
|
||||
entity: media_player.receiver_off
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Grid Full Size",
|
||||
config: `
|
||||
- type: grid
|
||||
columns: 1
|
||||
cards:
|
||||
- type: media-control
|
||||
entity: media_player.music_paused
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
class DemoHuiMediControlCard extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-media-control-card")
|
||||
class DemoHuiMediaControlCard extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(createMediaPlayerEntities());
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-media-control-card", DemoHuiMediControlCard);
|
||||
customElements.define("demo-hui-media-control-card", DemoHuiMediaControlCard);
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
import { createMediaPlayerEntities } from "../data/media_players";
|
||||
@@ -55,26 +60,21 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoHuiMediaPlayerRows extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-media-player-row")
|
||||
class DemoHuiMediaPlayerRow extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(createMediaPlayerEntities());
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-media-player-rows", DemoHuiMediaPlayerRows);
|
||||
customElements.define("demo-hui-media-player-row", DemoHuiMediaPlayerRow);
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -125,26 +130,21 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoPicElements extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-picture-elements-card")
|
||||
class DemoPictureElements extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-picture-elements-card", DemoPicElements);
|
||||
customElements.define("demo-hui-picture-elements-card", DemoPictureElements);
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -80,26 +85,21 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoPicEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-picture-entity-card")
|
||||
class DemoPictureEntity extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-picture-entity-card", DemoPicEntity);
|
||||
customElements.define("demo-hui-picture-entity-card", DemoPictureEntity);
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -121,26 +126,21 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoPicGlance extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-picture-glance-card")
|
||||
class DemoPictureGlance extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-picture-glance-card", DemoPicGlance);
|
||||
customElements.define("demo-hui-picture-glance-card", DemoPictureGlance);
|
||||
|
55
gallery/src/demos/demo-hui-plant-card.ts
Normal file
55
gallery/src/demos/demo-hui-plant-card.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
import { createPlantEntities } from "../data/plants";
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
heading: "Basic example",
|
||||
config: `
|
||||
- type: plant-status
|
||||
entity: plant.lemon_tree
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Problem (too bright) + low battery",
|
||||
config: `
|
||||
- type: plant-status
|
||||
entity: plant.apple_tree
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "With picture + multiple problems",
|
||||
config: `
|
||||
- type: plant-status
|
||||
entity: plant.sunflowers
|
||||
name: Sunflowers Name Overwrite
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-hui-plant-card")
|
||||
export class DemoPlantEntity extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(createPlantEntities());
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-plant-card", DemoPlantEntity);
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
|
||||
@@ -20,24 +25,19 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoShoppingListEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-shopping-list-card")
|
||||
class DemoShoppingListEntity extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
|
||||
hass.mockAPI("shopping_list", () => [
|
||||
{ name: "list", id: 1, complete: false },
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { mockHistory } from "../../../demo/src/stubs/history";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
@@ -132,24 +137,19 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoStack extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-stack-card")
|
||||
class DemoStack extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
mockHistory(hass);
|
||||
}
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -74,24 +79,19 @@ const CONFIGS = [
|
||||
},
|
||||
];
|
||||
|
||||
class DemoThermostatEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
@customElement("demo-hui-thermostat-card")
|
||||
class DemoThermostatEntity extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,27 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
customElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/ha-card";
|
||||
import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
|
||||
import {
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR_TEMP,
|
||||
SUPPORT_EFFECT,
|
||||
SUPPORT_FLASH,
|
||||
SUPPORT_COLOR,
|
||||
SUPPORT_TRANSITION,
|
||||
SUPPORT_WHITE_VALUE,
|
||||
} from "../../../src/data/light";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import {
|
||||
provideHass,
|
||||
MockHomeAssistant,
|
||||
} from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-more-infos";
|
||||
import "../../../src/dialogs/more-info/more-info-content";
|
||||
|
||||
@@ -14,38 +31,52 @@ const ENTITIES = [
|
||||
}),
|
||||
getEntity("light", "kitchen_light", "on", {
|
||||
friendly_name: "Brightness Light",
|
||||
brightness: 80,
|
||||
brightness: 200,
|
||||
supported_features: SUPPORT_BRIGHTNESS,
|
||||
}),
|
||||
getEntity("light", "color_temperature_light", "on", {
|
||||
friendly_name: "White Color Temperature Light",
|
||||
brightness: 128,
|
||||
color_temp: 75,
|
||||
min_mireds: 30,
|
||||
max_mireds: 150,
|
||||
supported_features: SUPPORT_BRIGHTNESS + SUPPORT_COLOR_TEMP,
|
||||
}),
|
||||
getEntity("light", "color_effectslight", "on", {
|
||||
friendly_name: "Color Effets Light",
|
||||
brightness: 255,
|
||||
hs_color: [30, 100],
|
||||
white_value: 36,
|
||||
supported_features:
|
||||
SUPPORT_BRIGHTNESS +
|
||||
SUPPORT_EFFECT +
|
||||
SUPPORT_FLASH +
|
||||
SUPPORT_COLOR +
|
||||
SUPPORT_TRANSITION +
|
||||
SUPPORT_WHITE_VALUE,
|
||||
effect_list: ["random", "colorloop"],
|
||||
}),
|
||||
];
|
||||
|
||||
class DemoMoreInfoLight extends PolymerElement {
|
||||
static get template() {
|
||||
@customElement("demo-more-info-light")
|
||||
class DemoMoreInfoLight extends LitElement {
|
||||
@property() public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
hass="[[hass]]"
|
||||
entities="[[_entities]]"
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_entities: {
|
||||
type: Array,
|
||||
value: ENTITIES.map((ent) => ent.entityId),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
this._setupDemo();
|
||||
}
|
||||
|
||||
private async _setupDemo() {
|
||||
const hass = provideHass(this);
|
||||
await hass.updateTranslations(null, "en");
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import "@material/mwc-button";
|
||||
import { html, LitElement, TemplateResult } from "lit-element";
|
||||
import { customElement, html, LitElement, TemplateResult } from "lit-element";
|
||||
import "../../../src/components/ha-card";
|
||||
import { ActionHandlerEvent } from "../../../src/data/lovelace";
|
||||
import { actionHandler } from "../../../src/panels/lovelace/common/directives/action-handler-directive";
|
||||
|
||||
@customElement("demo-util-long-press")
|
||||
export class DemoUtilLongPress extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
@@ -20,7 +21,7 @@ export class DemoUtilLongPress extends LitElement {
|
||||
|
||||
<textarea></textarea>
|
||||
|
||||
<div>(try pressing and scrolling too!)</div>
|
||||
<div>Try pressing and scrolling too!</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
@@ -62,5 +63,3 @@ export class DemoUtilLongPress extends LitElement {
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-util-long-press", DemoUtilLongPress);
|
||||
|
@@ -14,8 +14,6 @@ import "../../src/styles/polymer-ha-style";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { DEMOS } from "../build/import-demos";
|
||||
|
||||
const fixPath = (path) => path.substr(2, path.length - 5);
|
||||
|
||||
class HaGallery extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
|
@@ -69,7 +69,7 @@ const STAGE_ICON = {
|
||||
const PERMIS_DESC = {
|
||||
stage: {
|
||||
title: "Add-on Stage",
|
||||
description: `Add-ons can have one of three stages:\n\n<ha-svg-icon .path='${STAGE_ICON.stable}'></ha-svg-icon> **Stable**: These are add-ons ready to be used in production.\n\n<ha-svg-icon .path='${STAGE_ICON.experimental}'></ha-svg-icon> **Experimental**: These may contain bugs, and may be unfinished.\n\n<ha-svg-icon .path='${STAGE_ICON.deprecated}'></ha-svg-icon> **Deprecated**: These add-ons will no longer receive any updates.`,
|
||||
description: `Add-ons can have one of three stages:\n\n<ha-svg-icon path="${STAGE_ICON.stable}"></ha-svg-icon> **Stable**: These are add-ons ready to be used in production.\n\n<ha-svg-icon path="${STAGE_ICON.experimental}"></ha-svg-icon> **Experimental**: These may contain bugs, and may be unfinished.\n\n<ha-svg-icon path="${STAGE_ICON.deprecated}"></ha-svg-icon> **Deprecated**: These add-ons will no longer receive any updates.`,
|
||||
},
|
||||
rating: {
|
||||
title: "Add-on Security Rating",
|
||||
|
@@ -74,9 +74,7 @@ export class HassioUpdate extends LitElement {
|
||||
"Supervisor",
|
||||
this.supervisor.supervisor,
|
||||
"hassio/supervisor/update",
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${
|
||||
this.supervisor.supervisor.version_latest
|
||||
}`
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
|
||||
)}
|
||||
${this.supervisor.host.features.includes("hassos")
|
||||
? this._renderUpdateCard(
|
||||
|
@@ -178,7 +178,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
</div>`}
|
||||
${!this.supervisor.supervisor.healthy
|
||||
? html`<div class="error">
|
||||
Your installtion is running in an unhealthy state.
|
||||
Your installation is running in an unhealthy state.
|
||||
<button
|
||||
class="link"
|
||||
title="Learn more about why your system is marked as unhealthy"
|
||||
|
32
package.json
32
package.json
@@ -29,22 +29,22 @@
|
||||
"@fullcalendar/daygrid": "5.1.0",
|
||||
"@fullcalendar/interaction": "5.1.0",
|
||||
"@fullcalendar/list": "5.1.0",
|
||||
"@material/chips": "=8.0.0-canary.774dcfc8e.0",
|
||||
"@material/mwc-button": "^0.19.0",
|
||||
"@material/mwc-checkbox": "^0.19.0",
|
||||
"@material/mwc-circular-progress": "^0.19.0",
|
||||
"@material/mwc-dialog": "^0.19.0",
|
||||
"@material/mwc-fab": "^0.19.0",
|
||||
"@material/mwc-formfield": "^0.19.0",
|
||||
"@material/mwc-icon-button": "^0.19.0",
|
||||
"@material/mwc-list": "^0.19.0",
|
||||
"@material/mwc-menu": "^0.19.0",
|
||||
"@material/mwc-radio": "^0.19.0",
|
||||
"@material/mwc-ripple": "^0.19.0",
|
||||
"@material/mwc-switch": "^0.19.0",
|
||||
"@material/mwc-tab": "^0.19.0",
|
||||
"@material/mwc-tab-bar": "^0.19.0",
|
||||
"@material/top-app-bar": "=8.0.0-canary.774dcfc8e.0",
|
||||
"@material/chips": "=9.0.0-canary.1c156d69d.0",
|
||||
"@material/mwc-button": "^0.20.0",
|
||||
"@material/mwc-checkbox": "^0.20.0",
|
||||
"@material/mwc-circular-progress": "^0.20.0",
|
||||
"@material/mwc-dialog": "^0.20.0",
|
||||
"@material/mwc-fab": "^0.20.0",
|
||||
"@material/mwc-formfield": "^0.20.0",
|
||||
"@material/mwc-icon-button": "^0.20.0",
|
||||
"@material/mwc-list": "^0.20.0",
|
||||
"@material/mwc-menu": "^0.20.0",
|
||||
"@material/mwc-radio": "^0.20.0",
|
||||
"@material/mwc-ripple": "^0.20.0",
|
||||
"@material/mwc-switch": "^0.20.0",
|
||||
"@material/mwc-tab": "^0.20.0",
|
||||
"@material/mwc-tab-bar": "^0.20.0",
|
||||
"@material/top-app-bar": "=9.0.0-canary.1c156d69d.0",
|
||||
"@mdi/js": "5.6.55",
|
||||
"@mdi/svg": "5.6.55",
|
||||
"@polymer/app-layout": "^3.0.2",
|
||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20201126.0",
|
||||
version="20201229.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
6
src/common/ensure-array.ts
Normal file
6
src/common/ensure-array.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const ensureArray = (value?: any) => {
|
||||
if (!value || Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
return [value];
|
||||
};
|
@@ -67,6 +67,10 @@ export const computeStateDisplay = (
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === "counter") {
|
||||
return formatNumber(compareState, language);
|
||||
}
|
||||
|
||||
return (
|
||||
// Return device class translation
|
||||
(stateObj.attributes.device_class &&
|
||||
|
@@ -1,4 +1,13 @@
|
||||
export const copyToClipboard = (str) => {
|
||||
export const copyToClipboard = async (str) => {
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(str);
|
||||
return;
|
||||
} catch {
|
||||
// just continue with the fallback coding below
|
||||
}
|
||||
}
|
||||
|
||||
const el = document.createElement("textarea");
|
||||
el.value = str;
|
||||
document.body.appendChild(el);
|
||||
|
@@ -98,6 +98,12 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public hasFab = false;
|
||||
|
||||
/**
|
||||
* Add an extra row at the bottom of the data table
|
||||
* @type {TemplateResult}
|
||||
*/
|
||||
@property({ attribute: false }) public appendRow?;
|
||||
|
||||
@property({ type: Boolean, attribute: "auto-height" })
|
||||
public autoHeight = false;
|
||||
|
||||
@@ -126,6 +132,8 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@query("slot[name='header']") private _header!: HTMLSlotElement;
|
||||
|
||||
private _items: DataTableRowData[] = [];
|
||||
|
||||
private _checkableRowsCount?: number;
|
||||
|
||||
private _checkedRows: string[] = [];
|
||||
@@ -318,10 +326,13 @@ export class HaDataTable extends LitElement {
|
||||
@scroll=${this._saveScrollPos}
|
||||
>
|
||||
${scroll({
|
||||
items: !this.hasFab
|
||||
? this._filteredData
|
||||
: [...this._filteredData, ...[{ empty: true }]],
|
||||
items: this._items,
|
||||
renderItem: (row: DataTableRowData, index) => {
|
||||
if (row.append) {
|
||||
return html`
|
||||
<div class="mdc-data-table__row">${row.content}</div>
|
||||
`;
|
||||
}
|
||||
if (row.empty) {
|
||||
return html` <div class="mdc-data-table__row"></div> `;
|
||||
}
|
||||
@@ -447,6 +458,20 @@ export class HaDataTable extends LitElement {
|
||||
if (this.curRequest !== curRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.appendRow || this.hasFab) {
|
||||
this._items = [...data];
|
||||
|
||||
if (this.appendRow) {
|
||||
this._items.push({ append: true, content: this.appendRow });
|
||||
}
|
||||
|
||||
if (this.hasFab) {
|
||||
this._items.push({ empty: true });
|
||||
}
|
||||
} else {
|
||||
this._items = data;
|
||||
}
|
||||
this._filteredData = data;
|
||||
}
|
||||
|
||||
|
@@ -58,6 +58,14 @@ const sortData = (
|
||||
valB = valB.toUpperCase();
|
||||
}
|
||||
|
||||
// Ensure "undefined" is always sorted to the bottom
|
||||
if (valA === undefined && valB !== undefined) {
|
||||
return 1;
|
||||
}
|
||||
if (valB === undefined && valA !== undefined) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (valA < valB) {
|
||||
return sort * -1;
|
||||
}
|
||||
|
@@ -139,7 +139,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _filteredDevices: DeviceRegistryEntry[] = [];
|
||||
|
||||
private _getDevices = memoizeOne(
|
||||
private _getAreasWithDevices = memoizeOne(
|
||||
(
|
||||
devices: DeviceRegistryEntry[],
|
||||
areas: AreaRegistryEntry[],
|
||||
@@ -277,7 +277,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
if (!this._devices || !this._areas || !this._entities) {
|
||||
return html``;
|
||||
}
|
||||
const areas = this._getDevices(
|
||||
const areas = this._getAreasWithDevices(
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._entities,
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
@@ -12,6 +13,8 @@ import {
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -35,6 +38,7 @@ import {
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { mdiClose, mdiMenuUp, mdiMenuDown } from "@mdi/js";
|
||||
|
||||
interface Device {
|
||||
name: string;
|
||||
@@ -111,6 +115,10 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
@property({ type: Boolean })
|
||||
private _opened?: boolean;
|
||||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
|
||||
|
||||
private _init = false;
|
||||
|
||||
private _getDevices = memoizeOne(
|
||||
(
|
||||
devices: DeviceRegistryEntry[],
|
||||
@@ -122,18 +130,27 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
deviceFilter: this["deviceFilter"]
|
||||
): Device[] => {
|
||||
if (!devices.length) {
|
||||
return [];
|
||||
return [
|
||||
{
|
||||
id: "",
|
||||
area: "",
|
||||
name: this.hass.localize("ui.components.device-picker.no_devices"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
|
||||
if (includeDomains || excludeDomains || includeDeviceClasses) {
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
deviceEntityLookup[entity.device_id] = [];
|
||||
}
|
||||
deviceEntityLookup[entity.device_id].push(entity);
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
deviceEntityLookup[entity.device_id] = [];
|
||||
}
|
||||
deviceEntityLookup[entity.device_id].push(entity);
|
||||
}
|
||||
|
||||
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||
@@ -141,7 +158,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
areaLookup[area.area_id] = area;
|
||||
}
|
||||
|
||||
let inputDevices = [...devices];
|
||||
let inputDevices = devices.filter(
|
||||
(device) => device.id === this.value || !device.disabled_by
|
||||
);
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
@@ -208,6 +227,15 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
: this.hass.localize("ui.components.device-picker.no_area"),
|
||||
};
|
||||
});
|
||||
if (!outputDevices.length) {
|
||||
return [
|
||||
{
|
||||
id: "",
|
||||
area: "",
|
||||
name: this.hass.localize("ui.components.device-picker.no_match"),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (outputDevices.length === 1) {
|
||||
return outputDevices;
|
||||
}
|
||||
@@ -215,6 +243,18 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
);
|
||||
|
||||
public open() {
|
||||
this.updateComplete.then(() => {
|
||||
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
|
||||
});
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.updateComplete.then(() => {
|
||||
this.shadowRoot?.querySelector("paper-input")?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
@@ -229,25 +269,33 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this.devices && this.areas && this.entities) ||
|
||||
(changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
(this._comboBox as any).items = this._getDevices(
|
||||
this.devices!,
|
||||
this.areas!,
|
||||
this.entities!,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.devices || !this.areas || !this.entities) {
|
||||
return html``;
|
||||
}
|
||||
const devices = this._getDevices(
|
||||
this.devices,
|
||||
this.areas,
|
||||
this.entities,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter
|
||||
);
|
||||
return html`
|
||||
<vaadin-combo-box-light
|
||||
item-value-path="id"
|
||||
item-id-path="id"
|
||||
item-label-path="name"
|
||||
.items=${devices}
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@@ -265,34 +313,30 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
${this.value
|
||||
? html`
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.device-picker.clear"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="clear-button"
|
||||
icon="hass:close"
|
||||
@click=${this._clearValue}
|
||||
no-ripple
|
||||
>
|
||||
Clear
|
||||
</ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${devices.length > 0
|
||||
? html`
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.device-picker.show_devices"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
|
||||
>
|
||||
Toggle
|
||||
</ha-icon-button>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.device-picker.show_devices"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</paper-input>
|
||||
</vaadin-combo-box-light>
|
||||
`;
|
||||
@@ -329,7 +373,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-input > ha-icon-button {
|
||||
paper-input > mwc-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
padding: 2px;
|
||||
color: var(--secondary-text-color);
|
||||
|
@@ -95,13 +95,24 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
@property({ type: Boolean, attribute: "hide-clear-icon" })
|
||||
public hideClearIcon = false;
|
||||
@property({ type: Boolean }) public hideClearIcon = false;
|
||||
|
||||
@property({ type: Boolean }) private _opened = false;
|
||||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
|
||||
|
||||
public open() {
|
||||
this.updateComplete.then(() => {
|
||||
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
|
||||
});
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.updateComplete.then(() => {
|
||||
this.shadowRoot?.querySelector("paper-input")?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
private _initedStates = false;
|
||||
|
||||
private _states: HassEntity[] = [];
|
||||
@@ -154,6 +165,24 @@ export class HaEntityPicker extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
if (!states.length) {
|
||||
return [
|
||||
{
|
||||
entity_id: "",
|
||||
state: "",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", user_id: null },
|
||||
attributes: {
|
||||
friendly_name: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_match"
|
||||
),
|
||||
icon: "mdi:magnify",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
);
|
||||
@@ -204,7 +233,6 @@ export class HaEntityPicker extends LitElement {
|
||||
.label=${this.label === undefined
|
||||
? this.hass.localize("ui.components.entity.entity-picker.entity")
|
||||
: this.label}
|
||||
.value=${this._value}
|
||||
.disabled=${this.disabled}
|
||||
class="input"
|
||||
autocapitalize="none"
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
@@ -14,6 +15,8 @@ import {
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
PropertyValues,
|
||||
query,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
@@ -29,6 +32,18 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
DeviceEntityLookup,
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../data/entity_registry";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||
|
||||
const rowRenderer = (
|
||||
root: HTMLElement,
|
||||
@@ -71,31 +86,250 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property() public _areas?: AreaRegistryEntry[];
|
||||
|
||||
@property({ type: Boolean, attribute: "no-add" })
|
||||
public noAdd?: boolean;
|
||||
|
||||
/**
|
||||
* Show only areas with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no areas with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only areas with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
|
||||
|
||||
@internalProperty() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
@internalProperty() private _devices?: DeviceRegistryEntry[];
|
||||
|
||||
@internalProperty() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
@internalProperty() private _opened?: boolean;
|
||||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
|
||||
|
||||
private _init = false;
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this._areas = this.noAdd
|
||||
? areas
|
||||
: [
|
||||
...areas,
|
||||
{
|
||||
area_id: "add_new",
|
||||
name: this.hass.localize("ui.components.area-picker.add_new"),
|
||||
},
|
||||
];
|
||||
this._areas = areas;
|
||||
}),
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this._devices = devices;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.updateComplete.then(() => {
|
||||
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
|
||||
});
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.updateComplete.then(() => {
|
||||
this.shadowRoot?.querySelector("paper-input")?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
private _getAreas = memoizeOne(
|
||||
(
|
||||
areas: AreaRegistryEntry[],
|
||||
devices: DeviceRegistryEntry[],
|
||||
entities: EntityRegistryEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
entityFilter: this["entityFilter"],
|
||||
noAdd: this["noAdd"]
|
||||
): AreaRegistryEntry[] => {
|
||||
if (!areas.length) {
|
||||
return [
|
||||
{
|
||||
area_id: "",
|
||||
name: this.hass.localize("ui.components.area-picker.no_areas"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryEntry[] | undefined;
|
||||
|
||||
if (includeDomains || excludeDomains || includeDeviceClasses) {
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
deviceEntityLookup[entity.device_id] = [];
|
||||
}
|
||||
deviceEntityLookup[entity.device_id].push(entity);
|
||||
}
|
||||
inputDevices = devices;
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
} else if (deviceFilter) {
|
||||
inputDevices = devices;
|
||||
} else if (entityFilter) {
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
}
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter(
|
||||
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
entityFilter!(entity)
|
||||
);
|
||||
}
|
||||
|
||||
let outputAreas = areas;
|
||||
|
||||
let areaIds: string[] | undefined;
|
||||
|
||||
if (inputDevices) {
|
||||
areaIds = inputDevices
|
||||
.filter((device) => device.area_id)
|
||||
.map((device) => device.area_id!);
|
||||
}
|
||||
|
||||
if (inputEntities) {
|
||||
areaIds = (areaIds ?? []).concat(
|
||||
inputEntities
|
||||
.filter((entity) => entity.area_id)
|
||||
.map((entity) => entity.area_id!)
|
||||
);
|
||||
}
|
||||
|
||||
if (areaIds) {
|
||||
outputAreas = areas.filter((area) => areaIds!.includes(area.area_id));
|
||||
}
|
||||
|
||||
if (!outputAreas.length) {
|
||||
outputAreas = [
|
||||
{
|
||||
area_id: "",
|
||||
name: this.hass.localize("ui.components.area-picker.no_match"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return noAdd
|
||||
? outputAreas
|
||||
: [
|
||||
...outputAreas,
|
||||
{
|
||||
area_id: "add_new",
|
||||
name: this.hass.localize("ui.components.area-picker.add_new"),
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this._devices && this._areas && this._entities) ||
|
||||
(changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
(this._comboBox as any).items = this._getAreas(
|
||||
this._areas!,
|
||||
this._devices!,
|
||||
this._entities!,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.noAdd
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._areas) {
|
||||
if (!this._devices || !this._areas || !this._entities) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
@@ -103,7 +337,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
item-value-path="area_id"
|
||||
item-id-path="area_id"
|
||||
item-label-path="name"
|
||||
.items=${this._areas}
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@@ -124,34 +357,28 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
${this.value
|
||||
? html`
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.area-picker.clear"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="clear-button"
|
||||
icon="hass:close"
|
||||
@click=${this._clearValue}
|
||||
no-ripple
|
||||
>
|
||||
${this.hass.localize("ui.components.area-picker.clear")}
|
||||
</ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${this._areas.length > 0
|
||||
? html`
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.area-picker.show_areas"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
|
||||
>
|
||||
${this.hass.localize("ui.components.area-picker.toggle")}
|
||||
</ha-icon-button>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize("ui.components.area-picker.toggle")}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</paper-input>
|
||||
</vaadin-combo-box-light>
|
||||
`;
|
||||
@@ -227,7 +454,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-input > ha-icon-button {
|
||||
paper-input > mwc-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
padding: 2px;
|
||||
color: var(--secondary-text-color);
|
||||
|
@@ -63,7 +63,7 @@ class HaAttributes extends LitElement {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.data-entry .value {
|
||||
max-width: 50%;
|
||||
max-width: 60%;
|
||||
overflow-wrap: break-word;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -77,6 +77,9 @@ class HaAttributes extends LitElement {
|
||||
pre {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin: 0px;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-line;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -52,6 +52,7 @@ class HaBluePrintPicker extends LitElement {
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.blueprint-picker.label")}
|
||||
.disabled=${this.disabled}
|
||||
horizontal-align="left"
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
@@ -110,6 +111,9 @@ class HaBluePrintPicker extends LitElement {
|
||||
paper-listbox {
|
||||
min-width: 200px;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -23,8 +23,6 @@ export class HaButtonMenu extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public fixed = false;
|
||||
|
||||
@query("mwc-menu", true) private _menu?: Menu;
|
||||
|
||||
public get items() {
|
||||
@@ -41,7 +39,6 @@ export class HaButtonMenu extends LitElement {
|
||||
<slot name="trigger"></slot>
|
||||
</div>
|
||||
<mwc-menu
|
||||
.fixed=${this.fixed}
|
||||
.corner=${this.corner}
|
||||
.multi=${this.multi}
|
||||
.activatable=${this.activatable}
|
||||
|
@@ -11,6 +11,7 @@ import {
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { ToggleButton } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
|
||||
@customElement("ha-button-toggle-group")
|
||||
export class HaButtonToggleGroup extends LitElement {
|
||||
@@ -21,17 +22,22 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div>
|
||||
${this.buttons.map(
|
||||
(button) => html`
|
||||
<mwc-icon-button
|
||||
.label=${button.label}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
${this.buttons.map((button) =>
|
||||
button.iconPath
|
||||
? html`<mwc-icon-button
|
||||
.label=${button.label}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
|
||||
</mwc-icon-button>`
|
||||
: html`<mwc-button
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>${button.label}</mwc-button
|
||||
>`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
@@ -49,13 +55,15 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
--mdc-icon-button-size: var(--button-toggle-size, 36px);
|
||||
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
|
||||
}
|
||||
mwc-icon-button {
|
||||
mwc-icon-button,
|
||||
mwc-button {
|
||||
border: 1px solid var(--primary-color);
|
||||
border-right-width: 0px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
mwc-icon-button::before {
|
||||
mwc-icon-button::before,
|
||||
mwc-button::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
@@ -67,17 +75,21 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
content: "";
|
||||
transition: opacity 15ms linear, background-color 15ms linear;
|
||||
}
|
||||
mwc-icon-button[active]::before {
|
||||
mwc-icon-button[active]::before,
|
||||
mwc-button[active]::before {
|
||||
opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
|
||||
}
|
||||
mwc-icon-button:first-child {
|
||||
mwc-icon-button:first-child,
|
||||
mwc-button:first-child {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
mwc-icon-button:last-child {
|
||||
mwc-icon-button:last-child,
|
||||
mwc-button:last-child {
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
mwc-icon-button:only-child {
|
||||
mwc-icon-button:only-child,
|
||||
mwc-button:only-child {
|
||||
border-radius: 4px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
@@ -68,12 +68,6 @@ export class HaDialog extends MwcDialog {
|
||||
top: var(--dialog-surface-top);
|
||||
min-height: var(--mdc-dialog-min-height, auto);
|
||||
}
|
||||
|
||||
:host([full]) .mdc-dialog .mdc-dialog__surface {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -96,23 +90,6 @@ export class HaDialog extends MwcDialog {
|
||||
margin-left: 40px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.mdc-dialog__content::-webkit-scrollbar {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
|
||||
.mdc-dialog__content::-webkit-scrollbar-thumb {
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--scrollbar-thumb-color);
|
||||
}
|
||||
|
||||
.mdc-dialog__content {
|
||||
overflow-y: auto;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { mdiChevronDown } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@@ -9,9 +8,10 @@ import {
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-svg-icon";
|
||||
import { mdiChevronDown } from "@mdi/js";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
@customElement("ha-expansion-panel")
|
||||
class HaExpansionPanel extends LitElement {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { mdiHelpCircleOutline } from "@mdi/js";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
css,
|
||||
@@ -18,7 +18,7 @@ export class HaHelpTooltip extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-svg-icon .path=${mdiHelpCircleOutline}></ha-svg-icon>
|
||||
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
|
||||
<paper-tooltip
|
||||
offset="4"
|
||||
.position=${this.position}
|
||||
|
@@ -127,7 +127,7 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
// Parse playlist assuming it is a master playlist. Match group 1 is whether hevc, match group 2 is regular playlist url
|
||||
// See https://tools.ietf.org/html/rfc8216 for HLS spec details
|
||||
const playlistRegexp = /#EXT-X-STREAM-INF:.*?(?:CODECS=".*?(?<isHevc>hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(?<streamUrl>.+)/g;
|
||||
const playlistRegexp = /#EXT-X-STREAM-INF:.*?(?:CODECS=".*?(hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(.+)/g;
|
||||
const match = playlistRegexp.exec(masterPlaylist);
|
||||
const matchTwice = playlistRegexp.exec(masterPlaylist);
|
||||
|
||||
@@ -136,17 +136,13 @@ class HaHLSPlayer extends LitElement {
|
||||
let playlist_url: string;
|
||||
if (match !== null && matchTwice === null) {
|
||||
// Only send the regular playlist url if we match exactly once
|
||||
playlist_url = new URL(match.groups!.streamUrl, this.url).href;
|
||||
playlist_url = new URL(match[2], this.url).href;
|
||||
} else {
|
||||
playlist_url = this.url;
|
||||
}
|
||||
|
||||
// If codec is HEVC and ExoPlayer is supported, use ExoPlayer.
|
||||
if (
|
||||
this._useExoPlayer &&
|
||||
match !== null &&
|
||||
match.groups!.isHevc !== undefined
|
||||
) {
|
||||
if (this._useExoPlayer && match !== null && match[1] !== undefined) {
|
||||
this._renderHLSExoPlayer(playlist_url);
|
||||
} else if (hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, hls, playlist_url);
|
||||
|
@@ -60,8 +60,9 @@ export class HaIconInput extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-icon {
|
||||
position: relative;
|
||||
bottom: 4px;
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -10,7 +10,6 @@ class HaLabeledSlider extends PolymerElement {
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -30,6 +29,7 @@ class HaLabeledSlider extends PolymerElement {
|
||||
ha-slider {
|
||||
flex-grow: 1;
|
||||
background-image: var(--ha-slider-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@@ -13,7 +13,7 @@ import type { HomeAssistant } from "../types";
|
||||
class HaRelativeTime extends UpdatingElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public datetime?: string;
|
||||
@property({ attribute: false }) public datetime?: string | Date;
|
||||
|
||||
private _interval?: number;
|
||||
|
||||
|
45
src/components/ha-selector/ha-selector-action.ts
Normal file
45
src/components/ha-selector/ha-selector-action.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { ActionSelector } from "../../data/selector";
|
||||
import { Action } from "../../data/script";
|
||||
import "../../panels/config/automation/action/ha-automation-action";
|
||||
|
||||
@customElement("ha-selector-action")
|
||||
export class HaActionSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: ActionSelector;
|
||||
|
||||
@property() public value?: Action;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-automation-action
|
||||
.actions=${this.value || []}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-action>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-automation-action {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-action": HaActionSelector;
|
||||
}
|
||||
}
|
@@ -1,7 +1,16 @@
|
||||
import { customElement, html, LitElement, property } from "lit-element";
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AreaSelector } from "../../data/selector";
|
||||
import "../ha-area-picker";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
|
||||
@customElement("ha-selector-area")
|
||||
export class HaAreaSelector extends LitElement {
|
||||
@@ -13,14 +22,77 @@ export class HaAreaSelector extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@internalProperty() public _configEntries?: ConfigEntry[];
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
if (
|
||||
oldSelector !== this.selector &&
|
||||
this.selector.area.device?.integration
|
||||
) {
|
||||
this._loadConfigEntries();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${(device) => this._filterDevices(device)}
|
||||
.entityFilter=${(entity) => this._filterEntities(entity)}
|
||||
.includeDeviceClasses=${this.selector.area.entity?.device_class
|
||||
? [this.selector.area.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.area.entity?.domain
|
||||
? [this.selector.area.entity.domain]
|
||||
: undefined}
|
||||
></ha-area-picker>`;
|
||||
}
|
||||
|
||||
private _filterEntities(entity: EntityRegistryEntry): boolean {
|
||||
if (this.selector.area.entity?.integration) {
|
||||
if (entity.platform !== this.selector.area.entity.integration) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _filterDevices(device: DeviceRegistryEntry): boolean {
|
||||
if (
|
||||
this.selector.area.device?.manufacturer &&
|
||||
device.manufacturer !== this.selector.area.device.manufacturer
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.selector.area.device?.model &&
|
||||
device.model !== this.selector.area.device.model
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.selector.area.device?.integration) {
|
||||
if (
|
||||
this._configEntries &&
|
||||
!this._configEntries.some((entry) =>
|
||||
device.config_entries.includes(entry.entry_id)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) => entry.domain === this.selector.area.device?.integration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -63,7 +63,8 @@ export class HaDeviceSelector extends LitElement {
|
||||
}
|
||||
if (this.selector.device.integration) {
|
||||
if (
|
||||
!this._configEntries?.some((entry) =>
|
||||
this._configEntries &&
|
||||
!this._configEntries.some((entry) =>
|
||||
device.config_entries.includes(entry.entry_id)
|
||||
)
|
||||
) {
|
||||
|
@@ -19,7 +19,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public selector!: EntitySelector;
|
||||
|
||||
@internalProperty() private _entities?: Record<string, string>;
|
||||
@internalProperty() private _entityPlaformLookup?: Record<string, string>;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@@ -45,7 +45,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
entityLookup[confEnt.entity_id] = confEnt.platform;
|
||||
}
|
||||
this._entities = entityLookup;
|
||||
this._entityPlaformLookup = entityLookup;
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -66,8 +66,9 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
if (this.selector.entity.integration) {
|
||||
if (
|
||||
!this._entities ||
|
||||
this._entities[entity.entity_id] !== this.selector.entity.integration
|
||||
!this._entityPlaformLookup ||
|
||||
this._entityPlaformLookup[entity.entity_id] !==
|
||||
this.selector.entity.integration
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
153
src/components/ha-selector/ha-selector-target.ts
Normal file
153
src/components/ha-selector/ha-selector-target.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { TargetSelector } from "../../data/selector";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import "../ha-target-picker";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import { Target } from "../../data/target";
|
||||
import "@material/mwc-tab-bar/mwc-tab-bar";
|
||||
import "@material/mwc-tab/mwc-tab";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
|
||||
@customElement("ha-selector-target")
|
||||
export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: TargetSelector;
|
||||
|
||||
@property() public value?: Target;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@internalProperty() private _entityPlaformLookup?: Record<string, string>;
|
||||
|
||||
@internalProperty() private _configEntries?: ConfigEntry[];
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
const entityLookup = {};
|
||||
for (const confEnt of entities) {
|
||||
if (!confEnt.platform) {
|
||||
continue;
|
||||
}
|
||||
entityLookup[confEnt.entity_id] = confEnt.platform;
|
||||
}
|
||||
this._entityPlaformLookup = entityLookup;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
if (
|
||||
oldSelector !== this.selector &&
|
||||
this.selector.target.device?.integration
|
||||
) {
|
||||
this._loadConfigEntries();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<ha-target-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.deviceFilter=${(device) => this._filterDevices(device)}
|
||||
.entityRegFilter=${(entity: EntityRegistryEntry) =>
|
||||
this._filterRegEntities(entity)}
|
||||
.entityFilter=${(entity: HassEntity) => this._filterEntities(entity)}
|
||||
.includeDeviceClasses=${this.selector.target.entity?.device_class
|
||||
? [this.selector.target.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.target.entity?.domain
|
||||
? [this.selector.target.entity.domain]
|
||||
: undefined}
|
||||
></ha-target-picker>`;
|
||||
}
|
||||
|
||||
private _filterEntities(entity: HassEntity): boolean {
|
||||
if (this.selector.target.entity?.integration) {
|
||||
if (
|
||||
!this._entityPlaformLookup ||
|
||||
this._entityPlaformLookup[entity.entity_id] !==
|
||||
this.selector.target.entity.integration
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _filterRegEntities(entity: EntityRegistryEntry): boolean {
|
||||
if (this.selector.target.entity?.integration) {
|
||||
if (entity.platform !== this.selector.target.entity.integration) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _filterDevices(device: DeviceRegistryEntry): boolean {
|
||||
if (
|
||||
this.selector.target.device?.manufacturer &&
|
||||
device.manufacturer !== this.selector.target.device.manufacturer
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.selector.target.device?.model &&
|
||||
device.model !== this.selector.target.device.model
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.selector.target.device?.integration) {
|
||||
if (
|
||||
!this._configEntries?.some((entry) =>
|
||||
device.config_entries.includes(entry.entry_id)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) => entry.domain === this.selector.target.device?.integration
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-target-picker {
|
||||
margin: 0 -8px;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-target": HaTargetSelector;
|
||||
}
|
||||
}
|
@@ -5,9 +5,11 @@ import { HomeAssistant } from "../../types";
|
||||
import "./ha-selector-entity";
|
||||
import "./ha-selector-device";
|
||||
import "./ha-selector-area";
|
||||
import "./ha-selector-target";
|
||||
import "./ha-selector-number";
|
||||
import "./ha-selector-boolean";
|
||||
import "./ha-selector-time";
|
||||
import "./ha-selector-action";
|
||||
import { Selector } from "../../data/selector";
|
||||
|
||||
@customElement("ha-selector")
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@@ -18,10 +18,13 @@ export class HaSettingsRow extends LitElement {
|
||||
|
||||
protected render(): SVGTemplateResult {
|
||||
return html`
|
||||
<mwc-list-item noninteractive ?twoline=${!this.threeLine}>
|
||||
<paper-item-body
|
||||
?two-line=${!this.threeLine}
|
||||
?three-line=${this.threeLine}
|
||||
>
|
||||
<slot name="heading"></slot>
|
||||
<span slot="secondary"><slot name="description"></slot></span>
|
||||
</mwc-list-item>
|
||||
<div secondary><slot name="description"></slot></div>
|
||||
</paper-item-body>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
@@ -34,7 +37,14 @@ export class HaSettingsRow extends LitElement {
|
||||
align-content: normal;
|
||||
align-self: auto;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
paper-item-body {
|
||||
padding: 8px 16px 8px 0;
|
||||
}
|
||||
paper-item-body[two-line] {
|
||||
min-height: calc(
|
||||
var(--paper-item-body-two-line-min-height, 72px) - 16px
|
||||
);
|
||||
}
|
||||
:host([narrow]) {
|
||||
align-items: normal;
|
||||
@@ -42,12 +52,12 @@ export class HaSettingsRow extends LitElement {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
mwc-list-item {
|
||||
--mdc-list-side-padding: 0;
|
||||
}
|
||||
::slotted(ha-switch) {
|
||||
padding: 16px 0;
|
||||
}
|
||||
div[secondary] {
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
605
src/components/ha-target-picker.ts
Normal file
605
src/components/ha-target-picker.ts
Normal file
@@ -0,0 +1,605 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
query,
|
||||
unsafeCSS,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../types";
|
||||
// @ts-ignore
|
||||
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
|
||||
import {
|
||||
mdiSofa,
|
||||
mdiDevices,
|
||||
mdiClose,
|
||||
mdiPlus,
|
||||
mdiUnfoldMoreVertical,
|
||||
} from "@mdi/js";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-icon";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../data/area_registry";
|
||||
import {
|
||||
computeDeviceName,
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../data/entity_registry";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { stateIcon } from "../common/entity/state_icon";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { Target } from "../data/target";
|
||||
import { ensureArray } from "../common/ensure-array";
|
||||
import "./entity/ha-entity-picker";
|
||||
import "./device/ha-device-picker";
|
||||
import "./ha-area-picker";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
|
||||
@customElement("ha-target-picker")
|
||||
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public value?: Target;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
/**
|
||||
* Show only targets with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only targets with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property() public entityRegFilter?: (entity: EntityRegistryEntry) => boolean;
|
||||
|
||||
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
@internalProperty() private _areas?: { [areaId: string]: AreaRegistryEntry };
|
||||
|
||||
@internalProperty() private _devices?: {
|
||||
[deviceId: string]: DeviceRegistryEntry;
|
||||
};
|
||||
|
||||
@internalProperty() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
@internalProperty() private _addMode?: "area_id" | "entity_id" | "device_id";
|
||||
|
||||
@query("#input") private _inputElement?;
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||
for (const area of areas) {
|
||||
areaLookup[area.area_id] = area;
|
||||
}
|
||||
this._areas = areaLookup;
|
||||
}),
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
|
||||
for (const device of devices) {
|
||||
deviceLookup[device.id] = device;
|
||||
}
|
||||
this._devices = deviceLookup;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._areas || !this._devices || !this._entities) {
|
||||
return html``;
|
||||
}
|
||||
return html`<div class="mdc-chip-set items">
|
||||
${ensureArray(this.value?.area_id)?.map((area_id) => {
|
||||
const area = this._areas![area_id];
|
||||
return this._renderChip(
|
||||
"area_id",
|
||||
area_id,
|
||||
area?.name || area_id,
|
||||
undefined,
|
||||
mdiSofa
|
||||
);
|
||||
})}
|
||||
${ensureArray(this.value?.device_id)?.map((device_id) => {
|
||||
const device = this._devices![device_id];
|
||||
return this._renderChip(
|
||||
"device_id",
|
||||
device_id,
|
||||
device ? computeDeviceName(device, this.hass) : device_id,
|
||||
undefined,
|
||||
mdiDevices
|
||||
);
|
||||
})}
|
||||
${ensureArray(this.value?.entity_id)?.map((entity_id) => {
|
||||
const entity = this.hass.states[entity_id];
|
||||
return this._renderChip(
|
||||
"entity_id",
|
||||
entity_id,
|
||||
entity ? computeStateName(entity) : entity_id,
|
||||
entity ? stateIcon(entity) : undefined
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
${this._renderPicker()}
|
||||
<div class="mdc-chip-set">
|
||||
<div
|
||||
class="mdc-chip area_id add"
|
||||
.type=${"area_id"}
|
||||
@click=${this._showPicker}
|
||||
>
|
||||
<div class="mdc-chip__ripple"></div>
|
||||
<ha-svg-icon
|
||||
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
<span role="gridcell">
|
||||
<span role="button" tabindex="0" class="mdc-chip__primary-action">
|
||||
<span class="mdc-chip__text"
|
||||
>${this.hass.localize(
|
||||
"ui.components.target-picker.add_area_id"
|
||||
)}</span
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mdc-chip device_id add"
|
||||
.type=${"device_id"}
|
||||
@click=${this._showPicker}
|
||||
>
|
||||
<div class="mdc-chip__ripple"></div>
|
||||
<ha-svg-icon
|
||||
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
<span role="gridcell">
|
||||
<span role="button" tabindex="0" class="mdc-chip__primary-action">
|
||||
<span class="mdc-chip__text"
|
||||
>${this.hass.localize(
|
||||
"ui.components.target-picker.add_device_id"
|
||||
)}</span
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mdc-chip entity_id add"
|
||||
.type=${"entity_id"}
|
||||
@click=${this._showPicker}
|
||||
>
|
||||
<div class="mdc-chip__ripple"></div>
|
||||
<ha-svg-icon
|
||||
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
<span role="gridcell">
|
||||
<span role="button" tabindex="0" class="mdc-chip__primary-action">
|
||||
<span class="mdc-chip__text"
|
||||
>${this.hass.localize(
|
||||
"ui.components.target-picker.add_entity_id"
|
||||
)}</span
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _showPicker(ev) {
|
||||
this._addMode = ev.currentTarget.type;
|
||||
await this.updateComplete;
|
||||
setTimeout(() => {
|
||||
this._inputElement?.open();
|
||||
this._inputElement?.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private _renderChip(
|
||||
type: string,
|
||||
id: string,
|
||||
name: string,
|
||||
icon?: string,
|
||||
iconPath?: string
|
||||
) {
|
||||
return html`
|
||||
<div
|
||||
class="mdc-chip ${classMap({
|
||||
[type]: true,
|
||||
})}"
|
||||
>
|
||||
${iconPath
|
||||
? html`<ha-svg-icon
|
||||
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||
.path=${iconPath}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
${icon
|
||||
? html`<ha-icon
|
||||
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||
.icon=${icon}
|
||||
></ha-icon>`
|
||||
: ""}
|
||||
<span role="gridcell">
|
||||
<span role="button" tabindex="0" class="mdc-chip__primary-action">
|
||||
<span class="mdc-chip__text">${name}</span>
|
||||
</span>
|
||||
</span>
|
||||
${type === "entity_id"
|
||||
? ""
|
||||
: html` <span role="gridcell">
|
||||
<mwc-icon-button
|
||||
class="expand-btn mdc-chip__icon mdc-chip__icon--trailing"
|
||||
tabindex="-1"
|
||||
role="button"
|
||||
.label=${"Expand"}
|
||||
.id=${id}
|
||||
.type=${type}
|
||||
@click=${this._handleExpand}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiUnfoldMoreVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<paper-tooltip class="expand" animation-delay="0"
|
||||
>${this.hass.localize(
|
||||
`ui.components.target-picker.expand_${type}`
|
||||
)}</paper-tooltip
|
||||
>
|
||||
</span>`}
|
||||
<span role="gridcell">
|
||||
<mwc-icon-button
|
||||
class="mdc-chip__icon mdc-chip__icon--trailing"
|
||||
tabindex="-1"
|
||||
role="button"
|
||||
.label=${"Remove"}
|
||||
.id=${id}
|
||||
.type=${type}
|
||||
@click=${this._handleRemove}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<paper-tooltip animation-delay="0"
|
||||
>${this.hass.localize(
|
||||
`ui.components.target-picker.remove_${type}`
|
||||
)}</paper-tooltip
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPicker() {
|
||||
switch (this._addMode) {
|
||||
case "area_id":
|
||||
return html`<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"area_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_area_id"
|
||||
)}
|
||||
no-add
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityRegFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
></ha-area-picker>`;
|
||||
case "device_id":
|
||||
return html`<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"device_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_device_id"
|
||||
)}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityRegFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
></ha-device-picker>`;
|
||||
case "entity_id":
|
||||
return html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"entity_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_entity_id"
|
||||
)}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
></ha-entity-picker>`;
|
||||
}
|
||||
return html``;
|
||||
}
|
||||
|
||||
private _targetPicked(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.value) {
|
||||
return;
|
||||
}
|
||||
const value = ev.detail.value;
|
||||
const target = ev.currentTarget;
|
||||
target.value = "";
|
||||
this._addMode = undefined;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this.value
|
||||
? {
|
||||
...this.value,
|
||||
[target.type]: this.value[target.type]
|
||||
? [...ensureArray(this.value[target.type]), value]
|
||||
: value,
|
||||
}
|
||||
: { [target.type]: value },
|
||||
});
|
||||
}
|
||||
|
||||
private _handleExpand(ev) {
|
||||
const target = ev.currentTarget as any;
|
||||
const newDevices: string[] = [];
|
||||
const newEntities: string[] = [];
|
||||
if (target.type === "area_id") {
|
||||
Object.values(this._devices!).forEach((device) => {
|
||||
if (
|
||||
device.area_id === target.id &&
|
||||
!this.value!.device_id?.includes(device.id) &&
|
||||
this._deviceMeetsFilter(device)
|
||||
) {
|
||||
newDevices.push(device.id);
|
||||
}
|
||||
});
|
||||
this._entities!.forEach((entity) => {
|
||||
if (
|
||||
entity.area_id === target.id &&
|
||||
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||
this._entityRegMeetsFilter(entity)
|
||||
) {
|
||||
newEntities.push(entity.entity_id);
|
||||
}
|
||||
});
|
||||
} else if (target.type === "device_id") {
|
||||
this._entities!.forEach((entity) => {
|
||||
if (
|
||||
entity.device_id === target.id &&
|
||||
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||
this._entityRegMeetsFilter(entity)
|
||||
) {
|
||||
newEntities.push(entity.entity_id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
let value = this.value;
|
||||
if (newEntities.length) {
|
||||
value = this._addItems(value, "entity_id", newEntities);
|
||||
}
|
||||
if (newDevices.length) {
|
||||
value = this._addItems(value, "device_id", newDevices);
|
||||
}
|
||||
value = this._removeItem(value, target.type, target.id);
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
private _handleRemove(ev) {
|
||||
const target = ev.currentTarget as any;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this._removeItem(this.value, target.type, target.id),
|
||||
});
|
||||
}
|
||||
|
||||
private _addItems(
|
||||
value: this["value"],
|
||||
type: string,
|
||||
ids: string[]
|
||||
): this["value"] {
|
||||
return {
|
||||
...value,
|
||||
[type]: value![type] ? ensureArray(value![type])!.concat(ids) : ids,
|
||||
};
|
||||
}
|
||||
|
||||
private _removeItem(
|
||||
value: this["value"],
|
||||
type: string,
|
||||
id: string
|
||||
): this["value"] {
|
||||
const newVal = ensureArray(value![type])!.filter((val) => val !== id);
|
||||
if (newVal.length) {
|
||||
return {
|
||||
...value,
|
||||
[type]: newVal,
|
||||
};
|
||||
}
|
||||
const val = { ...value }!;
|
||||
delete val[type];
|
||||
if (Object.keys(val).length) {
|
||||
return val;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _deviceMeetsFilter(device: DeviceRegistryEntry): boolean {
|
||||
const devEntities = this._entities?.filter(
|
||||
(entity) => entity.device_id === device.id
|
||||
);
|
||||
if (this.includeDomains) {
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!devEntities.some((entity) =>
|
||||
this.includeDomains!.includes(computeDomain(entity.entity_id))
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.includeDeviceClasses) {
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!devEntities.some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
this.includeDeviceClasses!.includes(
|
||||
stateObj.attributes.device_class
|
||||
)
|
||||
);
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.deviceFilter) {
|
||||
return this.deviceFilter(device);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _entityRegMeetsFilter(entity: EntityRegistryEntry): boolean {
|
||||
if (
|
||||
this.includeDomains &&
|
||||
!this.includeDomains.includes(computeDomain(entity.entity_id))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.includeDeviceClasses) {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!stateObj.attributes.device_class ||
|
||||
!this.includeDeviceClasses!.includes(stateObj.attributes.device_class)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.entityRegFilter) {
|
||||
return this.entityRegFilter(entity);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
${unsafeCSS(chipStyles)}
|
||||
.mdc-chip {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.items {
|
||||
z-index: 2;
|
||||
}
|
||||
.mdc-chip.add {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
.mdc-chip:not(.add) {
|
||||
cursor: default;
|
||||
}
|
||||
.mdc-chip mwc-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
outline: none;
|
||||
}
|
||||
.mdc-chip mwc-icon-button ha-svg-icon {
|
||||
border-radius: 50%;
|
||||
background: var(--secondary-text-color);
|
||||
}
|
||||
.mdc-chip__icon.mdc-chip__icon--trailing {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
--mdc-icon-size: 14px;
|
||||
color: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip__icon--leading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
--mdc-icon-size: 20px;
|
||||
border-radius: 50%;
|
||||
padding: 6px;
|
||||
margin-left: -14px !important;
|
||||
}
|
||||
.expand-btn {
|
||||
margin-right: 0;
|
||||
}
|
||||
.mdc-chip.area_id:not(.add) {
|
||||
border: 2px solid #fed6a4;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip.area_id:not(.add) .mdc-chip__icon--leading,
|
||||
.mdc-chip.area_id.add {
|
||||
background: #fed6a4;
|
||||
}
|
||||
.mdc-chip.device_id:not(.add) {
|
||||
border: 2px solid #a8e1fb;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip.device_id:not(.add) .mdc-chip__icon--leading,
|
||||
.mdc-chip.device_id.add {
|
||||
background: #a8e1fb;
|
||||
}
|
||||
.mdc-chip.entity_id:not(.add) {
|
||||
border: 2px solid #d2e7b9;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip.entity_id:not(.add) .mdc-chip__icon--leading,
|
||||
.mdc-chip.entity_id.add {
|
||||
background: #d2e7b9;
|
||||
}
|
||||
.mdc-chip:hover {
|
||||
z-index: 5;
|
||||
}
|
||||
paper-tooltip.expand {
|
||||
min-width: 200px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-target-picker": HaTargetPicker;
|
||||
}
|
||||
}
|
@@ -6,7 +6,7 @@ import { navigate } from "../common/navigate";
|
||||
import { Context, HomeAssistant } from "../types";
|
||||
import { BlueprintInput } from "./blueprint";
|
||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import { Action } from "./script";
|
||||
import { Action, MODES } from "./script";
|
||||
|
||||
export interface AutomationEntity extends HassEntityBase {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
@@ -26,7 +26,7 @@ export interface ManualAutomationConfig {
|
||||
trigger: Trigger[];
|
||||
condition?: Condition[];
|
||||
action: Action[];
|
||||
mode?: "single" | "restart" | "queued" | "parallel";
|
||||
mode?: typeof MODES[number];
|
||||
max?: number;
|
||||
}
|
||||
|
||||
|
@@ -17,6 +17,7 @@ export interface DeviceRegistryEntry {
|
||||
area_id?: string;
|
||||
name_by_user?: string;
|
||||
entry_type: "service" | null;
|
||||
disabled_by: string | null;
|
||||
}
|
||||
|
||||
export interface DeviceEntityLookup {
|
||||
@@ -26,6 +27,7 @@ export interface DeviceEntityLookup {
|
||||
export interface DeviceRegistryEntryMutableParams {
|
||||
area_id?: string | null;
|
||||
name_by_user?: string | null;
|
||||
disabled_by?: string | null;
|
||||
}
|
||||
|
||||
export const fallbackDeviceName = (
|
||||
|
@@ -124,13 +124,17 @@ export const getLogbookMessage = (
|
||||
switch (domain) {
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
return state === "not_home"
|
||||
? hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`)
|
||||
: hass.localize(
|
||||
`${LOGBOOK_LOCALIZE_PATH}.was_at_state`,
|
||||
"state",
|
||||
state
|
||||
);
|
||||
if (state === "not_home") {
|
||||
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`);
|
||||
}
|
||||
if (state === "home") {
|
||||
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_home`);
|
||||
}
|
||||
return hass.localize(
|
||||
`${LOGBOOK_LOCALIZE_PATH}.was_at_state`,
|
||||
"state",
|
||||
state
|
||||
);
|
||||
|
||||
case "sun":
|
||||
return state === "above_horizon"
|
||||
|
@@ -148,6 +148,11 @@ export interface CustomActionConfig extends BaseActionConfig {
|
||||
action: "fire-dom-event";
|
||||
}
|
||||
|
||||
export interface MultipleActionConfig extends BaseActionConfig {
|
||||
action: "multiple";
|
||||
actions: ActionConfig[];
|
||||
}
|
||||
|
||||
export interface BaseActionConfig {
|
||||
confirmation?: ConfirmationRestrictionConfig;
|
||||
}
|
||||
@@ -168,7 +173,8 @@ export type ActionConfig =
|
||||
| UrlActionConfig
|
||||
| MoreInfoActionConfig
|
||||
| NoActionConfig
|
||||
| CustomActionConfig;
|
||||
| CustomActionConfig
|
||||
| MultipleActionConfig;
|
||||
|
||||
type LovelaceUpdatedEvent = HassEventBase & {
|
||||
event_type: "lovelace_updated";
|
||||
|
@@ -7,13 +7,13 @@ import { navigate } from "../common/navigate";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { Condition, Trigger } from "./automation";
|
||||
|
||||
export const MODES = ["single", "restart", "queued", "parallel"];
|
||||
export const MODES = ["single", "restart", "queued", "parallel"] as const;
|
||||
export const MODES_MAX = ["queued", "parallel"];
|
||||
|
||||
export interface ScriptEntity extends HassEntityBase {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
last_triggered: string;
|
||||
mode: "single" | "restart" | "queued" | "parallel";
|
||||
mode: typeof MODES[number];
|
||||
current?: number;
|
||||
max?: number;
|
||||
};
|
||||
@@ -23,7 +23,7 @@ export interface ScriptConfig {
|
||||
alias: string;
|
||||
sequence: Action[];
|
||||
icon?: string;
|
||||
mode?: "single" | "restart" | "queued" | "parallel";
|
||||
mode?: typeof MODES[number];
|
||||
max?: number;
|
||||
}
|
||||
|
||||
|
@@ -2,9 +2,11 @@ export type Selector =
|
||||
| EntitySelector
|
||||
| DeviceSelector
|
||||
| AreaSelector
|
||||
| TargetSelector
|
||||
| NumberSelector
|
||||
| BooleanSelector
|
||||
| TimeSelector;
|
||||
| TimeSelector
|
||||
| ActionSelector;
|
||||
|
||||
export interface EntitySelector {
|
||||
entity: {
|
||||
@@ -19,13 +21,41 @@ export interface DeviceSelector {
|
||||
integration?: string;
|
||||
manufacturer?: string;
|
||||
model?: string;
|
||||
entity?: EntitySelector["entity"];
|
||||
entity?: {
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface AreaSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
area: {};
|
||||
area: {
|
||||
entity?: {
|
||||
integration?: EntitySelector["entity"]["integration"];
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
device?: {
|
||||
integration?: DeviceSelector["device"]["integration"];
|
||||
manufacturer?: DeviceSelector["device"]["manufacturer"];
|
||||
model?: DeviceSelector["device"]["model"];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface TargetSelector {
|
||||
target: {
|
||||
entity?: {
|
||||
integration?: EntitySelector["entity"]["integration"];
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
device?: {
|
||||
integration?: DeviceSelector["device"]["integration"];
|
||||
manufacturer?: DeviceSelector["device"]["manufacturer"];
|
||||
model?: DeviceSelector["device"]["model"];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface NumberSelector {
|
||||
@@ -47,3 +77,8 @@ export interface TimeSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
time: {};
|
||||
}
|
||||
|
||||
export interface ActionSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
action: {};
|
||||
}
|
||||
|
@@ -38,3 +38,12 @@ export const addItem = (
|
||||
type: "shopping_list/items/add",
|
||||
name,
|
||||
});
|
||||
|
||||
export const reorderItems = (
|
||||
hass: HomeAssistant,
|
||||
itemIds: [string]
|
||||
): Promise<ShoppingListItem> =>
|
||||
hass.callWS({
|
||||
type: "shopping_list/items/reorder",
|
||||
item_ids: itemIds,
|
||||
});
|
||||
|
5
src/data/target.ts
Normal file
5
src/data/target.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Target {
|
||||
entity_id?: string[];
|
||||
device_id?: string[];
|
||||
area_id?: string[];
|
||||
}
|
@@ -20,6 +20,7 @@ export interface User {
|
||||
|
||||
export interface UpdateUserParams {
|
||||
name?: User["name"];
|
||||
is_active?: User["is_active"];
|
||||
group_ids?: User["group_ids"];
|
||||
}
|
||||
|
||||
|
@@ -17,17 +17,17 @@ import "../../components/ha-switch";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { DialogParams } from "./show-dialog-box";
|
||||
import { DialogBoxParams } from "./show-dialog-box";
|
||||
|
||||
@customElement("dialog-box")
|
||||
class DialogBox extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _params?: DialogParams;
|
||||
@internalProperty() private _params?: DialogBoxParams;
|
||||
|
||||
@internalProperty() private _value?: string;
|
||||
|
||||
public async showDialog(params: DialogParams): Promise<void> {
|
||||
public async showDialog(params: DialogBoxParams): Promise<void> {
|
||||
this._params = params;
|
||||
if (params.prompt) {
|
||||
this._value = params.defaultValue;
|
||||
@@ -55,8 +55,8 @@ class DialogBox extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
?scrimClickAction=${this._params.prompt}
|
||||
?escapeKeyAction=${this._params.prompt}
|
||||
?scrimClickAction=${confirmPrompt}
|
||||
?escapeKeyAction=${confirmPrompt}
|
||||
@closed=${this._dialogClosed}
|
||||
defaultAction="ignore"
|
||||
.heading=${this._params.title
|
||||
@@ -140,10 +140,10 @@ class DialogBox extends LitElement {
|
||||
}
|
||||
|
||||
private _dialogClosed(ev) {
|
||||
if (ev.detail.action === "ignore") {
|
||||
if (this._params?.prompt && ev.detail.action === "ignore") {
|
||||
return;
|
||||
}
|
||||
this.closeDialog();
|
||||
this._dismiss();
|
||||
}
|
||||
|
||||
private _close(): void {
|
||||
|
@@ -1,31 +1,31 @@
|
||||
import { TemplateResult } from "lit-html";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
interface BaseDialogParams {
|
||||
interface BaseDialogBoxParams {
|
||||
confirmText?: string;
|
||||
text?: string | TemplateResult;
|
||||
title?: string;
|
||||
warning?: boolean;
|
||||
}
|
||||
|
||||
export interface AlertDialogParams extends BaseDialogParams {
|
||||
export interface AlertDialogParams extends BaseDialogBoxParams {
|
||||
confirm?: () => void;
|
||||
}
|
||||
|
||||
export interface ConfirmationDialogParams extends BaseDialogParams {
|
||||
export interface ConfirmationDialogParams extends BaseDialogBoxParams {
|
||||
dismissText?: string;
|
||||
confirm?: () => void;
|
||||
cancel?: () => void;
|
||||
}
|
||||
|
||||
export interface PromptDialogParams extends BaseDialogParams {
|
||||
export interface PromptDialogParams extends BaseDialogBoxParams {
|
||||
inputLabel?: string;
|
||||
inputType?: string;
|
||||
defaultValue?: string;
|
||||
confirm?: (out?: string) => void;
|
||||
}
|
||||
|
||||
export interface DialogParams
|
||||
export interface DialogBoxParams
|
||||
extends ConfirmationDialogParams,
|
||||
PromptDialogParams {
|
||||
confirm?: (out?: string) => void;
|
||||
@@ -37,10 +37,10 @@ export const loadGenericDialog = () => import("./dialog-box");
|
||||
|
||||
const showDialogHelper = (
|
||||
element: HTMLElement,
|
||||
dialogParams: DialogParams,
|
||||
dialogParams: DialogBoxParams,
|
||||
extra?: {
|
||||
confirmation?: DialogParams["confirmation"];
|
||||
prompt?: DialogParams["prompt"];
|
||||
confirmation?: DialogBoxParams["confirmation"];
|
||||
prompt?: DialogBoxParams["prompt"];
|
||||
}
|
||||
) =>
|
||||
new Promise((resolve) => {
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
||||
|
||||
@customElement("more-info-counter")
|
||||
class MoreInfoCounter extends LitElement {
|
||||
@@ -22,21 +23,29 @@ class MoreInfoCounter extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const disabled = UNAVAILABLE_STATES.includes(this.stateObj!.state);
|
||||
|
||||
return html`
|
||||
<div class="actions">
|
||||
<mwc-button
|
||||
.action="${"increment"}"
|
||||
@click="${this._handleActionClick}"
|
||||
@click=${this._handleActionClick}
|
||||
.disabled=${disabled}
|
||||
>
|
||||
${this.hass!.localize("ui.card.counter.actions.increment")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
.action="${"decrement"}"
|
||||
@click="${this._handleActionClick}"
|
||||
@click=${this._handleActionClick}
|
||||
.disabled=${disabled}
|
||||
>
|
||||
${this.hass!.localize("ui.card.counter.actions.decrement")}
|
||||
</mwc-button>
|
||||
<mwc-button .action="${"reset"}" @click="${this._handleActionClick}">
|
||||
<mwc-button
|
||||
.action="${"reset"}"
|
||||
@click=${this._handleActionClick}
|
||||
.disabled=${disabled}
|
||||
>
|
||||
${this.hass!.localize("ui.card.counter.actions.reset")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
@@ -53,8 +62,7 @@ class MoreInfoCounter extends LitElement {
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.actions {
|
||||
margin: 0;
|
||||
padding-top: 20px;
|
||||
margin: 8px 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
@@ -75,7 +75,7 @@ class MoreInfoPerson extends LitElement {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.actions {
|
||||
margin: 36px 0 8px 0;
|
||||
margin: 8px 0;
|
||||
text-align: right;
|
||||
}
|
||||
ha-map {
|
||||
|
@@ -44,7 +44,7 @@ class MoreInfoSun extends LitElement {
|
||||
>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetimeObj=${item === "ris" ? risingDate : settingDate}
|
||||
.datetime=${item === "ris" ? risingDate : settingDate}
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
<div class="value">
|
||||
@@ -80,6 +80,7 @@ class MoreInfoSun extends LitElement {
|
||||
}
|
||||
ha-relative-time {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
ha-relative-time::first-letter {
|
||||
text-transform: lowercase;
|
||||
|
@@ -76,8 +76,7 @@ class MoreInfoTimer extends LitElement {
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.actions {
|
||||
margin: 0;
|
||||
padding-top: 20px;
|
||||
margin: 8px 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
@@ -60,6 +60,12 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
*/
|
||||
@property({ type: Boolean }) public hasFab = false;
|
||||
|
||||
/**
|
||||
* Add an extra row at the bottom of the data table
|
||||
* @type {TemplateResult}
|
||||
*/
|
||||
@property({ attribute: false }) public appendRow?;
|
||||
|
||||
/**
|
||||
* Field with a unique id per entry in data.
|
||||
* @type {String}
|
||||
@@ -171,6 +177,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
.noDataText=${this.noDataText}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
.clickable=${this.clickable}
|
||||
.appendRow=${this.appendRow}
|
||||
>
|
||||
${!this.narrow
|
||||
? html`
|
||||
|
@@ -17,6 +17,7 @@ import { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
|
||||
class DialogAreaDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -154,6 +155,8 @@ class DialogAreaDetail extends LitElement {
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
|
||||
navigate(this, "/config/areas/dashboard");
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
|
@@ -15,12 +15,14 @@ import {
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
query,
|
||||
} from "lit-element";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { Action } from "../../../../data/script";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
@@ -103,6 +105,8 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
|
||||
@internalProperty() private _yamlMode = false;
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("action")) {
|
||||
return;
|
||||
@@ -111,6 +115,10 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
if (!this._uiModeAvailable && !this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
|
||||
if (this._yamlMode && this._yamlEditor) {
|
||||
this._yamlEditor.setValue(this.action);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -187,7 +195,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
<ul>
|
||||
${this._warnings.map((warning) => html`<li>${warning}</li>`)}
|
||||
</ul>
|
||||
You can still edit your config in yaml.
|
||||
You can still edit your config in YAML.
|
||||
</div>`
|
||||
: ""}
|
||||
${yamlMode
|
||||
|
@@ -19,12 +19,12 @@ import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor";
|
||||
import { ServiceAction } from "../../../../../data/script";
|
||||
import type { PolymerChangedEvent } from "../../../../../polymer-types";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { EntityId } from "../../../../lovelace/common/structs/is-entity-id";
|
||||
import { EntityIdOrAll } from "../../../../lovelace/common/structs/is-entity-id";
|
||||
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
|
||||
|
||||
const actionStruct = object({
|
||||
service: optional(string()),
|
||||
entity_id: optional(EntityId),
|
||||
entity_id: optional(EntityIdOrAll),
|
||||
data: optional(any()),
|
||||
});
|
||||
|
||||
|
@@ -39,7 +39,7 @@ export class HaWaitForTriggerAction extends LitElement
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${continue_on_timeout}
|
||||
.checked=${continue_on_timeout ?? true}
|
||||
@change=${this._continueChanged}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
|
@@ -18,13 +18,9 @@ import "@polymer/paper-input/paper-textarea";
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "./trigger/ha-automation-trigger";
|
||||
import "./condition/ha-automation-condition";
|
||||
import "./action/ha-automation-action";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import {
|
||||
BlueprintOrError,
|
||||
Blueprints,
|
||||
@@ -63,7 +59,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
|
||||
protected render() {
|
||||
const blueprint = this._blueprint;
|
||||
return html`<ha-config-section .isWide=${this.isWide}>
|
||||
return html`<ha-config-section vertical .isWide=${this.isWide}>
|
||||
${!this.narrow
|
||||
? html` <span slot="header">${this.config.alias}</span> `
|
||||
: ""}
|
||||
@@ -119,7 +115,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<ha-config-section vertical .isWide=${this.isWide}>
|
||||
<span slot="header"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.blueprint.header"
|
||||
@@ -144,11 +140,6 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
"ui.panel.config.automation.editor.blueprint.no_blueprints"
|
||||
)
|
||||
: html`<ha-circular-progress active></ha-circular-progress>`}
|
||||
<mwc-button @click=${this._navigateBlueprints}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.blueprint.manage_blueprints"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
|
||||
${this.config.use_blueprint.path
|
||||
@@ -157,41 +148,37 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
There is an error in this Blueprint: ${blueprint.error}
|
||||
</p>`
|
||||
: html`${blueprint?.metadata.description
|
||||
? html`<p>${blueprint.metadata.description}</p>`
|
||||
? html`<p class="card-content pre-line">
|
||||
${blueprint.metadata.description}
|
||||
</p>`
|
||||
: ""}
|
||||
${blueprint?.metadata?.input &&
|
||||
Object.keys(blueprint.metadata.input).length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.blueprint.inputs"
|
||||
)}
|
||||
</h3>
|
||||
${Object.entries(blueprint.metadata.input).map(
|
||||
([key, value]) =>
|
||||
html`<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">${value?.name || key}</span>
|
||||
<span slot="description"
|
||||
>${value?.description}</span
|
||||
>
|
||||
${value?.selector
|
||||
? html`<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${value.selector}
|
||||
.key=${key}
|
||||
.value=${(this.config.use_blueprint.input &&
|
||||
this.config.use_blueprint.input[key]) ||
|
||||
value?.default}
|
||||
@value-changed=${this._inputChanged}
|
||||
></ha-selector>`
|
||||
: html`<paper-input
|
||||
.key=${key}
|
||||
.value=${this.config.use_blueprint.input &&
|
||||
this.config.use_blueprint.input[key]}
|
||||
@value-changed=${this._inputChanged}
|
||||
no-label-float
|
||||
></paper-input>`}
|
||||
</ha-settings-row>`
|
||||
)}`
|
||||
? Object.entries(blueprint.metadata.input).map(
|
||||
([key, value]) =>
|
||||
html`<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">${value?.name || key}</span>
|
||||
<span slot="description">${value?.description}</span>
|
||||
${value?.selector
|
||||
? html`<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${value.selector}
|
||||
.key=${key}
|
||||
.value=${(this.config.use_blueprint.input &&
|
||||
this.config.use_blueprint.input[key]) ??
|
||||
value?.default}
|
||||
@value-changed=${this._inputChanged}
|
||||
></ha-selector>`
|
||||
: html`<paper-input
|
||||
.key=${key}
|
||||
required
|
||||
.value=${this.config.use_blueprint.input &&
|
||||
this.config.use_blueprint.input[key]}
|
||||
@value-changed=${this._inputChanged}
|
||||
no-label-float
|
||||
></paper-input>`}
|
||||
</ha-settings-row>`
|
||||
)
|
||||
: html`<p class="padding">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.blueprint.no_inputs"
|
||||
@@ -237,12 +224,18 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const input = { ...this.config.use_blueprint.input, [key]: value };
|
||||
|
||||
if (value === "" || value === undefined) {
|
||||
delete input[key];
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config!,
|
||||
use_blueprint: {
|
||||
...this.config.use_blueprint,
|
||||
input: { ...this.config.use_blueprint.input, [key]: value },
|
||||
input,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -267,25 +260,18 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _navigateBlueprints() {
|
||||
navigate(this, "/config/blueprint");
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
.padding {
|
||||
padding: 16px;
|
||||
}
|
||||
.pre-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
.blueprint-picker-container {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
h3 {
|
||||
margin: 16px;
|
||||
@@ -304,10 +290,10 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
:host(:not([narrow])) ha-settings-row paper-input {
|
||||
width: 50%;
|
||||
width: 60%;
|
||||
}
|
||||
:host(:not([narrow])) ha-settings-row ha-selector {
|
||||
width: 50%;
|
||||
width: 60%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -32,6 +32,7 @@ import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
|
||||
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||
import {
|
||||
AutomationConfig,
|
||||
AutomationEntity,
|
||||
@@ -206,6 +207,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
? html`<blueprint-automation-editor
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.stateObj=${stateObj}
|
||||
.config=${this._config}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -213,6 +215,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
: html`<manual-automation-editor
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.stateObj=${stateObj}
|
||||
.config=${this._config}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -392,9 +395,12 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
return cleanConfig;
|
||||
}
|
||||
|
||||
private async _copyYaml() {
|
||||
private async _copyYaml(): Promise<void> {
|
||||
if (this._editor?.yaml) {
|
||||
navigator.clipboard.writeText(this._editor.yaml);
|
||||
await copyToClipboard(this._editor.yaml);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,7 @@ import {
|
||||
internalProperty,
|
||||
query,
|
||||
TemplateResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "../../../components/ha-dialog";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
@@ -73,7 +74,9 @@ class DialogImportBlueprint extends LitElement {
|
||||
this._result.blueprint.metadata.domain
|
||||
)}
|
||||
<br />
|
||||
${this._result.blueprint.metadata.description}
|
||||
<p class="pre-line">
|
||||
${this._result.blueprint.metadata.description}
|
||||
</p>
|
||||
${this._result.validation_errors
|
||||
? html`
|
||||
<p class="error">
|
||||
@@ -104,7 +107,16 @@ class DialogImportBlueprint extends LitElement {
|
||||
<pre>${this._result.raw_data}</pre>
|
||||
</ha-expansion-panel>`
|
||||
: html`${this.hass.localize(
|
||||
"ui.panel.config.blueprint.add.import_introduction"
|
||||
"ui.panel.config.blueprint.add.import_introduction_link",
|
||||
"community_link",
|
||||
html`<a
|
||||
href="https://www.home-assistant.io/get-blueprints"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.blueprint.add.community_forums"
|
||||
)}</a
|
||||
>`
|
||||
)}<paper-input
|
||||
id="input"
|
||||
.label=${this.hass.localize(
|
||||
@@ -199,8 +211,15 @@ class DialogImportBlueprint extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return haStyleDialog;
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.pre-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import "../../../components/ha-fab";
|
||||
import "@material/mwc-icon-button";
|
||||
import { mdiPlus, mdiHelpCircle, mdiDelete, mdiRobot } from "@mdi/js";
|
||||
import { mdiHelpCircle, mdiDelete, mdiRobot, mdiDownload } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
CSSResult,
|
||||
@@ -112,7 +112,6 @@ class HaBlueprintOverview extends LitElement {
|
||||
create: {
|
||||
title: "",
|
||||
type: narrow ? "icon-button" : undefined,
|
||||
width: narrow ? undefined : "180px",
|
||||
template: (_, blueprint: any) =>
|
||||
blueprint.error
|
||||
? ""
|
||||
@@ -126,8 +125,9 @@ class HaBlueprintOverview extends LitElement {
|
||||
"ui.panel.config.blueprint.overview.use_blueprint"
|
||||
)}
|
||||
@click=${(ev) => this._createNew(ev)}
|
||||
><ha-svg-icon .path=${mdiRobot}></ha-svg-icon
|
||||
></mwc-icon-button>`
|
||||
>
|
||||
<ha-svg-icon .path=${mdiRobot}></ha-svg-icon>
|
||||
</mwc-icon-button>`
|
||||
: html`<mwc-button
|
||||
.blueprint=${blueprint}
|
||||
@click=${(ev) => this._createNew(ev)}
|
||||
@@ -170,6 +170,23 @@ class HaBlueprintOverview extends LitElement {
|
||||
"ui.panel.config.blueprint.overview.no_blueprints"
|
||||
)}
|
||||
hasFab
|
||||
.appendRow=${html` <div
|
||||
class="mdc-data-table__cell"
|
||||
style="width: 100%; text-align: center;"
|
||||
role="cell"
|
||||
>
|
||||
<a
|
||||
href="https://www.home-assistant.io/get-blueprints"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<mwc-button
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.blueprint.overview.discover_more"
|
||||
)}</mwc-button
|
||||
>
|
||||
</a>
|
||||
</div>`}
|
||||
>
|
||||
<mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}>
|
||||
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
|
||||
@@ -182,7 +199,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
extended
|
||||
@click=${this._addBlueprint}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
@@ -195,7 +212,10 @@ class HaBlueprintOverview extends LitElement {
|
||||
${this.hass.localize("ui.panel.config.blueprint.overview.introduction")}
|
||||
<p>
|
||||
<a
|
||||
href="${documentationUrl(this.hass, "/docs/blueprint/editor/")}"
|
||||
href="${documentationUrl(
|
||||
this.hass,
|
||||
"/docs/automation/using_blueprints/"
|
||||
)}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
@@ -92,7 +92,7 @@ class HaConfigCustomize extends LocalizeMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
_computeTabs() {
|
||||
return configSections.general;
|
||||
return configSections.advanced;
|
||||
}
|
||||
|
||||
computeEntities(hass) {
|
||||
|
@@ -8,7 +8,6 @@ import {
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
@@ -31,7 +30,7 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
|
||||
@property() public entities!: EntityRegistryStateEntry[];
|
||||
|
||||
@internalProperty() private _showDisabled = false;
|
||||
@property() public showDisabled = false;
|
||||
|
||||
private _entityRows: Array<LovelaceRow | HuiErrorCard> = [];
|
||||
|
||||
@@ -68,7 +67,7 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
})}
|
||||
</div>
|
||||
${disabledEntities.length
|
||||
? !this._showDisabled
|
||||
? !this.showDisabled
|
||||
? html`
|
||||
<button
|
||||
class="show-more"
|
||||
@@ -119,7 +118,7 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
}
|
||||
|
||||
private _toggleShowDisabled() {
|
||||
this._showDisabled = !this._showDisabled;
|
||||
this.showDisabled = !this.showDisabled;
|
||||
}
|
||||
|
||||
private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult {
|
||||
@@ -227,3 +226,9 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-device-entities-card": HaDeviceEntitiesCard;
|
||||
}
|
||||
}
|
||||
|
@@ -19,10 +19,11 @@ import {
|
||||
|
||||
import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
import { PolymerChangedEvent } from "../../../../polymer-types";
|
||||
import { computeDeviceName } from "../../../../data/device_registry";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
|
||||
@customElement("dialog-device-registry-detail")
|
||||
class DialogDeviceRegistryDetail extends LitElement {
|
||||
@@ -36,6 +37,8 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
|
||||
@internalProperty() private _areaId?: string;
|
||||
|
||||
@internalProperty() private _disabledBy!: string | null;
|
||||
|
||||
@internalProperty() private _submitting?: boolean;
|
||||
|
||||
public async showDialog(
|
||||
@@ -45,6 +48,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
this._error = undefined;
|
||||
this._nameByUser = this._params.device.name_by_user || "";
|
||||
this._areaId = this._params.device.area_id;
|
||||
this._disabledBy = this._params.device.disabled_by;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
@@ -80,6 +84,32 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
.value=${this._areaId}
|
||||
@value-changed=${this._areaPicked}
|
||||
></ha-area-picker>
|
||||
<div class="row">
|
||||
<ha-switch
|
||||
.checked=${!this._disabledBy}
|
||||
@change=${this._disabledByChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
<div>
|
||||
<div>
|
||||
${this.hass.localize("ui.panel.config.devices.enabled_label")}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this._disabledBy && this._disabledBy !== "user"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.devices.enabled_cause",
|
||||
"cause",
|
||||
this.hass.localize(
|
||||
`config_entry.disabled_by.${this._disabledBy}`
|
||||
)
|
||||
)
|
||||
: ""}
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.enabled_description"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button
|
||||
@@ -109,12 +139,17 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
this._areaId = event.detail.value;
|
||||
}
|
||||
|
||||
private _disabledByChanged(ev: Event): void {
|
||||
this._disabledBy = (ev.target as HaSwitch).checked ? null : "user";
|
||||
}
|
||||
|
||||
private async _updateEntry(): Promise<void> {
|
||||
this._submitting = true;
|
||||
try {
|
||||
await this._params!.updateEntry({
|
||||
name_by_user: this._nameByUser.trim() || null,
|
||||
area_id: this._areaId || null,
|
||||
disabled_by: this._disabledBy || null,
|
||||
});
|
||||
this._params = undefined;
|
||||
} catch (err) {
|
||||
@@ -128,6 +163,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
.form {
|
||||
@@ -139,6 +175,15 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
ha-switch {
|
||||
margin-right: 16px;
|
||||
}
|
||||
.row {
|
||||
margin-top: 8px;
|
||||
color: var(--primary-text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -46,6 +46,7 @@ import "./device-detail/ha-device-entities-card";
|
||||
import "./device-detail/ha-device-info-card";
|
||||
import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
|
||||
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
|
||||
stateName?: string | null;
|
||||
@@ -246,6 +247,28 @@ export class HaConfigDevicePage extends LitElement {
|
||||
.devices=${this.devices}
|
||||
.device=${device}
|
||||
>
|
||||
${
|
||||
device.disabled_by
|
||||
? html`
|
||||
<div>
|
||||
<p class="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.enabled_cause",
|
||||
"cause",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.disabled_by.${device.disabled_by}`
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions" slot="actions">
|
||||
<mwc-button unelevated @click=${this._enableDevice}>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: html``
|
||||
}
|
||||
${this._renderIntegrationInfo(device, integrations)}
|
||||
</ha-device-info-card>
|
||||
|
||||
@@ -255,6 +278,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
<ha-device-entities-card
|
||||
.hass=${this.hass}
|
||||
.entities=${entities}
|
||||
.showDisabled=${device.disabled_by !== null}
|
||||
>
|
||||
</ha-device-entities-card>
|
||||
`
|
||||
@@ -272,9 +296,14 @@ export class HaConfigDevicePage extends LitElement {
|
||||
)}
|
||||
<ha-icon-button
|
||||
@click=${this._showAutomationDialog}
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.create"
|
||||
)}
|
||||
.disabled=${device.disabled_by}
|
||||
title=${device.disabled_by
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.devices.automation.create_disabled"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.devices.automation.create"
|
||||
)}
|
||||
icon="hass:plus-circle"
|
||||
></ha-icon-button>
|
||||
</h1>
|
||||
@@ -342,9 +371,16 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
<ha-icon-button
|
||||
@click=${this._createScene}
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.create"
|
||||
)}
|
||||
.disabled=${device.disabled_by}
|
||||
title=${
|
||||
device.disabled_by
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.devices.scene.create_disabled"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.devices.scene.create"
|
||||
)
|
||||
}
|
||||
icon="hass:plus-circle"
|
||||
></ha-icon-button>
|
||||
</h1>
|
||||
@@ -415,9 +451,14 @@ export class HaConfigDevicePage extends LitElement {
|
||||
)}
|
||||
<ha-icon-button
|
||||
@click=${this._showScriptDialog}
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.create"
|
||||
)}
|
||||
.disabled=${device.disabled_by}
|
||||
title=${device.disabled_by
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.devices.script.create_disabled"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.devices.script.create"
|
||||
)}
|
||||
icon="hass:plus-circle"
|
||||
></ha-icon-button>
|
||||
</h1>
|
||||
@@ -632,128 +673,137 @@ export class HaConfigDevicePage extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: auto;
|
||||
max-width: 1000px;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
private async _enableDevice(): Promise<void> {
|
||||
await updateDeviceRegistryEntry(this.hass, this.deviceId, {
|
||||
disabled_by: null,
|
||||
});
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: auto;
|
||||
max-width: 1000px;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.card-header ha-icon-button {
|
||||
margin-right: -8px;
|
||||
color: var(--primary-color);
|
||||
height: auto;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
padding: 16px;
|
||||
}
|
||||
.card-header ha-icon-button {
|
||||
margin-right: -8px;
|
||||
color: var(--primary-color);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.show-more {
|
||||
}
|
||||
.device-info {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 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);
|
||||
}
|
||||
.show-more {
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
h1 {
|
||||
margin: 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);
|
||||
}
|
||||
|
||||
.column,
|
||||
.fullwidth {
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.column {
|
||||
width: 33%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.fullwidth {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
align-self: center;
|
||||
}
|
||||
.column,
|
||||
.fullwidth {
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.column {
|
||||
width: 33%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.fullwidth {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.header-right img {
|
||||
height: 30px;
|
||||
}
|
||||
.header-right {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
}
|
||||
.header-right img {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.header-right:first-child {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.header-right {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header-right > *:not(:first-child) {
|
||||
margin-left: 16px;
|
||||
}
|
||||
.header-right:first-child {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.battery {
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
.header-right > *:not(:first-child) {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.column > *:not(:first-child) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.battery {
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:host([narrow]) .column {
|
||||
width: 100%;
|
||||
}
|
||||
.column > *:not(:first-child) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:host([narrow]) .container {
|
||||
margin-top: 0;
|
||||
}
|
||||
:host([narrow]) .column {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
font-size: var(--paper-font-body1_-_font-size);
|
||||
}
|
||||
:host([narrow]) .container {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
paper-item.no-link {
|
||||
cursor: default;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
font-size: var(--paper-font-body1_-_font-size);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
paper-item.no-link {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
ha-card a {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
`;
|
||||
ha-card {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
ha-card a {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,9 @@
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import { mdiPlus, mdiFilterVariant, mdiCancel } from "@mdi/js";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
@@ -7,7 +11,9 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
@@ -18,6 +24,7 @@ import {
|
||||
RowClickedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/entity/ha-battery-icon";
|
||||
import "../../../components/ha-button-menu";
|
||||
import { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { ConfigEntry } from "../../../data/config_entries";
|
||||
import {
|
||||
@@ -34,6 +41,7 @@ import { domainToName } from "../../../data/integration";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
|
||||
interface DeviceRowData extends DeviceRegistryEntry {
|
||||
device?: DeviceRowData;
|
||||
@@ -64,6 +72,12 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
window.location.search
|
||||
);
|
||||
|
||||
@internalProperty() private _showDisabled = false;
|
||||
|
||||
@internalProperty() private _filter = "";
|
||||
|
||||
@internalProperty() private _numHiddenDevices = 0;
|
||||
|
||||
private _activeFilters = memoizeOne(
|
||||
(
|
||||
entries: ConfigEntry[],
|
||||
@@ -74,6 +88,10 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
filters.forEach((value, key) => {
|
||||
switch (key) {
|
||||
case "config_entry": {
|
||||
// If we are requested to show the devices for a given config entry,
|
||||
// also show the disabled ones by default.
|
||||
this._showDisabled = true;
|
||||
|
||||
const configEntry = entries.find(
|
||||
(entry) => entry.entry_id === value
|
||||
);
|
||||
@@ -105,6 +123,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
entities: EntityRegistryEntry[],
|
||||
areas: AreaRegistryEntry[],
|
||||
filters: URLSearchParams,
|
||||
showDisabled: boolean,
|
||||
localize: LocalizeFunc
|
||||
) => {
|
||||
// Some older installations might have devices pointing at invalid entryIDs
|
||||
@@ -117,6 +136,9 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
deviceLookup[device.id] = device;
|
||||
}
|
||||
|
||||
// If nothing gets filtered, this is our correct count of devices
|
||||
let startLength = outputDevices.length;
|
||||
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
@@ -145,6 +167,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
outputDevices = outputDevices.filter((device) =>
|
||||
device.config_entries.includes(value)
|
||||
);
|
||||
startLength = outputDevices.length;
|
||||
const configEntry = entries.find((entry) => entry.entry_id === value);
|
||||
if (configEntry) {
|
||||
filterDomains.push(configEntry.domain);
|
||||
@@ -152,6 +175,10 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
}
|
||||
});
|
||||
|
||||
if (!showDisabled) {
|
||||
outputDevices = outputDevices.filter((device) => !device.disabled_by);
|
||||
}
|
||||
|
||||
outputDevices = outputDevices.map((device) => {
|
||||
return {
|
||||
...device,
|
||||
@@ -162,9 +189,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
),
|
||||
model: device.model || "<unknown>",
|
||||
manufacturer: device.manufacturer || "<unknown>",
|
||||
area: device.area_id
|
||||
? areaLookup[device.area_id].name
|
||||
: this.hass.localize("ui.panel.config.devices.data_table.no_area"),
|
||||
area: device.area_id ? areaLookup[device.area_id].name : undefined,
|
||||
integration: device.config_entries.length
|
||||
? device.config_entries
|
||||
.filter((entId) => entId in entryLookup)
|
||||
@@ -182,16 +207,19 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
};
|
||||
});
|
||||
|
||||
this._numHiddenDevices = startLength - outputDevices.length;
|
||||
return { devicesOutput: outputDevices, filteredDomains: filterDomains };
|
||||
}
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer => {
|
||||
(narrow: boolean, showDisabled: boolean): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = narrow
|
||||
? {
|
||||
name: {
|
||||
title: "Device",
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.device"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
@@ -277,6 +305,24 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
: html` - `;
|
||||
},
|
||||
};
|
||||
if (showDisabled) {
|
||||
columns.disabled_by = {
|
||||
title: "",
|
||||
type: "icon",
|
||||
template: (disabled_by) =>
|
||||
disabled_by
|
||||
? html`<div
|
||||
tabindex="0"
|
||||
style="display:inline-block; position: relative;"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiCancel}></ha-svg-icon>
|
||||
<paper-tooltip animation-delay="0" position="left">
|
||||
${this.hass.localize("ui.panel.config.devices.disabled")}
|
||||
</paper-tooltip>
|
||||
</div>`
|
||||
: "",
|
||||
};
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
@@ -298,9 +344,119 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
this.entities,
|
||||
this.areas,
|
||||
this._searchParms,
|
||||
this._showDisabled,
|
||||
this.hass.localize
|
||||
);
|
||||
const includeZHAFab = filteredDomains.includes("zha");
|
||||
const activeFilters = this._activeFilters(
|
||||
this.entries,
|
||||
this._searchParms,
|
||||
this.hass.localize
|
||||
);
|
||||
|
||||
const headerToolbar = html`
|
||||
<search-input
|
||||
no-label-float
|
||||
no-underline
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.devices.picker.search")}
|
||||
></search-input
|
||||
>${activeFilters
|
||||
? html`<div class="active-filters">
|
||||
${this.narrow
|
||||
? html` <div>
|
||||
<ha-icon icon="hass:filter-variant"></ha-icon>
|
||||
<paper-tooltip animation-delay="0" position="left">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.filtering.filtering_by"
|
||||
)}
|
||||
${activeFilters.join(", ")}
|
||||
${this._numHiddenDevices
|
||||
? "(" +
|
||||
this.hass.localize(
|
||||
"ui.panel.config.devices.picker.filter.hidden_devices",
|
||||
"number",
|
||||
this._numHiddenDevices
|
||||
) +
|
||||
")"
|
||||
: ""}
|
||||
</paper-tooltip>
|
||||
</div>`
|
||||
: `${this.hass.localize(
|
||||
"ui.panel.config.filtering.filtering_by"
|
||||
)} ${activeFilters.join(", ")}
|
||||
${
|
||||
this._numHiddenDevices
|
||||
? "(" +
|
||||
this.hass.localize(
|
||||
"ui.panel.config.devices.picker.filter.hidden_devices",
|
||||
"number",
|
||||
this._numHiddenDevices
|
||||
) +
|
||||
")"
|
||||
: ""
|
||||
}
|
||||
`}
|
||||
<mwc-button @click=${this._clearFilter}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.filtering.clear"
|
||||
)}</mwc-button
|
||||
>
|
||||
</div>`
|
||||
: ""}
|
||||
${this._numHiddenDevices && !activeFilters
|
||||
? html`<div class="active-filters">
|
||||
${this.narrow
|
||||
? html` <div>
|
||||
<ha-icon icon="hass:filter-variant"></ha-icon>
|
||||
<paper-tooltip animation-delay="0" position="left">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.filter.hidden_devices",
|
||||
"number",
|
||||
this._numHiddenDevices
|
||||
)}
|
||||
</paper-tooltip>
|
||||
</div>`
|
||||
: `${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.filter.hidden_devices",
|
||||
"number",
|
||||
this._numHiddenDevices
|
||||
)}`}
|
||||
<mwc-button @click=${this._showAll}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.filter.show_all"
|
||||
)}</mwc-button
|
||||
>
|
||||
</div>`
|
||||
: ""}
|
||||
<ha-button-menu corner="BOTTOM_START" multi>
|
||||
<mwc-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.devices.picker.filter.filter"
|
||||
)}
|
||||
.title=${this.hass!.localize(
|
||||
"ui.panel.config.devices.picker.filter.filter"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiFilterVariant}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-list-item
|
||||
@request-selected="${this._showDisabledChanged}"
|
||||
graphic="control"
|
||||
.selected=${this._showDisabled}
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="graphic"
|
||||
.checked=${this._showDisabled}
|
||||
></ha-checkbox>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.devices.picker.filter.show_disabled"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
`;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
@@ -311,13 +467,9 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
: "/config"}
|
||||
.tabs=${configSections.integrations}
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.columns=${this._columns(this.narrow, this._showDisabled)}
|
||||
.data=${devicesOutput}
|
||||
.activeFilters=${this._activeFilters(
|
||||
this.entries,
|
||||
this._searchParms,
|
||||
this.hass.localize
|
||||
)}
|
||||
.filter=${this._filter}
|
||||
@row-click=${this._handleRowClicked}
|
||||
clickable
|
||||
.hasFab=${includeZHAFab}
|
||||
@@ -333,6 +485,15 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
</ha-fab>
|
||||
</a>`
|
||||
: html``}
|
||||
<div
|
||||
class=${classMap({
|
||||
"search-toolbar": this.narrow,
|
||||
"table-header": !this.narrow,
|
||||
})}
|
||||
slot="header"
|
||||
>
|
||||
${headerToolbar}
|
||||
</div>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
@@ -363,6 +524,136 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
const deviceId = ev.detail.id;
|
||||
navigate(this, `/config/devices/device/${deviceId}`);
|
||||
}
|
||||
|
||||
private _showDisabledChanged(ev: CustomEvent<RequestSelectedDetail>) {
|
||||
if (ev.detail.source !== "property") {
|
||||
return;
|
||||
}
|
||||
this._showDisabled = ev.detail.selected;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _clearFilter() {
|
||||
navigate(this, window.location.pathname, true);
|
||||
}
|
||||
|
||||
private _showAll() {
|
||||
this._showDisabled = true;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
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);
|
||||
}
|
||||
h2 {
|
||||
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);
|
||||
}
|
||||
p {
|
||||
font-family: var(--paper-font-subhead_-_font-family);
|
||||
-webkit-font-smoothing: var(
|
||||
--paper-font-subhead_-_-webkit-font-smoothing
|
||||
);
|
||||
font-weight: var(--paper-font-subhead_-_font-weight);
|
||||
line-height: var(--paper-font-subhead_-_line-height);
|
||||
}
|
||||
ha-data-table {
|
||||
width: 100%;
|
||||
--data-table-border-width: 0;
|
||||
}
|
||||
:host(:not([narrow])) ha-data-table {
|
||||
height: calc(100vh - 1px - var(--header-height));
|
||||
display: block;
|
||||
}
|
||||
ha-button-menu {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
}
|
||||
search-input {
|
||||
margin-left: 16px;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
.search-toolbar search-input {
|
||||
margin-left: 8px;
|
||||
top: 1px;
|
||||
}
|
||||
.search-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.search-toolbar ha-button-menu {
|
||||
position: static;
|
||||
}
|
||||
.selected-txt {
|
||||
font-weight: bold;
|
||||
padding-left: 16px;
|
||||
}
|
||||
.table-header .selected-txt {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.search-toolbar .selected-txt {
|
||||
font-size: 16px;
|
||||
}
|
||||
.header-btns > mwc-button,
|
||||
.header-btns > ha-icon-button {
|
||||
margin: 8px;
|
||||
}
|
||||
.active-filters {
|
||||
color: var(--primary-text-color);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 2px 2px 8px;
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.active-filters ha-icon {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.active-filters mwc-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.active-filters::before {
|
||||
background-color: var(--primary-color);
|
||||
opacity: 0.12;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
content: "";
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -111,10 +111,19 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
return html`
|
||||
${!stateObj
|
||||
? html`
|
||||
<div class="container">
|
||||
<div class="container warning">
|
||||
${this.hass!.localize(
|
||||
"ui.dialogs.entity_registry.editor.unavailable"
|
||||
)}
|
||||
${this._device?.disabled_by
|
||||
? html`<br />${this.hass!.localize(
|
||||
"ui.dialogs.entity_registry.editor.device_disabled"
|
||||
)}<br /><mwc-button @click=${this._openDeviceSettings}>
|
||||
${this.hass!.localize(
|
||||
"ui.dialogs.entity_registry.editor.open_device_settings"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
@@ -161,6 +170,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
<div class="row">
|
||||
<ha-switch
|
||||
.checked=${!this._disabledBy}
|
||||
.disabled=${this._device?.disabled_by}
|
||||
@change=${this._disabledByChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
|
@@ -189,9 +189,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
? (name, entity: any) =>
|
||||
html`
|
||||
${name}<br />
|
||||
${entity.entity_id} |
|
||||
${this.hass.localize(`component.${entity.platform}.title`) ||
|
||||
entity.platform}
|
||||
<div class="secondary">
|
||||
${entity.entity_id} |
|
||||
${this.hass.localize(`component.${entity.platform}.title`) ||
|
||||
entity.platform}
|
||||
</div>
|
||||
`
|
||||
: undefined,
|
||||
},
|
||||
|
@@ -5,6 +5,8 @@ import { classMap } from "lit-html/directives/class-map";
|
||||
export class HaConfigSection extends LitElement {
|
||||
@property() public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public vertical = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -16,8 +18,8 @@ export class HaConfigSection extends LitElement {
|
||||
<div
|
||||
class="together layout ${classMap({
|
||||
narrow: !this.isWide,
|
||||
vertical: !this.isWide,
|
||||
horizontal: this.isWide,
|
||||
vertical: this.vertical || !this.isWide,
|
||||
horizontal: !this.vertical && this.isWide,
|
||||
})}"
|
||||
>
|
||||
<div class="intro"><slot name="introduction"></slot></div>
|
||||
|
@@ -148,7 +148,7 @@ export class HaConfigHelpers extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automation}
|
||||
.tabs=${configSections.helpers}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._getItems(this._stateItems)}
|
||||
@row-click=${this._openEditDialog}
|
||||
|
@@ -197,7 +197,7 @@ class SystemHealthCard extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _copyInfo(ev: CustomEvent<ActionDetail>): void {
|
||||
private async _copyInfo(ev: CustomEvent<ActionDetail>): Promise<void> {
|
||||
const github = ev.detail.index === 1;
|
||||
let haContent: string | undefined;
|
||||
const domainParts: string[] = [];
|
||||
@@ -250,13 +250,15 @@ class SystemHealthCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
copyToClipboard(
|
||||
await copyToClipboard(
|
||||
`${github ? "## " : ""}System Health\n${haContent}\n\n${domainParts.join(
|
||||
"\n\n"
|
||||
)}`
|
||||
);
|
||||
|
||||
showToast(this, { message: this.hass.localize("ui.common.copied") });
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
|
@@ -141,11 +141,15 @@ class OZWNodeDashboard extends LitElement {
|
||||
${this._metadata.metadata.ResetHelp}
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card class="content" header="WakeUp">
|
||||
<div class="card-content">
|
||||
${this._metadata.metadata.WakeupHelp}
|
||||
</div>
|
||||
</ha-card>
|
||||
${this._metadata.metadata.WakeupHelp
|
||||
? html`
|
||||
<ha-card class="content" header="WakeUp">
|
||||
<div class="card-content">
|
||||
${this._metadata.metadata.WakeupHelp}
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: ``}
|
||||
`
|
||||
: ``}
|
||||
`
|
||||
@@ -199,6 +203,10 @@ class OZWNodeDashboard extends LitElement {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.content:last-child {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
position: relative;
|
||||
padding-right: 40px;
|
||||
|
@@ -49,19 +49,17 @@ class ZHADevicePairingStatusCard extends LitElement {
|
||||
class="discovered ${classMap({
|
||||
initialized: this.device.pairing_status === INITIALIZED,
|
||||
})}"
|
||||
><div
|
||||
class="header"
|
||||
>
|
||||
<h1>
|
||||
><div class="header">
|
||||
<h4>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.zha.device_pairing_card.${this.device.pairing_status}`
|
||||
)}
|
||||
</h1>
|
||||
<h4>
|
||||
</h4>
|
||||
<h1>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.zha.device_pairing_card.${this.device.pairing_status}_status_text`
|
||||
)}
|
||||
</h4>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
${[INTERVIEW_COMPLETE, CONFIGURED].includes(
|
||||
|
@@ -15,6 +15,12 @@ import { fetchDevices, ZHADevice } from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { Network, Edge, Node, EdgeOptions } from "vis-network";
|
||||
import "../../../../../common/search/search-input";
|
||||
import "../../../../../components/ha-button-menu";
|
||||
import "../../../../../components/device/ha-device-picker";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import { formatAsPaddedHex } from "./functions";
|
||||
import { PolymerChangedEvent } from "../../../../../polymer-types";
|
||||
|
||||
@customElement("zha-network-visualization-page")
|
||||
export class ZHANetworkVisualizationPage extends LitElement {
|
||||
@@ -28,9 +34,21 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
@internalProperty()
|
||||
private _devices: Map<string, ZHADevice> = new Map();
|
||||
|
||||
@internalProperty()
|
||||
private _devicesByDeviceId: Map<string, ZHADevice> = new Map();
|
||||
|
||||
@internalProperty()
|
||||
private _nodes: Node[] = [];
|
||||
|
||||
@internalProperty()
|
||||
private _network?: Network;
|
||||
|
||||
@internalProperty()
|
||||
private _filter?: string;
|
||||
|
||||
@internalProperty()
|
||||
private _zoomedDeviceId?: string;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.hass) {
|
||||
@@ -91,6 +109,27 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
"ui.panel.config.zha.visualization.header"
|
||||
)}
|
||||
>
|
||||
<div class="table-header">
|
||||
<search-input
|
||||
no-label-float
|
||||
no-underline
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zha.visualization.highlight_label"
|
||||
)}
|
||||
>
|
||||
</search-input>
|
||||
<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._zoomedDeviceId}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zha.visualization.zoom_label"
|
||||
)}
|
||||
.includeDomains="['zha']"
|
||||
@value-changed=${this._zoomToDevice}
|
||||
></ha-device-picker>
|
||||
</div>
|
||||
<div id="visualization"></div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
@@ -101,15 +140,18 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
this._devices = new Map(
|
||||
devices.map((device: ZHADevice) => [device.ieee, device])
|
||||
);
|
||||
this._devicesByDeviceId = new Map(
|
||||
devices.map((device: ZHADevice) => [device.device_reg_id, device])
|
||||
);
|
||||
this._updateDevices(devices);
|
||||
}
|
||||
|
||||
private _updateDevices(devices: ZHADevice[]) {
|
||||
const nodes: Node[] = [];
|
||||
this._nodes = [];
|
||||
const edges: Edge[] = [];
|
||||
|
||||
devices.forEach((device) => {
|
||||
nodes.push({
|
||||
this._nodes.push({
|
||||
id: device.ieee,
|
||||
label: this._buildLabel(device),
|
||||
shape: this._getShape(device),
|
||||
@@ -137,7 +179,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
}
|
||||
});
|
||||
|
||||
this._network?.setData({ nodes: nodes, edges: edges });
|
||||
this._network?.setData({ nodes: this._nodes, edges: edges });
|
||||
}
|
||||
|
||||
private _getLQI(lqi: number): EdgeOptions["color"] {
|
||||
@@ -181,7 +223,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
label += `<b>IEEE: </b>${device.ieee}`;
|
||||
label += `\n<b>Device Type: </b>${device.device_type.replace("_", " ")}`;
|
||||
if (device.nwk != null) {
|
||||
label += `\n<b>NWK: </b>${device.nwk}`;
|
||||
label += `\n<b>NWK: </b>${formatAsPaddedHex(device.nwk)}`;
|
||||
}
|
||||
if (device.manufacturer != null && device.model != null) {
|
||||
label += `\n<b>Device: </b>${device.manufacturer} ${device.model}`;
|
||||
@@ -194,6 +236,56 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
return label;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
const filterText = this._filter!.toLowerCase();
|
||||
if (!this._network) {
|
||||
return;
|
||||
}
|
||||
if (this._filter) {
|
||||
const filteredNodeIds: (string | number)[] = [];
|
||||
this._nodes.forEach((node) => {
|
||||
if (node.label && node.label.toLowerCase().includes(filterText)) {
|
||||
filteredNodeIds.push(node.id!);
|
||||
}
|
||||
});
|
||||
this._zoomedDeviceId = "";
|
||||
this._zoomOut();
|
||||
this._network.selectNodes(filteredNodeIds, true);
|
||||
} else {
|
||||
this._network.unselectAll();
|
||||
}
|
||||
}
|
||||
|
||||
private _zoomToDevice(event: PolymerChangedEvent<string>) {
|
||||
event.stopPropagation();
|
||||
this._zoomedDeviceId = event.detail.value;
|
||||
if (!this._network) {
|
||||
return;
|
||||
}
|
||||
this._filter = "";
|
||||
if (!this._zoomedDeviceId) {
|
||||
this._zoomOut();
|
||||
} else {
|
||||
const device: ZHADevice | undefined = this._devicesByDeviceId.get(
|
||||
this._zoomedDeviceId
|
||||
);
|
||||
if (device) {
|
||||
this._network.fit({
|
||||
nodes: [device.ieee],
|
||||
animation: { duration: 500, easingFunction: "easeInQuad" },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _zoomOut() {
|
||||
this._network!.fit({
|
||||
nodes: [],
|
||||
animation: { duration: 500, easingFunction: "easeOutQuad" },
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
css`
|
||||
@@ -208,6 +300,30 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
line-height: var(--paper-font-display1_-_line-height);
|
||||
opacity: var(--dark-primary-opacity);
|
||||
}
|
||||
.table-header {
|
||||
border-bottom: 1px solid --divider-color;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--header-height);
|
||||
}
|
||||
.search-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
padding: 0 16px;
|
||||
}
|
||||
search-input {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
flex: 1;
|
||||
}
|
||||
search-input.header {
|
||||
left: -8px;
|
||||
}
|
||||
ha-device-picker {
|
||||
flex: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user