mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-01 13:37:47 +00:00
commit
f5022f4e1e
@ -69,7 +69,7 @@ class DemoAlarmPanelEntity extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
@ -74,7 +74,7 @@ class DemoConditional extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
@ -188,7 +188,7 @@ class DemoEntities extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
@ -89,7 +89,7 @@ class DemoEntityButtonEntity extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
@ -105,7 +105,7 @@ class DemoFilter extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
@ -89,10 +89,10 @@ const CONFIGS = [
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Custom column width",
|
||||
heading: "Custom number of columns",
|
||||
config: `
|
||||
- type: glance
|
||||
column_width: calc(100% / 7)
|
||||
columns: 7
|
||||
entities:
|
||||
- device_tracker.demo_paulus
|
||||
- media_player.living_room
|
||||
@ -230,7 +230,7 @@ class DemoPicEntity extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
@ -38,7 +38,7 @@ class DemoLightEntity extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
@ -139,7 +139,7 @@ class DemoMap extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
@ -95,7 +95,7 @@ class DemoHuiMediaPlayerRows extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
@ -93,7 +93,7 @@ class DemoPicElements extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
@ -36,7 +36,7 @@ class DemoShoppingListEntity extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
|
@ -104,7 +104,7 @@ class DemoStack extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
@ -75,7 +75,7 @@ class DemoThermostatEntity extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
@ -8,16 +8,7 @@ import getEntity from "../data/entity";
|
||||
import provideHass from "../data/provide_hass";
|
||||
|
||||
import "../components/demo-more-infos";
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
const SUPPORT_BRIGHTNESS = 1;
|
||||
const SUPPORT_COLOR_TEMP = 2;
|
||||
const SUPPORT_EFFECT = 4;
|
||||
const SUPPORT_FLASH = 8;
|
||||
const SUPPORT_COLOR = 16;
|
||||
const SUPPORT_TRANSITION = 32;
|
||||
const SUPPORT_WHITE_VALUE = 128;
|
||||
import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("light", "bed_light", "on", {
|
||||
@ -49,7 +40,7 @@ class DemoMoreInfoLight extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this);
|
||||
hass.addEntities(ENTITIES);
|
79
gallery/src/demos/demo-util-long-press.ts
Normal file
79
gallery/src/demos/demo-util-long-press.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
|
||||
import "../../../src/components/ha-card";
|
||||
import { longPress } from "../../../src/panels/lovelace/common/directives/long-press-directive";
|
||||
|
||||
export class DemoUtilLongPress extends LitElement {
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
${
|
||||
[1, 2, 3].map(
|
||||
() => html`
|
||||
<ha-card>
|
||||
<paper-button
|
||||
@ha-click="${this._handleTap}"
|
||||
@ha-hold="${this._handleHold}"
|
||||
.longPress="${longPress()}"
|
||||
>
|
||||
(long) press me!
|
||||
</paper-button>
|
||||
|
||||
<textarea></textarea>
|
||||
|
||||
<div>(try pressing and scrolling too!)</div>
|
||||
</ha-card>
|
||||
`
|
||||
)
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleTap(ev: Event) {
|
||||
this._addValue(ev, "tap");
|
||||
}
|
||||
|
||||
private _handleHold(ev: Event) {
|
||||
this._addValue(ev, "hold");
|
||||
}
|
||||
|
||||
private _addValue(ev: Event, value: string) {
|
||||
const area = (ev.currentTarget as HTMLElement)
|
||||
.nextElementSibling! as HTMLTextAreaElement;
|
||||
const now = new Date().toTimeString().split(" ")[0];
|
||||
area.value += `${now}: ${value}\n`;
|
||||
area.scrollTop = area.scrollHeight;
|
||||
}
|
||||
|
||||
private renderStyle() {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
width: 200px;
|
||||
margin: calc(42vh - 140px) auto;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
ha-card:first-of-type {
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-card:last-of-type {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
paper-button {
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-util-long-press", DemoUtilLongPress);
|
@ -11,7 +11,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../src/managers/notification-manager";
|
||||
|
||||
const DEMOS = require.context("./demos", true, /^(.*\.(js$))[^.]*$/im);
|
||||
const DEMOS = require.context("./demos", true, /^(.*\.(ts$))[^.]*$/im);
|
||||
|
||||
const fixPath = (path) => path.substr(2, path.length - 5);
|
||||
|
||||
@ -118,6 +118,22 @@ class HaGallery extends PolymerElement {
|
||||
</a>
|
||||
</template>
|
||||
</paper-card>
|
||||
|
||||
<paper-card heading="Util demos">
|
||||
<div class='card-content intro'>
|
||||
<p>
|
||||
Test pages for our utility functions.
|
||||
</p>
|
||||
</div>
|
||||
<template is='dom-repeat' items='[[_utilDemos]]'>
|
||||
<a href='#[[item]]'>
|
||||
<paper-item>
|
||||
<paper-item-body>{{ item }}</paper-item-body>
|
||||
<iron-icon icon="hass:chevron-right"></iron-icon>
|
||||
</paper-item>
|
||||
</a>
|
||||
</template>
|
||||
</paper-card>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@ -145,6 +161,10 @@ class HaGallery extends PolymerElement {
|
||||
type: Array,
|
||||
computed: "_computeMoreInfos(_demos)",
|
||||
},
|
||||
_utilDemos: {
|
||||
type: Array,
|
||||
computed: "_computeUtil(_demos)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -178,7 +198,7 @@ class HaGallery extends PolymerElement {
|
||||
while (root.lastChild) root.removeChild(root.lastChild);
|
||||
|
||||
if (demo) {
|
||||
DEMOS(`./${demo}.js`);
|
||||
DEMOS(`./${demo}.ts`);
|
||||
const el = document.createElement(demo);
|
||||
root.appendChild(el);
|
||||
}
|
||||
@ -199,6 +219,10 @@ class HaGallery extends PolymerElement {
|
||||
_computeMoreInfos(demos) {
|
||||
return demos.filter((demo) => demo.includes("more-info"));
|
||||
}
|
||||
|
||||
_computeUtil(demos) {
|
||||
return demos.filter((demo) => demo.includes("util"));
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-gallery", HaGallery);
|
||||
|
@ -2,6 +2,7 @@ import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
|
||||
|
||||
import "../../../src/resources/ha-style";
|
||||
|
||||
@ -15,10 +16,13 @@ class HassioAddonLogs extends PolymerElement {
|
||||
}
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
${ANSI_HTML_STYLE}
|
||||
<paper-card heading="Log">
|
||||
<div class="card-content"><pre>[[log]]</pre></div>
|
||||
<div class="card-content" id="content"></div>
|
||||
<div class="card-actions">
|
||||
<paper-button on-click="refresh">Refresh</paper-button>
|
||||
</div>
|
||||
@ -33,7 +37,6 @@ class HassioAddonLogs extends PolymerElement {
|
||||
type: String,
|
||||
observer: "addonSlugChanged",
|
||||
},
|
||||
log: String,
|
||||
};
|
||||
}
|
||||
|
||||
@ -51,8 +54,11 @@ class HassioAddonLogs extends PolymerElement {
|
||||
refresh() {
|
||||
this.hass
|
||||
.callApi("get", `hassio/addons/${this.addonSlug}/logs`)
|
||||
.then((info) => {
|
||||
this.log = info;
|
||||
.then((text) => {
|
||||
while (this.$.content.lastChild) {
|
||||
this.$.content.removeChild(this.$.content.lastChild);
|
||||
}
|
||||
this.$.content.appendChild(parseTextToColoredPre(text));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
203
hassio/src/ansi-to-html.js
Normal file
203
hassio/src/ansi-to-html.js
Normal file
@ -0,0 +1,203 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
|
||||
export const ANSI_HTML_STYLE = html`
|
||||
<style>
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.underline.strikethrough {
|
||||
text-decoration: underline line-through;
|
||||
}
|
||||
.fg-red {
|
||||
color: rgb(222, 56, 43);
|
||||
}
|
||||
.fg-green {
|
||||
color: rgb(57, 181, 74);
|
||||
}
|
||||
.fg-yellow {
|
||||
color: rgb(255, 199, 6);
|
||||
}
|
||||
.fg-blue {
|
||||
color: rgb(0, 111, 184);
|
||||
}
|
||||
.fg-magenta {
|
||||
color: rgb(118, 38, 113);
|
||||
}
|
||||
.fg-cyan {
|
||||
color: rgb(44, 181, 233);
|
||||
}
|
||||
.fg-white {
|
||||
color: rgb(204, 204, 204);
|
||||
}
|
||||
.bg-black {
|
||||
background-color: rgb(0, 0, 0);
|
||||
}
|
||||
.bg-red {
|
||||
background-color: rgb(222, 56, 43);
|
||||
}
|
||||
.bg-green {
|
||||
background-color: rgb(57, 181, 74);
|
||||
}
|
||||
.bg-yellow {
|
||||
background-color: rgb(255, 199, 6);
|
||||
}
|
||||
.bg-blue {
|
||||
background-color: rgb(0, 111, 184);
|
||||
}
|
||||
.bg-magenta {
|
||||
background-color: rgb(118, 38, 113);
|
||||
}
|
||||
.bg-cyan {
|
||||
background-color: rgb(44, 181, 233);
|
||||
}
|
||||
.bg-white {
|
||||
background-color: rgb(204, 204, 204);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
export function parseTextToColoredPre(text) {
|
||||
const pre = document.createElement("pre");
|
||||
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
|
||||
let i = 0;
|
||||
|
||||
const state = {
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
strikethrough: false,
|
||||
foregroundColor: null,
|
||||
backgroundColor: null,
|
||||
};
|
||||
|
||||
const addSpan = (content) => {
|
||||
const span = document.createElement("span");
|
||||
if (state.bold) span.classList.add("bold");
|
||||
if (state.italic) span.classList.add("italic");
|
||||
if (state.underline) span.classList.add("underline");
|
||||
if (state.strikethrough) span.classList.add("strikethrough");
|
||||
if (state.foregroundColor !== null)
|
||||
span.classList.add(`fg-${state.foregroundColor}`);
|
||||
if (state.backgroundColor !== null)
|
||||
span.classList.add(`bg-${state.backgroundColor}`);
|
||||
span.appendChild(document.createTextNode(content));
|
||||
pre.appendChild(span);
|
||||
};
|
||||
|
||||
/* eslint-disable no-cond-assign */
|
||||
let match;
|
||||
while ((match = re.exec(text)) !== null) {
|
||||
const j = match.index;
|
||||
addSpan(text.substring(i, j));
|
||||
i = j + match[0].length;
|
||||
|
||||
if (match[1] === undefined) continue;
|
||||
|
||||
for (const colorCode of match[1].split(";")) {
|
||||
switch (parseInt(colorCode)) {
|
||||
case 0:
|
||||
// reset
|
||||
state.bold = false;
|
||||
state.italic = false;
|
||||
state.underline = false;
|
||||
state.strikethrough = false;
|
||||
state.foregroundColor = null;
|
||||
state.backgroundColor = null;
|
||||
break;
|
||||
case 1:
|
||||
state.bold = true;
|
||||
break;
|
||||
case 3:
|
||||
state.italic = true;
|
||||
break;
|
||||
case 4:
|
||||
state.underline = true;
|
||||
break;
|
||||
case 9:
|
||||
state.strikethrough = true;
|
||||
break;
|
||||
case 22:
|
||||
state.bold = false;
|
||||
break;
|
||||
case 23:
|
||||
state.italic = false;
|
||||
break;
|
||||
case 24:
|
||||
state.underline = false;
|
||||
break;
|
||||
case 29:
|
||||
state.strikethrough = false;
|
||||
break;
|
||||
case 30:
|
||||
// foreground black
|
||||
state.foregroundColor = null;
|
||||
break;
|
||||
case 31:
|
||||
state.foregroundColor = "red";
|
||||
break;
|
||||
case 32:
|
||||
state.foregroundColor = "green";
|
||||
break;
|
||||
case 33:
|
||||
state.foregroundColor = "yellow";
|
||||
break;
|
||||
case 34:
|
||||
state.foregroundColor = "blue";
|
||||
break;
|
||||
case 35:
|
||||
state.foregroundColor = "magenta";
|
||||
break;
|
||||
case 36:
|
||||
state.foregroundColor = "cyan";
|
||||
break;
|
||||
case 37:
|
||||
state.foregroundColor = "white";
|
||||
break;
|
||||
case 39:
|
||||
// foreground reset
|
||||
state.foregroundColor = null;
|
||||
break;
|
||||
case 40:
|
||||
state.backgroundColor = "black";
|
||||
break;
|
||||
case 41:
|
||||
state.backgroundColor = "red";
|
||||
break;
|
||||
case 42:
|
||||
state.backgroundColor = "green";
|
||||
break;
|
||||
case 43:
|
||||
state.backgroundColor = "yellow";
|
||||
break;
|
||||
case 44:
|
||||
state.backgroundColor = "blue";
|
||||
break;
|
||||
case 45:
|
||||
state.backgroundColor = "magenta";
|
||||
break;
|
||||
case 46:
|
||||
state.backgroundColor = "cyan";
|
||||
break;
|
||||
case 47:
|
||||
state.backgroundColor = "white";
|
||||
break;
|
||||
case 49:
|
||||
// background reset
|
||||
state.backgroundColor = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
addSpan(text.substring(i));
|
||||
|
||||
return pre;
|
||||
}
|
@ -2,6 +2,7 @@ import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
|
||||
|
||||
class HassioSupervisorLog extends PolymerElement {
|
||||
static get template() {
|
||||
@ -12,12 +13,18 @@ class HassioSupervisorLog extends PolymerElement {
|
||||
}
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.fg-green {
|
||||
color: var(--primary-text-color) !important;
|
||||
}
|
||||
</style>
|
||||
${ANSI_HTML_STYLE}
|
||||
<paper-card>
|
||||
<div class="card-content"><pre>[[log]]</pre></div>
|
||||
<div class="card-content" id="content"></div>
|
||||
<div class="card-actions">
|
||||
<paper-button on-click="refreshTapped">Refresh</paper-button>
|
||||
<paper-button on-click="refresh">Refresh</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
@ -26,7 +33,6 @@ class HassioSupervisorLog extends PolymerElement {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
log: String,
|
||||
};
|
||||
}
|
||||
|
||||
@ -37,16 +43,20 @@ class HassioSupervisorLog extends PolymerElement {
|
||||
|
||||
loadData() {
|
||||
this.hass.callApi("get", "hassio/supervisor/logs").then(
|
||||
(info) => {
|
||||
this.log = info;
|
||||
(text) => {
|
||||
while (this.$.content.lastChild) {
|
||||
this.$.content.removeChild(this.$.content.lastChild);
|
||||
}
|
||||
this.$.content.appendChild(parseTextToColoredPre(text));
|
||||
},
|
||||
() => {
|
||||
this.log = "Error fetching logs";
|
||||
this.$.content.innerHTML =
|
||||
'<span class="fg-red bold">Error fetching logs</span>';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
refreshTapped() {
|
||||
refresh() {
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
|
21
package.json
21
package.json
@ -64,10 +64,10 @@
|
||||
"@polymer/paper-toggle-button": "^3.0.1",
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "^3.0.5",
|
||||
"@vaadin/vaadin-combo-box": "^4.2.0-beta2",
|
||||
"@vaadin/vaadin-date-picker": "^3.3.0",
|
||||
"@webcomponents/shadycss": "^1.5.2",
|
||||
"@webcomponents/webcomponentsjs": "^2.1.3",
|
||||
"@vaadin/vaadin-combo-box": "^4.2.0",
|
||||
"@vaadin/vaadin-date-picker": "^3.3.1",
|
||||
"@webcomponents/shadycss": "^1.6.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.0",
|
||||
"chart.js": "~2.7.2",
|
||||
"chartjs-chart-timeline": "^0.2.1",
|
||||
"es6-object-assign": "^1.1.0",
|
||||
@ -87,6 +87,7 @@
|
||||
"react-big-calendar": "^0.19.2",
|
||||
"regenerator-runtime": "^0.12.1",
|
||||
"round-slider": "^1.3.2",
|
||||
"superstruct": "^0.6.0",
|
||||
"unfetch": "^4.0.1",
|
||||
"web-animations-js": "^2.3.1",
|
||||
"xss": "^1.0.3"
|
||||
@ -152,15 +153,11 @@
|
||||
"workbox-webpack-plugin": "^3.5.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"inherits": "2.0.3",
|
||||
"samsam": "1.1.3",
|
||||
"supports-color": "3.1.2",
|
||||
"type-detect": "1.0.0",
|
||||
"@polymer/polymer": "3.1.0",
|
||||
"@webcomponents/webcomponentsjs": "2.1.3",
|
||||
"@webcomponents/shadycss": "^1.5.2",
|
||||
"@vaadin/vaadin-overlay": "3.2.0-alpha3",
|
||||
"@vaadin/vaadin-lumo-styles": "1.2.0",
|
||||
"@webcomponents/webcomponentsjs": "2.2.1",
|
||||
"@webcomponents/shadycss": "^1.6.0",
|
||||
"@vaadin/vaadin-overlay": "3.2.2",
|
||||
"@vaadin/vaadin-lumo-styles": "1.3.0",
|
||||
"fecha": "https://github.com/taylorhakes/fecha/archive/5e8fe08d982647fdb19fb403459838b02647813c.tar.gz",
|
||||
"lit-html": "0.12.0",
|
||||
"@polymer/lit-element": "0.6.2"
|
||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20181121.1",
|
||||
version="20181205.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@ -103,6 +103,7 @@ class HaPlantCard extends EventsMixin(PolymerElement) {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: Object,
|
||||
config: Object,
|
||||
};
|
||||
}
|
||||
|
||||
@ -118,7 +119,7 @@ class HaPlantCard extends EventsMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
computeTitle(stateObj) {
|
||||
return computeStateName(stateObj);
|
||||
return this.config.name || computeStateName(stateObj);
|
||||
}
|
||||
|
||||
computeAttributes(data) {
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import computeStateName from "../common/entity/compute_state_name";
|
||||
|
||||
import "../components/ha-card";
|
||||
import "../components/ha-icon";
|
||||
|
||||
@ -106,7 +108,7 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
<ha-card>
|
||||
<div class="header">
|
||||
[[computeState(stateObj.state, localize)]]
|
||||
<div class="name">[[stateObj.attributes.friendly_name]]</div>
|
||||
<div class="name">[[computeName(stateObj)]]</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="now">
|
||||
@ -271,6 +273,10 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
return localize(`state.weather.${state}`) || state;
|
||||
}
|
||||
|
||||
computeName(stateObj) {
|
||||
return this.config.name || computeStateName(stateObj);
|
||||
}
|
||||
|
||||
showWeatherIcon(condition) {
|
||||
return condition in this.weatherIcons;
|
||||
}
|
||||
|
@ -10,31 +10,43 @@ const langKey = ["second", "minute", "hour", "day"];
|
||||
|
||||
export default function relativeTime(
|
||||
dateObj: Date,
|
||||
localize: LocalizeFunc
|
||||
localize: LocalizeFunc,
|
||||
options: {
|
||||
compareTime?: Date;
|
||||
includeTense?: boolean;
|
||||
} = {}
|
||||
): string {
|
||||
let delta = (new Date().getTime() - dateObj.getTime()) / 1000;
|
||||
const compareTime = options.compareTime || new Date();
|
||||
let delta = (compareTime.getTime() - dateObj.getTime()) / 1000;
|
||||
const tense = delta >= 0 ? "past" : "future";
|
||||
delta = Math.abs(delta);
|
||||
|
||||
let timeDesc;
|
||||
|
||||
for (let i = 0; i < tests.length; i++) {
|
||||
if (delta < tests[i]) {
|
||||
delta = Math.floor(delta);
|
||||
const timeDesc = localize(
|
||||
timeDesc = localize(
|
||||
`ui.components.relative_time.duration.${langKey[i]}`,
|
||||
"count",
|
||||
delta
|
||||
);
|
||||
return localize(`ui.components.relative_time.${tense}`, "time", timeDesc);
|
||||
break;
|
||||
}
|
||||
|
||||
delta /= tests[i];
|
||||
}
|
||||
|
||||
delta = Math.floor(delta);
|
||||
const time = localize(
|
||||
"ui.components.relative_time.duration.week",
|
||||
"count",
|
||||
delta
|
||||
);
|
||||
return localize(`ui.components.relative_time.${tense}`, "time", time);
|
||||
if (timeDesc === undefined) {
|
||||
delta = Math.floor(delta);
|
||||
timeDesc = localize(
|
||||
"ui.components.relative_time.duration.week",
|
||||
"count",
|
||||
delta
|
||||
);
|
||||
}
|
||||
|
||||
return options.includeTense === false
|
||||
? timeDesc
|
||||
: localize(`ui.components.relative_time.${tense}`, "time", timeDesc);
|
||||
}
|
||||
|
@ -28,6 +28,17 @@
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
declare global {
|
||||
// tslint:disable-next-line
|
||||
interface HASSDomEvents {}
|
||||
}
|
||||
|
||||
export type ValidHassDomEvent = keyof HASSDomEvents;
|
||||
|
||||
export interface HASSDomEvent<T> extends Event {
|
||||
detail: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a custom event with an optional detail value.
|
||||
*
|
||||
@ -35,23 +46,33 @@
|
||||
* @param {*=} detail Detail value containing event-specific
|
||||
* payload.
|
||||
* @param {{ bubbles: (boolean|undefined),
|
||||
cancelable: (boolean|undefined),
|
||||
composed: (boolean|undefined) }=}
|
||||
* options Object specifying options. These may include:
|
||||
* `bubbles` (boolean, defaults to `true`),
|
||||
* `cancelable` (boolean, defaults to false), and
|
||||
* `node` on which to fire the event (HTMLElement, defaults to `this`).
|
||||
* @return {Event} The new event that was fired.
|
||||
*/
|
||||
export const fireEvent = (node, type, detail, options) => {
|
||||
* cancelable: (boolean|undefined),
|
||||
* composed: (boolean|undefined) }=}
|
||||
* options Object specifying options. These may include:
|
||||
* `bubbles` (boolean, defaults to `true`),
|
||||
* `cancelable` (boolean, defaults to false), and
|
||||
* `node` on which to fire the event (HTMLElement, defaults to `this`).
|
||||
* @return {Event} The new event that was fired.
|
||||
*/
|
||||
export const fireEvent = <HassEvent extends ValidHassDomEvent>(
|
||||
node: HTMLElement,
|
||||
type: HassEvent,
|
||||
detail?: HASSDomEvents[HassEvent],
|
||||
options?: {
|
||||
bubbles?: boolean;
|
||||
cancelable?: boolean;
|
||||
composed?: boolean;
|
||||
}
|
||||
) => {
|
||||
options = options || {};
|
||||
// @ts-ignore
|
||||
detail = detail === null || detail === undefined ? {} : detail;
|
||||
const event = new Event(type, {
|
||||
bubbles: options.bubbles === undefined ? true : options.bubbles,
|
||||
cancelable: Boolean(options.cancelable),
|
||||
composed: options.composed === undefined ? true : options.composed,
|
||||
});
|
||||
event.detail = detail;
|
||||
(event as any).detail = detail;
|
||||
node.dispatchEvent(event);
|
||||
return event;
|
||||
};
|
@ -2,6 +2,7 @@ import { HassEntity } from "home-assistant-js-websocket";
|
||||
import canToggleDomain from "./can_toggle_domain";
|
||||
import computeStateDomain from "./compute_state_domain";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { supportsFeature } from "./supports-feature";
|
||||
|
||||
export default function canToggleState(
|
||||
hass: HomeAssistant,
|
||||
@ -12,8 +13,7 @@ export default function canToggleState(
|
||||
return stateObj.state === "on" || stateObj.state === "off";
|
||||
}
|
||||
if (domain === "climate") {
|
||||
// tslint:disable-next-line
|
||||
return (stateObj.attributes.supported_features! & 4096) !== 0;
|
||||
return supportsFeature(stateObj, 4096);
|
||||
}
|
||||
|
||||
return canToggleDomain(hass, domain);
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import { DEFAULT_VIEW_ENTITY_ID } from "../const";
|
||||
import { GroupEntity } from "../../types";
|
||||
|
||||
// Return an ordered array of available views
|
||||
export default function extractViews(entities: HassEntities): HassEntity[] {
|
||||
const views: HassEntity[] = [];
|
||||
export default function extractViews(entities: HassEntities): GroupEntity[] {
|
||||
const views: GroupEntity[] = [];
|
||||
|
||||
Object.keys(entities).forEach((entityId) => {
|
||||
const entity = entities[entityId];
|
||||
if (entity.attributes.view) {
|
||||
views.push(entity);
|
||||
views.push(entity as GroupEntity);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { supportsFeature } from "./supports-feature";
|
||||
|
||||
// Expects classNames to be an object mapping feature-bit -> className
|
||||
export default function featureClassNames(
|
||||
@ -9,12 +10,9 @@ export default function featureClassNames(
|
||||
return "";
|
||||
}
|
||||
|
||||
const features = stateObj.attributes.supported_features;
|
||||
|
||||
return Object.keys(classNames)
|
||||
.map((feature) =>
|
||||
// tslint:disable-next-line
|
||||
(features & Number(feature)) !== 0 ? classNames[feature] : ""
|
||||
supportsFeature(stateObj, Number(feature)) ? classNames[feature] : ""
|
||||
)
|
||||
.filter((attr) => attr !== "")
|
||||
.join(" ");
|
||||
|
@ -8,7 +8,7 @@ import { GroupEntity } from "../../types";
|
||||
export default function getViewEntities(
|
||||
entities: HassEntities,
|
||||
view: GroupEntity
|
||||
) {
|
||||
): HassEntities {
|
||||
const viewEntities = {};
|
||||
|
||||
view.attributes.entity_id.forEach((entityId) => {
|
||||
|
@ -1,18 +1,19 @@
|
||||
import computeDomain from "./compute_domain";
|
||||
import { HassEntity, HassEntities } from "home-assistant-js-websocket";
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import { GroupEntity } from "../../types";
|
||||
|
||||
// Split a collection into a list of groups and a 'rest' list of ungrouped
|
||||
// entities.
|
||||
// Returns { groups: [], ungrouped: {} }
|
||||
export default function splitByGroups(entities: HassEntities) {
|
||||
const groups: HassEntity[] = [];
|
||||
const groups: GroupEntity[] = [];
|
||||
const ungrouped: HassEntities = {};
|
||||
|
||||
Object.keys(entities).forEach((entityId) => {
|
||||
const entity = entities[entityId];
|
||||
|
||||
if (computeDomain(entityId) === "group") {
|
||||
groups.push(entity);
|
||||
groups.push(entity as GroupEntity);
|
||||
} else {
|
||||
ungrouped[entityId] = entity;
|
||||
}
|
||||
|
9
src/common/entity/supports-feature.ts
Normal file
9
src/common/entity/supports-feature.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
export const supportsFeature = (
|
||||
stateObj: HassEntity,
|
||||
feature: number
|
||||
): boolean => {
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
return (stateObj.attributes.supported_features! & feature) !== 0;
|
||||
};
|
9
src/common/util/compute_rtl.ts
Normal file
9
src/common/util/compute_rtl.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
export function computeRTL(hass: HomeAssistant) {
|
||||
const lang = hass.language || "en";
|
||||
if (hass.translationMetadata.translations[lang]) {
|
||||
return hass.translationMetadata.translations[lang].isRTL || false;
|
||||
}
|
||||
return false;
|
||||
}
|
9
src/common/util/uid.ts
Normal file
9
src/common/util/uid.ts
Normal file
@ -0,0 +1,9 @@
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
|
||||
export function uid() {
|
||||
return s4() + s4() + s4() + s4() + s4();
|
||||
}
|
@ -4,6 +4,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../ha-relative-time";
|
||||
import "./state-badge";
|
||||
import computeStateName from "../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
|
||||
class StateInfo extends PolymerElement {
|
||||
static get template() {
|
||||
@ -25,10 +26,20 @@ class StateInfo extends PolymerElement {
|
||||
float: left;
|
||||
}
|
||||
|
||||
:host([rtl]) state-badge {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 56px;
|
||||
}
|
||||
|
||||
:host([rtl]) .info {
|
||||
margin-right: 56px;
|
||||
margin-left: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.name {
|
||||
@apply --paper-font-common-nowrap;
|
||||
color: var(--primary-text-color);
|
||||
@ -87,12 +98,21 @@ class StateInfo extends PolymerElement {
|
||||
hass: Object,
|
||||
stateObj: Object,
|
||||
inDialog: Boolean,
|
||||
rtl: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
computed: "computeRTL(hass)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computeStateName(stateObj) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
|
||||
computeRTL(hass) {
|
||||
return computeRTL(hass);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("state-info", StateInfo);
|
||||
|
@ -12,6 +12,7 @@ class HaCard extends PolymerElement {
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s ease-out;
|
||||
background-color: var(--paper-card-background-color, white);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.header {
|
||||
@apply --paper-font-headline;
|
||||
|
@ -123,7 +123,7 @@ class HaCards extends PolymerElement {
|
||||
</style>
|
||||
|
||||
<div id="main">
|
||||
<template is="dom-if" if="[[cards.badges]]">
|
||||
<template is="dom-if" if="[[cards.badges.length]]">
|
||||
<div class="badges">
|
||||
<template is="dom-if" if="[[cards.demo]]">
|
||||
<ha-demo-badge></ha-demo-badge>
|
||||
@ -159,7 +159,6 @@ class HaCards extends PolymerElement {
|
||||
},
|
||||
|
||||
states: Object,
|
||||
panelVisible: Boolean,
|
||||
|
||||
viewVisible: {
|
||||
type: Boolean,
|
||||
@ -173,19 +172,11 @@ class HaCards extends PolymerElement {
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return [
|
||||
"updateCards(columns, states, panelVisible, viewVisible, orderedGroupEntities)",
|
||||
];
|
||||
return ["updateCards(columns, states, viewVisible, orderedGroupEntities)"];
|
||||
}
|
||||
|
||||
updateCards(
|
||||
columns,
|
||||
states,
|
||||
panelVisible,
|
||||
viewVisible,
|
||||
orderedGroupEntities
|
||||
) {
|
||||
if (!panelVisible || !viewVisible) {
|
||||
updateCards(columns, states, viewVisible, orderedGroupEntities) {
|
||||
if (!viewVisible) {
|
||||
if (this.$.main.parentNode) {
|
||||
this.$.main._parentNode = this.$.main.parentNode;
|
||||
this.$.main.parentNode.removeChild(this.$.main);
|
||||
@ -200,7 +191,7 @@ class HaCards extends PolymerElement {
|
||||
timeOut.after(10),
|
||||
() => {
|
||||
// Things might have changed since it got scheduled.
|
||||
if (this.panelVisible && this.viewVisible) {
|
||||
if (this.viewVisible) {
|
||||
this.cards = this.computeCards(columns, states, orderedGroupEntities);
|
||||
}
|
||||
}
|
||||
|
19
src/data/cloud.ts
Normal file
19
src/data/cloud.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface CloudWebhook {
|
||||
webhook_id: string;
|
||||
cloudhook_id: string;
|
||||
cloudhook_url: string;
|
||||
}
|
||||
|
||||
export const createCloudhook = (hass: HomeAssistant, webhookId: string) =>
|
||||
hass.callWS<CloudWebhook>({
|
||||
type: "cloud/cloudhook/create",
|
||||
webhook_id: webhookId,
|
||||
});
|
||||
|
||||
export const deleteCloudhook = (hass: HomeAssistant, webhookId: string) =>
|
||||
hass.callWS({
|
||||
type: "cloud/cloudhook/delete",
|
||||
webhook_id: webhookId,
|
||||
});
|
1
src/data/entity.ts
Normal file
1
src/data/entity.ts
Normal file
@ -0,0 +1 @@
|
||||
export const UNAVAILABLE = "unavailable";
|
7
src/data/light.ts
Normal file
7
src/data/light.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const SUPPORT_BRIGHTNESS = 1;
|
||||
export const SUPPORT_COLOR_TEMP = 2;
|
||||
export const SUPPORT_EFFECT = 4;
|
||||
export const SUPPORT_FLASH = 8;
|
||||
export const SUPPORT_COLOR = 16;
|
||||
export const SUPPORT_TRANSITION = 32;
|
||||
export const SUPPORT_WHITE_VALUE = 128;
|
150
src/data/lovelace.ts
Normal file
150
src/data/lovelace.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface LovelaceConfig {
|
||||
_frontendAuto: boolean;
|
||||
title?: string;
|
||||
views: LovelaceViewConfig[];
|
||||
}
|
||||
|
||||
export interface LovelaceViewConfig {
|
||||
title?: string;
|
||||
badges?: string[];
|
||||
cards?: LovelaceCardConfig[];
|
||||
id?: string;
|
||||
icon?: string;
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
export interface LovelaceCardConfig {
|
||||
id?: string;
|
||||
type: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ToggleActionConfig {
|
||||
action: "toggle";
|
||||
}
|
||||
|
||||
export interface CallServiceActionConfig {
|
||||
action: "call-service";
|
||||
service: string;
|
||||
service_data?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface NavigateActionConfig {
|
||||
action: "navigate";
|
||||
navigation_path: string;
|
||||
}
|
||||
|
||||
export interface MoreInfoActionConfig {
|
||||
action: "more-info";
|
||||
}
|
||||
|
||||
export interface NoActionConfig {
|
||||
action: "none";
|
||||
}
|
||||
|
||||
export type ActionConfig =
|
||||
| ToggleActionConfig
|
||||
| CallServiceActionConfig
|
||||
| NavigateActionConfig
|
||||
| MoreInfoActionConfig
|
||||
| NoActionConfig;
|
||||
|
||||
export const fetchConfig = (hass: HomeAssistant): Promise<LovelaceConfig> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/config",
|
||||
});
|
||||
|
||||
export const migrateConfig = (hass: HomeAssistant): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/config/migrate",
|
||||
});
|
||||
|
||||
export const saveConfig = (
|
||||
hass: HomeAssistant,
|
||||
config: LovelaceConfig | string,
|
||||
format: "json" | "yaml"
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/config/save",
|
||||
config,
|
||||
format,
|
||||
});
|
||||
|
||||
export const getCardConfig = (
|
||||
hass: HomeAssistant,
|
||||
cardId: string
|
||||
): Promise<string> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/config/card/get",
|
||||
card_id: cardId,
|
||||
});
|
||||
|
||||
export const updateCardConfig = (
|
||||
hass: HomeAssistant,
|
||||
cardId: string,
|
||||
config: LovelaceCardConfig | string,
|
||||
format: "json" | "yaml"
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/config/card/update",
|
||||
card_id: cardId,
|
||||
card_config: config,
|
||||
format,
|
||||
});
|
||||
|
||||
export const deleteCard = (
|
||||
hass: HomeAssistant,
|
||||
cardId: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/config/card/delete",
|
||||
card_id: cardId,
|
||||
});
|
||||
|
||||
export const addCard = (
|
||||
hass: HomeAssistant,
|
||||
viewId: string,
|
||||
config: LovelaceCardConfig | string,
|
||||
format: "json" | "yaml"
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/config/card/add",
|
||||
view_id: viewId,
|
||||
card_config: config,
|
||||
format,
|
||||
});
|
||||
|
||||
export const updateViewConfig = (
|
||||
hass: HomeAssistant,
|
||||
viewId: string,
|
||||
config: LovelaceViewConfig | string,
|
||||
format: "json" | "yaml"
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/config/view/update",
|
||||
view_id: viewId,
|
||||
view_config: config,
|
||||
format,
|
||||
});
|
||||
|
||||
export const deleteView = (
|
||||
hass: HomeAssistant,
|
||||
viewId: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/config/view/delete",
|
||||
view_id: viewId,
|
||||
});
|
||||
|
||||
export const addView = (
|
||||
hass: HomeAssistant,
|
||||
config: LovelaceViewConfig | string,
|
||||
format: "json" | "yaml"
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/config/view/add",
|
||||
view_config: config,
|
||||
format,
|
||||
});
|
4
src/data/media-player.ts
Normal file
4
src/data/media-player.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const SUPPORT_PAUSE = 1;
|
||||
export const SUPPORT_NEXT_TRACK = 32;
|
||||
export const SUPPORTS_PLAY = 16384;
|
||||
export const OFF_STATES = ["off", "idle"];
|
@ -11,31 +11,30 @@ export const fetchItems = (hass: HomeAssistant): Promise<ShoppingListItem[]> =>
|
||||
type: "shopping_list/items",
|
||||
});
|
||||
|
||||
export const saveEdit = (
|
||||
export const updateItem = (
|
||||
hass: HomeAssistant,
|
||||
itemId: number,
|
||||
name: string
|
||||
item: {
|
||||
name?: string;
|
||||
complete?: boolean;
|
||||
}
|
||||
): Promise<ShoppingListItem> =>
|
||||
hass.callApi("POST", "shopping_list/item/" + itemId, {
|
||||
name,
|
||||
});
|
||||
|
||||
export const completeItem = (
|
||||
hass: HomeAssistant,
|
||||
itemId: number,
|
||||
complete: boolean
|
||||
): Promise<void> =>
|
||||
hass.callApi("POST", "shopping_list/item/" + itemId, {
|
||||
complete,
|
||||
hass.callWS({
|
||||
type: "shopping_list/items/update",
|
||||
item_id: itemId,
|
||||
...item,
|
||||
});
|
||||
|
||||
export const clearItems = (hass: HomeAssistant): Promise<void> =>
|
||||
hass.callApi("POST", "shopping_list/clear_completed");
|
||||
hass.callWS({
|
||||
type: "shopping_list/items/clear",
|
||||
});
|
||||
|
||||
export const addItem = (
|
||||
hass: HomeAssistant,
|
||||
name: string
|
||||
): Promise<ShoppingListItem> =>
|
||||
hass.callApi("POST", "shopping_list/item", {
|
||||
hass.callWS({
|
||||
type: "shopping_list/items/add",
|
||||
name,
|
||||
});
|
||||
|
12
src/data/webhook.ts
Normal file
12
src/data/webhook.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface Webhook {
|
||||
webhook_id: string;
|
||||
domain: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const fetchWebhooks = (hass: HomeAssistant): Promise<Webhook[]> =>
|
||||
hass.callWS({
|
||||
type: "webhook/list",
|
||||
});
|
@ -13,6 +13,7 @@ import "../../../components/ha-paper-slider";
|
||||
|
||||
import attributeClassNames from "../../../common/entity/attribute_class_names";
|
||||
import featureClassNames from "../../../common/entity/feature_class_names";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
@ -385,45 +386,45 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
|
||||
supportsTemperature(stateObj) {
|
||||
return (
|
||||
(stateObj.attributes.supported_features & 1) !== 0 &&
|
||||
supportsFeature(stateObj, 1) &&
|
||||
typeof stateObj.attributes.temperature === "number"
|
||||
);
|
||||
}
|
||||
|
||||
supportsTemperatureRange(stateObj) {
|
||||
return (
|
||||
(stateObj.attributes.supported_features & 6) !== 0 &&
|
||||
supportsFeature(stateObj, 6) &&
|
||||
(typeof stateObj.attributes.target_temp_low === "number" ||
|
||||
typeof stateObj.attributes.target_temp_high === "number")
|
||||
);
|
||||
}
|
||||
|
||||
supportsHumidity(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 8) !== 0;
|
||||
return supportsFeature(stateObj, 8);
|
||||
}
|
||||
|
||||
supportsFanMode(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 64) !== 0;
|
||||
return supportsFeature(stateObj, 64);
|
||||
}
|
||||
|
||||
supportsOperationMode(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 128) !== 0;
|
||||
return supportsFeature(stateObj, 128);
|
||||
}
|
||||
|
||||
supportsSwingMode(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 512) !== 0;
|
||||
return supportsFeature(stateObj, 512);
|
||||
}
|
||||
|
||||
supportsAwayMode(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 1024) !== 0;
|
||||
return supportsFeature(stateObj, 1024);
|
||||
}
|
||||
|
||||
supportsAuxHeat(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 2048) !== 0;
|
||||
return supportsFeature(stateObj, 2048);
|
||||
}
|
||||
|
||||
supportsOn(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 4096) !== 0;
|
||||
return supportsFeature(stateObj, 4096);
|
||||
}
|
||||
|
||||
computeClassNames(stateObj) {
|
||||
|
@ -8,6 +8,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/ha-attributes";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
|
||||
class MoreInfoVacuum extends PolymerElement {
|
||||
static get template() {
|
||||
@ -158,57 +159,53 @@ class MoreInfoVacuum extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
supportsPause(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 4) !== 0;
|
||||
return supportsFeature(stateObj, 4);
|
||||
}
|
||||
|
||||
supportsStop(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 8) !== 0;
|
||||
return supportsFeature(stateObj, 8);
|
||||
}
|
||||
|
||||
supportsReturnHome(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 16) !== 0;
|
||||
return supportsFeature(stateObj, 16);
|
||||
}
|
||||
|
||||
supportsFanSpeed(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 32) !== 0;
|
||||
return supportsFeature(stateObj, 32);
|
||||
}
|
||||
|
||||
supportsBattery(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 64) !== 0;
|
||||
return supportsFeature(stateObj, 64);
|
||||
}
|
||||
|
||||
supportsStatus(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 128) !== 0;
|
||||
return supportsFeature(stateObj, 128);
|
||||
}
|
||||
|
||||
supportsLocate(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 512) !== 0;
|
||||
return supportsFeature(stateObj, 512);
|
||||
}
|
||||
|
||||
supportsCleanSpot(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 1024) !== 0;
|
||||
return supportsFeature(stateObj, 1024);
|
||||
}
|
||||
|
||||
supportsStart(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 8192) !== 0;
|
||||
return supportsFeature(stateObj, 8192);
|
||||
}
|
||||
|
||||
supportsCommandBar(stateObj) {
|
||||
return (
|
||||
((stateObj.attributes.supported_features & 4) !== 0) |
|
||||
((stateObj.attributes.supported_features & 8) !== 0) |
|
||||
((stateObj.attributes.supported_features & 16) !== 0) |
|
||||
((stateObj.attributes.supported_features & 512) !== 0) |
|
||||
((stateObj.attributes.supported_features & 1024) !== 0) |
|
||||
((stateObj.attributes.supported_features & 8192) !== 0)
|
||||
supportsFeature(stateObj, 4) |
|
||||
supportsFeature(stateObj, 8) |
|
||||
supportsFeature(stateObj, 16) |
|
||||
supportsFeature(stateObj, 512) |
|
||||
supportsFeature(stateObj, 1024) |
|
||||
supportsFeature(stateObj, 8192)
|
||||
);
|
||||
}
|
||||
|
||||
/* eslint-enable no-bitwise */
|
||||
|
||||
fanSpeedChanged(fanSpeedIndex) {
|
||||
var fanSpeedInput;
|
||||
// Selected Option will transition to '' before transitioning to new value
|
||||
|
@ -12,6 +12,7 @@ import "../../../components/ha-water_heater-control";
|
||||
import "../../../components/ha-paper-slider";
|
||||
|
||||
import featureClassNames from "../../../common/entity/feature_class_names";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
@ -198,17 +199,17 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
|
||||
supportsTemperature(stateObj) {
|
||||
return (
|
||||
(stateObj.attributes.supported_features & 1) !== 0 &&
|
||||
supportsFeature(stateObj, 1) &&
|
||||
typeof stateObj.attributes.temperature === "number"
|
||||
);
|
||||
}
|
||||
|
||||
supportsOperationMode(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 2) !== 0;
|
||||
return supportsFeature(stateObj, 2);
|
||||
}
|
||||
|
||||
supportsAwayMode(stateObj) {
|
||||
return (stateObj.attributes.supported_features & 4) !== 0;
|
||||
return supportsFeature(stateObj, 4);
|
||||
}
|
||||
|
||||
computeClassNames(stateObj) {
|
||||
|
@ -16,6 +16,7 @@ import computeStateDomain from "../../common/entity/compute_state_domain";
|
||||
import isComponentLoaded from "../../common/config/is_component_loaded";
|
||||
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
|
||||
import EventsMixin from "../../mixins/events-mixin";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
|
||||
const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"];
|
||||
/*
|
||||
@ -58,6 +59,11 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
:host([domain="camera"]) paper-dialog-scrollable {
|
||||
margin: 0 -24px -21px;
|
||||
}
|
||||
|
||||
:host([rtl]) app-toolbar {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<app-toolbar>
|
||||
@ -147,6 +153,11 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
hoursToShow: 24,
|
||||
},
|
||||
},
|
||||
rtl: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
computed: "_computeRTL(hass)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -190,5 +201,9 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
_gotoSettings() {
|
||||
this.fire("more-info-page", { page: "settings" });
|
||||
}
|
||||
|
||||
_computeRTL(hass) {
|
||||
return computeRTL(hass);
|
||||
}
|
||||
}
|
||||
customElements.define("more-info-controls", MoreInfoControls);
|
||||
|
@ -33,7 +33,7 @@
|
||||
Home Assistant
|
||||
</div>
|
||||
|
||||
<ha-onboarding>Initializing…</ha-onboarding>
|
||||
<ha-onboarding>Initializing</ha-onboarding>
|
||||
</div>
|
||||
<% if (!latestBuild) { %>
|
||||
<script src="/static/custom-elements-es5-adapter.js"></script>
|
||||
|
@ -1,25 +0,0 @@
|
||||
export default (superClass) =>
|
||||
class extends superClass {
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener("register-dialog", (e) =>
|
||||
this.registerDialog(e.detail)
|
||||
);
|
||||
}
|
||||
|
||||
registerDialog({ dialogShowEvent, dialogTag, dialogImport }) {
|
||||
let loaded = null;
|
||||
|
||||
this.addEventListener(dialogShowEvent, (showEv) => {
|
||||
if (!loaded) {
|
||||
loaded = dialogImport().then(() => {
|
||||
const dialogEl = document.createElement(dialogTag);
|
||||
this.shadowRoot.appendChild(dialogEl);
|
||||
this.provideHass(dialogEl);
|
||||
return dialogEl;
|
||||
});
|
||||
}
|
||||
loaded.then((dialogEl) => dialogEl.showDialog(showEv.detail));
|
||||
});
|
||||
}
|
||||
};
|
56
src/layouts/app/dialog-manager-mixin.ts
Normal file
56
src/layouts/app/dialog-manager-mixin.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { PolymerElement } from "@polymer/polymer";
|
||||
import { Constructor } from "@polymer/lit-element";
|
||||
import { HASSDomEvent, ValidHassDomEvent } from "../../common/dom/fire_event";
|
||||
|
||||
interface RegisterDialogParams {
|
||||
dialogShowEvent: keyof HASSDomEvents;
|
||||
dialogTag: keyof HTMLElementTagNameMap;
|
||||
dialogImport: () => Promise<unknown>;
|
||||
}
|
||||
|
||||
interface HassDialog<T = HASSDomEvents[ValidHassDomEvent]> extends HTMLElement {
|
||||
showDialog(params: T);
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"register-dialog": RegisterDialogParams;
|
||||
}
|
||||
// for add event listener
|
||||
interface HTMLElementEventMap {
|
||||
"register-dialog": HASSDomEvent<RegisterDialogParams>;
|
||||
}
|
||||
}
|
||||
|
||||
export const dialogManagerMixin = (superClass: Constructor<PolymerElement>) =>
|
||||
class extends superClass {
|
||||
public ready() {
|
||||
super.ready();
|
||||
this.addEventListener("register-dialog", (e) =>
|
||||
this.registerDialog(e.detail)
|
||||
);
|
||||
}
|
||||
|
||||
private registerDialog({
|
||||
dialogShowEvent,
|
||||
dialogTag,
|
||||
dialogImport,
|
||||
}: RegisterDialogParams) {
|
||||
let loaded: Promise<HassDialog<unknown>>;
|
||||
|
||||
this.addEventListener(dialogShowEvent, (showEv) => {
|
||||
if (!loaded) {
|
||||
loaded = dialogImport().then(() => {
|
||||
const dialogEl = document.createElement(dialogTag) as HassDialog;
|
||||
this.shadowRoot!.appendChild(dialogEl);
|
||||
(this as any).provideHass(dialogEl);
|
||||
return dialogEl;
|
||||
});
|
||||
}
|
||||
loaded.then((dialogEl) =>
|
||||
dialogEl.showDialog((showEv as HASSDomEvent<unknown>).detail)
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
@ -4,6 +4,7 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { afterNextRender } from "@polymer/polymer/lib/utils/render-status";
|
||||
import { html as litHtml, LitElement } from "@polymer/lit-element";
|
||||
|
||||
import "../home-assistant-main";
|
||||
import "../ha-init-page";
|
||||
@ -16,11 +17,13 @@ import TranslationsMixin from "./translations-mixin";
|
||||
import ThemesMixin from "./themes-mixin";
|
||||
import MoreInfoMixin from "./more-info-mixin";
|
||||
import SidebarMixin from "./sidebar-mixin";
|
||||
import DialogManagerMixin from "./dialog-manager-mixin";
|
||||
import { dialogManagerMixin } from "./dialog-manager-mixin";
|
||||
import ConnectionMixin from "./connection-mixin";
|
||||
import NotificationMixin from "./notification-mixin";
|
||||
import DisconnectToastMixin from "./disconnect-toast-mixin";
|
||||
|
||||
LitElement.prototype.html = litHtml;
|
||||
|
||||
const ext = (baseClass, mixins) =>
|
||||
mixins.reduceRight((base, mixin) => mixin(base), baseClass);
|
||||
|
||||
@ -33,7 +36,7 @@ class HomeAssistant extends ext(PolymerElement, [
|
||||
DisconnectToastMixin,
|
||||
ConnectionMixin,
|
||||
NotificationMixin,
|
||||
DialogManagerMixin,
|
||||
dialogManagerMixin,
|
||||
HassBaseMixin,
|
||||
]) {
|
||||
static get template() {
|
||||
@ -91,7 +94,7 @@ class HomeAssistant extends ext(PolymerElement, [
|
||||
}
|
||||
|
||||
computePanelUrl(routeData) {
|
||||
return (routeData && routeData.panel) || "states";
|
||||
return (routeData && routeData.panel) || "lovelace";
|
||||
}
|
||||
|
||||
panelUrlChanged(newPanelUrl) {
|
||||
|
@ -2,16 +2,15 @@ import "@polymer/app-layout/app-drawer-layout/app-drawer-layout";
|
||||
import "@polymer/app-layout/app-drawer/app-drawer";
|
||||
import "@polymer/app-route/app-route";
|
||||
import "@polymer/iron-media-query/iron-media-query";
|
||||
import "@polymer/iron-pages/iron-pages";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../util/ha-url-sync";
|
||||
|
||||
import "./partial-cards";
|
||||
import "./partial-panel-resolver";
|
||||
import EventsMixin from "../mixins/events-mixin";
|
||||
import NavigateMixin from "../mixins/navigate-mixin";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
|
||||
import(/* webpackChunkName: "ha-sidebar" */ "../components/ha-sidebar");
|
||||
import(/* webpackChunkName: "voice-command-dialog" */ "../dialogs/ha-voice-command-dialog");
|
||||
@ -30,21 +29,16 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
||||
:host([rtl]) {
|
||||
direction: rtl;
|
||||
}
|
||||
iron-pages,
|
||||
partial-panel-resolver,
|
||||
ha-sidebar {
|
||||
/* allow a light tap highlight on the actual interface elements */
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
iron-pages {
|
||||
partial-panel-resolver {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<ha-url-sync hass="[[hass]]"></ha-url-sync>
|
||||
<app-route
|
||||
route="{{route}}"
|
||||
pattern="/states"
|
||||
tail="{{statesRouteTail}}"
|
||||
></app-route>
|
||||
<ha-voice-command-dialog
|
||||
hass="[[hass]]"
|
||||
id="voiceDialog"
|
||||
@ -72,29 +66,12 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
||||
></ha-sidebar>
|
||||
</app-drawer>
|
||||
|
||||
<iron-pages
|
||||
attr-for-selected="id"
|
||||
fallback-selection="panel-resolver"
|
||||
selected="[[hass.panelUrl]]"
|
||||
selected-attribute="panel-visible"
|
||||
>
|
||||
<partial-cards
|
||||
id="states"
|
||||
narrow="[[narrow]]"
|
||||
hass="[[hass]]"
|
||||
show-menu="[[dockedSidebar]]"
|
||||
route="[[statesRouteTail]]"
|
||||
show-tabs=""
|
||||
></partial-cards>
|
||||
|
||||
<partial-panel-resolver
|
||||
id="panel-resolver"
|
||||
narrow="[[narrow]]"
|
||||
hass="[[hass]]"
|
||||
route="[[route]]"
|
||||
show-menu="[[dockedSidebar]]"
|
||||
></partial-panel-resolver>
|
||||
</iron-pages>
|
||||
<partial-panel-resolver
|
||||
narrow="[[narrow]]"
|
||||
hass="[[hass]]"
|
||||
route="[[route]]"
|
||||
show-menu="[[dockedSidebar]]"
|
||||
></partial-panel-resolver>
|
||||
</app-drawer-layout>
|
||||
`;
|
||||
}
|
||||
@ -107,7 +84,6 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
||||
type: Object,
|
||||
observer: "_routeChanged",
|
||||
},
|
||||
statesRouteTail: Object,
|
||||
dockedSidebar: {
|
||||
type: Boolean,
|
||||
computed: "computeDockedSidebar(hass)",
|
||||
@ -115,14 +91,14 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
||||
rtl: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
computed: "computeRTL(hass)",
|
||||
computed: "_computeRTL(hass)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this._defaultPage = localStorage.defaultPage || "states";
|
||||
this._defaultPage = localStorage.defaultPage || "lovelace";
|
||||
this.addEventListener("hass-open-menu", () => this.handleOpenMenu());
|
||||
this.addEventListener("hass-close-menu", () => this.handleCloseMenu());
|
||||
this.addEventListener("hass-start-voice", (ev) =>
|
||||
@ -159,7 +135,7 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (document.location.pathname === "/") {
|
||||
this.navigate(`/${localStorage.defaultPage || "states"}`, true);
|
||||
this.navigate(`/${localStorage.defaultPage || "lovelace"}`, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,12 +151,8 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
||||
return NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1;
|
||||
}
|
||||
|
||||
computeRTL(hass) {
|
||||
var lang = hass.selectedLanguage || hass.language;
|
||||
if (hass.translationMetadata.translations[lang]) {
|
||||
return hass.translationMetadata.translations[lang].isRTL || false;
|
||||
}
|
||||
return false;
|
||||
_computeRTL(hass) {
|
||||
return computeRTL(hass);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,10 @@ function ensureLoaded(panel) {
|
||||
imported = import(/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace");
|
||||
break;
|
||||
|
||||
case "states":
|
||||
imported = import(/* webpackChunkName: "panel-states" */ "../panels/states/ha-panel-states");
|
||||
break;
|
||||
|
||||
case "history":
|
||||
imported = import(/* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history");
|
||||
break;
|
||||
|
@ -24,6 +24,10 @@ export class CloudAlexaPref extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.cloudStatus) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const enabled = this.cloudStatus!.prefs.alexa_enabled;
|
||||
|
||||
return html`
|
||||
|
@ -25,7 +25,11 @@ export class CloudGooglePref extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const { google_enabled, google_allow_unlock } = this.cloudStatus!.prefs;
|
||||
if (!this.cloudStatus) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const { google_enabled, google_allow_unlock } = this.cloudStatus.prefs;
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
|
142
src/panels/config/cloud/cloud-webhook-manage-dialog.ts
Normal file
142
src/panels/config/cloud/cloud-webhook-manage-dialog.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-dialog/paper-dialog";
|
||||
// This is not a duplicate import, one is for types, one is for element.
|
||||
// tslint:disable-next-line
|
||||
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
|
||||
// tslint:disable-next-line
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
|
||||
import { buttonLink } from "../../../resources/ha-style";
|
||||
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { WebhookDialogParams } from "./types";
|
||||
|
||||
const inputLabel = "Public URL – Click to copy to clipboard";
|
||||
|
||||
export class CloudWebhookManageDialog extends LitElement {
|
||||
protected hass?: HomeAssistant;
|
||||
private _params?: WebhookDialogParams;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
_params: {},
|
||||
};
|
||||
}
|
||||
|
||||
public async showDialog(params: WebhookDialogParams) {
|
||||
this._params = params;
|
||||
// Wait till dialog is rendered.
|
||||
await this.updateComplete;
|
||||
this._dialog.open();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
const { webhook, cloudhook } = this._params;
|
||||
const docsUrl =
|
||||
webhook.domain === "automation"
|
||||
? "https://www.home-assistant.io/docs/automation/trigger/#webhook-trigger"
|
||||
: `https://www.home-assistant.io/components/${webhook.domain}/`;
|
||||
return html`
|
||||
${this._renderStyle()}
|
||||
<paper-dialog with-backdrop>
|
||||
<h2>Webhook for ${webhook.name}</h2>
|
||||
<div>
|
||||
<p>The webhook is available at the following url:</p>
|
||||
<paper-input
|
||||
label="${inputLabel}"
|
||||
value="${cloudhook.cloudhook_url}"
|
||||
@click="${this._copyClipboard}"
|
||||
@blur="${this._restoreLabel}"
|
||||
></paper-input>
|
||||
<p>
|
||||
If you no longer want to use this webhook, you can
|
||||
<button class="link" @click="${this._disableWebhook}">
|
||||
disable it</button
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="paper-dialog-buttons">
|
||||
<a href="${docsUrl}" target="_blank"
|
||||
><paper-button>VIEW DOCUMENTATION</paper-button></a
|
||||
>
|
||||
<paper-button @click="${this._closeDialog}">CLOSE</paper-button>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _dialog(): PaperDialogElement {
|
||||
return this.shadowRoot!.querySelector("paper-dialog")!;
|
||||
}
|
||||
|
||||
private get _paperInput(): PaperInputElement {
|
||||
return this.shadowRoot!.querySelector("paper-input")!;
|
||||
}
|
||||
|
||||
private _closeDialog() {
|
||||
this._dialog.close();
|
||||
}
|
||||
|
||||
private async _disableWebhook() {
|
||||
if (!confirm("Are you sure you want to disable this webhook?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._params!.disableHook();
|
||||
this._closeDialog();
|
||||
}
|
||||
|
||||
private _copyClipboard(ev: FocusEvent) {
|
||||
// paper-input -> iron-input -> input
|
||||
const paperInput = ev.currentTarget as PaperInputElement;
|
||||
const input = (paperInput.inputElement as any)
|
||||
.inputElement as HTMLInputElement;
|
||||
input.setSelectionRange(0, input.value.length);
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
paperInput.label = "COPIED TO CLIPBOARD";
|
||||
} catch (err) {
|
||||
// Copying failed. Oh no
|
||||
}
|
||||
}
|
||||
|
||||
private _restoreLabel() {
|
||||
this._paperInput.label = inputLabel;
|
||||
}
|
||||
|
||||
private _renderStyle() {
|
||||
return html`
|
||||
<style>
|
||||
paper-dialog {
|
||||
width: 650px;
|
||||
}
|
||||
paper-input {
|
||||
margin-top: -8px;
|
||||
}
|
||||
${buttonLink} button.link {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"cloud-webhook-manage-dialog": CloudWebhookManageDialog;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("cloud-webhook-manage-dialog", CloudWebhookManageDialog);
|
234
src/panels/config/cloud/cloud-webhooks.ts
Normal file
234
src/panels/config/cloud/cloud-webhooks.ts
Normal file
@ -0,0 +1,234 @@
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
} from "@polymer/lit-element";
|
||||
import "@polymer/paper-toggle-button/paper-toggle-button";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@polymer/paper-spinner/paper-spinner";
|
||||
import "../../../components/ha-card";
|
||||
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
import { HomeAssistant, WebhookError } from "../../../types";
|
||||
import { WebhookDialogParams, CloudStatusLoggedIn } from "./types";
|
||||
import { Webhook, fetchWebhooks } from "../../../data/webhook";
|
||||
import {
|
||||
createCloudhook,
|
||||
deleteCloudhook,
|
||||
CloudWebhook,
|
||||
} from "../../../data/cloud";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"manage-cloud-webhook": WebhookDialogParams;
|
||||
}
|
||||
}
|
||||
|
||||
export class CloudWebhooks extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
public cloudStatus?: CloudStatusLoggedIn;
|
||||
private _cloudHooks?: { [webhookId: string]: CloudWebhook };
|
||||
private _localHooks?: Webhook[];
|
||||
private _progress: string[];
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
cloudStatus: {},
|
||||
_cloudHooks: {},
|
||||
_localHooks: {},
|
||||
_progress: {},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._progress = [];
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card header="Webhooks">
|
||||
<div class="body">
|
||||
Anything that is configured to be triggered by a webhook can be given
|
||||
a publicly accessible URL to allow you to send data back to Home
|
||||
Assistant from anywhere, without exposing your instance to the
|
||||
internet.
|
||||
</div>
|
||||
|
||||
${this._renderBody()}
|
||||
|
||||
<div class="footer">
|
||||
<a href="https://www.nabucasa.com/config/webhooks" target="_blank">
|
||||
Learn more about creating webhook-powered automations.
|
||||
</a>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("cloudStatus") && this.cloudStatus) {
|
||||
this._cloudHooks = this.cloudStatus.prefs.cloudhooks || {};
|
||||
}
|
||||
}
|
||||
|
||||
private _renderBody() {
|
||||
if (!this.cloudStatus || !this._localHooks || !this._cloudHooks) {
|
||||
return html`
|
||||
<div class="loading">Loading…</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return this._localHooks.map(
|
||||
(entry) => html`
|
||||
<div class="webhook" .entry="${entry}">
|
||||
<paper-item-body two-line>
|
||||
<div>
|
||||
${entry.name}
|
||||
${
|
||||
entry.domain === entry.name.toLowerCase()
|
||||
? ""
|
||||
: ` (${entry.domain})`
|
||||
}
|
||||
</div>
|
||||
<div secondary>${entry.webhook_id}</div>
|
||||
</paper-item-body>
|
||||
${
|
||||
this._progress.includes(entry.webhook_id)
|
||||
? html`
|
||||
<div class="progress">
|
||||
<paper-spinner active></paper-spinner>
|
||||
</div>
|
||||
`
|
||||
: this._cloudHooks![entry.webhook_id]
|
||||
? html`
|
||||
<paper-button @click="${this._handleManageButton}"
|
||||
>Manage</paper-button
|
||||
>
|
||||
`
|
||||
: html`
|
||||
<paper-toggle-button
|
||||
@click="${this._enableWebhook}"
|
||||
></paper-toggle-button>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
private _showDialog(webhookId: string) {
|
||||
const webhook = this._localHooks!.find(
|
||||
(ent) => ent.webhook_id === webhookId
|
||||
);
|
||||
const cloudhook = this._cloudHooks![webhookId];
|
||||
const params: WebhookDialogParams = {
|
||||
webhook: webhook!,
|
||||
cloudhook,
|
||||
disableHook: () => this._disableWebhook(webhookId),
|
||||
};
|
||||
fireEvent(this, "manage-cloud-webhook", params);
|
||||
}
|
||||
|
||||
private _handleManageButton(ev: MouseEvent) {
|
||||
const entry = (ev.currentTarget as any).parentElement.entry as Webhook;
|
||||
this._showDialog(entry.webhook_id);
|
||||
}
|
||||
|
||||
private async _enableWebhook(ev: MouseEvent) {
|
||||
const entry = (ev.currentTarget as any).parentElement.entry;
|
||||
this._progress = [...this._progress, entry.webhook_id];
|
||||
let updatedWebhook;
|
||||
|
||||
try {
|
||||
updatedWebhook = await createCloudhook(this.hass!, entry.webhook_id);
|
||||
} catch (err) {
|
||||
alert((err as WebhookError).message);
|
||||
return;
|
||||
} finally {
|
||||
this._progress = this._progress.filter((wid) => wid !== entry.webhook_id);
|
||||
}
|
||||
|
||||
this._cloudHooks = {
|
||||
...this._cloudHooks,
|
||||
[entry.webhook_id]: updatedWebhook,
|
||||
};
|
||||
|
||||
// Only open dialog if we're not also enabling others, otherwise it's confusing
|
||||
if (this._progress.length === 0) {
|
||||
this._showDialog(entry.webhook_id);
|
||||
}
|
||||
}
|
||||
|
||||
private async _disableWebhook(webhookId: string) {
|
||||
this._progress = [...this._progress, webhookId];
|
||||
try {
|
||||
await deleteCloudhook(this.hass!, webhookId!);
|
||||
} catch (err) {
|
||||
alert(`Failed to disable webhook: ${(err as WebhookError).message}`);
|
||||
return;
|
||||
} finally {
|
||||
this._progress = this._progress.filter((wid) => wid !== webhookId);
|
||||
}
|
||||
|
||||
// Remove cloud related parts from entry.
|
||||
const { [webhookId]: disabledHook, ...newHooks } = this._cloudHooks!;
|
||||
this._cloudHooks = newHooks;
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
this._localHooks = await fetchWebhooks(this.hass!);
|
||||
}
|
||||
|
||||
private renderStyle() {
|
||||
return html`
|
||||
<style>
|
||||
.body {
|
||||
padding: 0 16px 8px;
|
||||
}
|
||||
.loading {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.webhook {
|
||||
display: flex;
|
||||
padding: 4px 16px;
|
||||
}
|
||||
.progress {
|
||||
margin-right: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
paper-button {
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.footer {
|
||||
padding: 16px;
|
||||
}
|
||||
.footer a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"cloud-webhooks": CloudWebhooks;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("cloud-webhooks", CloudWebhooks);
|
@ -10,15 +10,19 @@ import "../../../layouts/hass-subpage";
|
||||
import "../../../resources/ha-style";
|
||||
|
||||
import "../ha-config-section";
|
||||
import "./cloud-webhooks";
|
||||
|
||||
import formatDateTime from "../../../common/datetime/format_date_time";
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
import { fetchSubscriptionInfo } from "./data";
|
||||
import "./cloud-alexa-pref";
|
||||
import "./cloud-google-pref";
|
||||
|
||||
let registeredWebhookDialog = false;
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin LocalizeMixin
|
||||
@ -129,6 +133,11 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
hass="[[hass]]"
|
||||
cloud-status="[[cloudStatus]]"
|
||||
></cloud-google-pref>
|
||||
|
||||
<cloud-webhooks
|
||||
hass="[[hass]]"
|
||||
cloud-status="[[cloudStatus]]"
|
||||
></cloud-webhooks>
|
||||
</ha-config-section>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
@ -152,9 +161,26 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
this._fetchSubscriptionInfo();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
if (!registeredWebhookDialog) {
|
||||
registeredWebhookDialog = true;
|
||||
fireEvent(this, "register-dialog", {
|
||||
dialogShowEvent: "manage-cloud-webhook",
|
||||
dialogTag: "cloud-webhook-manage-dialog",
|
||||
dialogImport: () => import("./cloud-webhook-manage-dialog"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _fetchSubscriptionInfo() {
|
||||
this._subscription = await fetchSubscriptionInfo(this.hass);
|
||||
if (this._subscription.provider && this.cloudStatus.cloud !== "connected") {
|
||||
if (
|
||||
this._subscription.provider &&
|
||||
this.cloudStatus &&
|
||||
this.cloudStatus.cloud !== "connected"
|
||||
) {
|
||||
this.fire("ha-refresh-cloud-status");
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { CloudWebhook } from "../../../data/cloud";
|
||||
import { Webhook } from "../../../data/webhook";
|
||||
|
||||
export interface EntityFilter {
|
||||
include_domains: string[];
|
||||
include_entities: string[];
|
||||
@ -19,6 +22,7 @@ export type CloudStatusLoggedIn = CloudStatusBase & {
|
||||
google_enabled: boolean;
|
||||
alexa_enabled: boolean;
|
||||
google_allow_unlock: boolean;
|
||||
cloudhooks: { [webhookId: string]: CloudWebhook };
|
||||
};
|
||||
};
|
||||
|
||||
@ -27,3 +31,9 @@ export type CloudStatus = CloudStatusBase | CloudStatusLoggedIn;
|
||||
export interface SubscriptionInfo {
|
||||
human_description: string;
|
||||
}
|
||||
|
||||
export interface WebhookDialogParams {
|
||||
webhook: Webhook;
|
||||
cloudhook: CloudWebhook;
|
||||
disableHook: () => void;
|
||||
}
|
||||
|
@ -24,7 +24,18 @@ export default class NumericStateTrigger extends Component {
|
||||
/* eslint-disable camelcase */
|
||||
render({ trigger, hass, localize }) {
|
||||
const { value_template, entity_id, below, above } = trigger;
|
||||
let trgFor = trigger.for;
|
||||
|
||||
if (trgFor && (trgFor.hours || trgFor.minutes || trgFor.seconds)) {
|
||||
// If the trigger was defined using the yaml dict syntax, convert it to
|
||||
// the equivalent string format
|
||||
let { hours = 0, minutes = 0, seconds = 0 } = trgFor;
|
||||
hours = hours.toString();
|
||||
minutes = minutes.toString().padStart(2, "0");
|
||||
seconds = seconds.toString().padStart(2, "0");
|
||||
|
||||
trgFor = `${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<ha-entity-picker
|
||||
@ -57,6 +68,14 @@ export default class NumericStateTrigger extends Component {
|
||||
value={value_template}
|
||||
onvalue-changed={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label={localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.state.for"
|
||||
)}
|
||||
name="for"
|
||||
value={trgFor}
|
||||
onvalue-changed={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -164,7 +164,7 @@ class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
</template>
|
||||
</p>
|
||||
<p>
|
||||
<a href='/lovelace'>Try out the new Lovelace UI (experimental)</a>
|
||||
<a href='/states'>Go back to the old states page</a>
|
||||
<div id="love" style="cursor:pointer;" on-click="_toggleDefaultPage">[[_defaultPageText()]]</div
|
||||
</p>
|
||||
</div>
|
||||
@ -364,15 +364,15 @@ class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
|
||||
_defaultPageText() {
|
||||
return `>> ${
|
||||
localStorage.defaultPage === "lovelace" ? "Remove" : "Set"
|
||||
} lovelace as default page on this device <<`;
|
||||
localStorage.defaultPage === "states" ? "Remove" : "Set"
|
||||
} the old states as default page on this device <<`;
|
||||
}
|
||||
|
||||
_toggleDefaultPage() {
|
||||
if (localStorage.defaultPage === "lovelace") {
|
||||
if (localStorage.defaultPage === "states") {
|
||||
delete localStorage.defaultPage;
|
||||
} else {
|
||||
localStorage.defaultPage = "lovelace";
|
||||
localStorage.defaultPage = "states";
|
||||
}
|
||||
this.$.love.innerText = this._defaultPageText();
|
||||
}
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../layouts/partial-cards";
|
||||
import "../states/ha-panel-states";
|
||||
|
||||
class HaPanelKiosk extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<partial-cards
|
||||
<ha-panel-states
|
||||
id="kiosk-states"
|
||||
hass="[[hass]]"
|
||||
show-menu
|
||||
route="[[route]]"
|
||||
panel-visible
|
||||
></partial-cards>
|
||||
></ha-panel-states>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -209,8 +209,8 @@ class HuiAlarmPanelCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
|
||||
_computeHeader(localize, stateObj) {
|
||||
if (!stateObj) return "";
|
||||
return this._config.title
|
||||
? this._config.title
|
||||
return this._config.name
|
||||
? this._config.name
|
||||
: this._label(localize, stateObj.state);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import createCardElement from "../common/create-card-element";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
|
||||
interface Condition {
|
||||
entity: string;
|
||||
@ -9,8 +10,8 @@ interface Condition {
|
||||
state_not?: string;
|
||||
}
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
card: LovelaceConfig;
|
||||
interface Config extends LovelaceCardConfig {
|
||||
card: LovelaceCardConfig;
|
||||
conditions: Condition[];
|
||||
}
|
||||
|
||||
|
@ -14,8 +14,9 @@ import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { EntityConfig, EntityRow } from "../entity-rows/types";
|
||||
import { LovelaceCard, LovelaceConfig, LovelaceCardEditor } from "../types";
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import createRowElement from "../common/create-row-element";
|
||||
import computeDomain from "../../../common/entity/compute_domain";
|
||||
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
|
||||
@ -29,7 +30,7 @@ export interface ConfigEntity extends EntityConfig {
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface Config extends LovelaceConfig {
|
||||
export interface Config extends LovelaceCardConfig {
|
||||
show_header_toggle?: boolean;
|
||||
title?: string;
|
||||
entities: ConfigEntity[];
|
||||
@ -42,6 +43,11 @@ class HuiEntitiesCard extends hassLocalizeLitMixin(LitElement)
|
||||
await import("../editor/config-elements/hui-entities-card-editor");
|
||||
return document.createElement("hui-entities-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(): object {
|
||||
return { entities: [] };
|
||||
}
|
||||
|
||||
protected _hass?: HomeAssistant;
|
||||
protected _config?: Config;
|
||||
protected _configEntities?: ConfigEntity[];
|
||||
|
@ -10,7 +10,6 @@ import { styleMap } from "lit-html/directives/styleMap";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
|
||||
import toggleEntity from "../common/entity/toggle-entity";
|
||||
import isValidEntityId from "../../../common/entity/valid_entity_id";
|
||||
import stateIcon from "../../../common/entity/state_icon";
|
||||
import computeStateDomain from "../../../common/entity/compute_state_domain";
|
||||
@ -18,19 +17,18 @@ import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
|
||||
import { HomeAssistant, LightEntity } from "../../../types";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { handleClick } from "../common/handle-click";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
interface Config extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
theme?: string;
|
||||
tap_action?: "toggle" | "call-service" | "more-info";
|
||||
hold_action?: "toggle" | "call-service" | "more-info";
|
||||
service?: string;
|
||||
service_data?: object;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
}
|
||||
|
||||
class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
|
||||
@ -81,8 +79,8 @@ class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card
|
||||
@ha-click="${() => this.handleClick(false)}"
|
||||
@ha-hold="${() => this.handleClick(true)}"
|
||||
@ha-click="${this._handleTap}"
|
||||
@ha-hold="${this._handleHold}"
|
||||
.longPress="${longPress()}"
|
||||
>
|
||||
${
|
||||
@ -186,34 +184,12 @@ class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
|
||||
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
|
||||
}
|
||||
|
||||
private handleClick(hold: boolean): void {
|
||||
const config = this._config;
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass!.states[config.entity];
|
||||
if (!stateObj) {
|
||||
return;
|
||||
}
|
||||
const entityId = stateObj.entity_id;
|
||||
const action = hold ? config.hold_action : config.tap_action || "more-info";
|
||||
switch (action) {
|
||||
case "toggle":
|
||||
toggleEntity(this.hass, entityId);
|
||||
break;
|
||||
case "call-service":
|
||||
if (!config.service) {
|
||||
return;
|
||||
}
|
||||
const [domain, service] = config.service.split(".", 2);
|
||||
const serviceData = { entity_id: entityId, ...config.service_data };
|
||||
this.hass!.callService(domain, service, serviceData);
|
||||
break;
|
||||
case "more-info":
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
break;
|
||||
default:
|
||||
}
|
||||
private _handleTap() {
|
||||
handleClick(this, this.hass!, this._config!, false);
|
||||
}
|
||||
|
||||
private _handleHold() {
|
||||
handleClick(this, this.hass!, this._config!, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import createCardElement from "../common/create-card-element";
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
|
||||
function getEntities(hass, filterState, entities) {
|
||||
return entities.filter((entityConf) => {
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
interface Config extends LovelaceCardConfig {
|
||||
error: string;
|
||||
origConfig: LovelaceConfig;
|
||||
origConfig: LovelaceCardConfig;
|
||||
}
|
||||
|
||||
class HuiErrorCard extends LitElement implements LovelaceCard {
|
||||
@ -50,7 +51,7 @@ class HuiErrorCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
private _toStr(config: LovelaceConfig): string {
|
||||
private _toStr(config: LovelaceCardConfig): string {
|
||||
return JSON.stringify(config, null, 2);
|
||||
}
|
||||
}
|
||||
|
@ -6,20 +6,22 @@ import {
|
||||
} from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
|
||||
import isValidEntityId from "../../../common/entity/valid_entity_id";
|
||||
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
interface Config extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
title?: string;
|
||||
unit_of_measurement?: string;
|
||||
name?: string;
|
||||
unit?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
severity?: object;
|
||||
@ -87,12 +89,14 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
<div id="percent">
|
||||
${stateObj.state}
|
||||
${
|
||||
this._config.unit_of_measurement ||
|
||||
this._config.unit ||
|
||||
stateObj.attributes.unit_of_measurement ||
|
||||
""
|
||||
}
|
||||
</div>
|
||||
<div id="title">${this._config.title}</div>
|
||||
<div id="name">
|
||||
${this._config.name || computeStateName(stateObj)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@ -210,7 +214,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
.gauge-data #percent {
|
||||
font-size: calc(var(--base-unit) * 0.55);
|
||||
}
|
||||
.gauge-data #title {
|
||||
.gauge-data #name {
|
||||
padding-top: calc(var(--base-unit) * 0.15);
|
||||
font-size: calc(var(--base-unit) * 0.3);
|
||||
}
|
||||
|
@ -7,31 +7,29 @@ import {
|
||||
import { TemplateResult } from "lit-html";
|
||||
import { classMap } from "lit-html/directives/classMap";
|
||||
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard, LovelaceConfig, LovelaceCardEditor } from "../types";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { EntityConfig } from "../entity-rows/types";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
|
||||
import toggleEntity from "../common/entity/toggle-entity";
|
||||
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import { handleClick } from "../common/handle-click";
|
||||
|
||||
export interface ConfigEntity extends EntityConfig {
|
||||
tap_action?: "toggle" | "call-service" | "more-info";
|
||||
hold_action?: "toggle" | "call-service" | "more-info";
|
||||
service?: string;
|
||||
service_data?: object;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface Config extends LovelaceConfig {
|
||||
export interface Config extends LovelaceCardConfig {
|
||||
show_name?: boolean;
|
||||
show_state?: boolean;
|
||||
title?: string;
|
||||
@ -46,6 +44,9 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
|
||||
await import("../editor/config-elements/hui-glance-card-editor");
|
||||
return document.createElement("hui-glance-card-editor");
|
||||
}
|
||||
public static getStubConfig(): object {
|
||||
return { entities: [] };
|
||||
}
|
||||
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
@ -67,13 +68,16 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
this._config = { theme: "default", ...config };
|
||||
const entities = processConfigEntities(config.entities);
|
||||
const entities = processConfigEntities<ConfigEntity>(config.entities);
|
||||
|
||||
for (const entity of entities) {
|
||||
if (
|
||||
(entity.tap_action === "call-service" ||
|
||||
entity.hold_action === "call-service") &&
|
||||
!entity.service
|
||||
(entity.tap_action &&
|
||||
entity.tap_action.action === "call-service" &&
|
||||
!entity.tap_action.service) ||
|
||||
(entity.hold_action &&
|
||||
entity.hold_action.action === "call-service" &&
|
||||
!entity.hold_action.service)
|
||||
) {
|
||||
throw new Error(
|
||||
'Missing required property "service" when tap_action or hold_action is call-service'
|
||||
@ -199,8 +203,8 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
|
||||
<div
|
||||
class="entity"
|
||||
.entityConf="${entityConf}"
|
||||
@ha-click="${(ev) => this.handleClick(ev, false)}"
|
||||
@ha-hold="${(ev) => this.handleClick(ev, true)}"
|
||||
@ha-click="${this._handleTap}"
|
||||
@ha-hold="${this._handleHold}"
|
||||
.longPress="${longPress()}"
|
||||
>
|
||||
${
|
||||
@ -239,24 +243,14 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
|
||||
`;
|
||||
}
|
||||
|
||||
private handleClick(ev: MouseEvent, hold: boolean): void {
|
||||
private _handleTap(ev: MouseEvent) {
|
||||
const config = (ev.currentTarget as any).entityConf as ConfigEntity;
|
||||
const entityId = config.entity;
|
||||
const action = hold ? config.hold_action : config.tap_action || "more-info";
|
||||
switch (action) {
|
||||
case "toggle":
|
||||
toggleEntity(this.hass, entityId);
|
||||
break;
|
||||
case "call-service":
|
||||
const [domain, service] = config.service!.split(".", 2);
|
||||
const serviceData = { entity_id: entityId, ...config.service_data };
|
||||
this.hass!.callService(domain, service, serviceData);
|
||||
break;
|
||||
case "more-info":
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
break;
|
||||
default:
|
||||
}
|
||||
handleClick(this, this.hass!, config, false);
|
||||
}
|
||||
|
||||
private _handleHold(ev: MouseEvent) {
|
||||
const config = (ev.currentTarget as any).entityConf as ConfigEntity;
|
||||
handleClick(this, this.hass!, config, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ import "../../../components/ha-card";
|
||||
import "../../../components/state-history-charts";
|
||||
import "../../../data/ha-state-history-data";
|
||||
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
|
||||
class HuiHistoryGraphCard extends PolymerElement {
|
||||
static get template() {
|
||||
|
@ -2,11 +2,12 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import { TemplateResult } from "lit-html";
|
||||
import { styleMap } from "lit-html/directives/styleMap";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
interface Config extends LovelaceCardConfig {
|
||||
aspect_ratio?: string;
|
||||
title?: string;
|
||||
url: string;
|
||||
|
@ -34,6 +34,7 @@ export default class LegacyWrapperCard extends HTMLElement {
|
||||
this._ensureElement(this._tag);
|
||||
this.lastChild.hass = hass;
|
||||
this.lastChild.stateObj = hass.states[entityId];
|
||||
this.lastChild.config = this._config;
|
||||
} else {
|
||||
this._ensureElement("HUI-ERROR-CARD");
|
||||
this.lastChild.setConfig(
|
||||
|
@ -12,7 +12,8 @@ import { jQuery } from "../../../resources/jquery";
|
||||
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
|
||||
import { HomeAssistant, LightEntity } from "../../../types";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
|
||||
import stateIcon from "../../../common/entity/state_icon";
|
||||
@ -37,7 +38,7 @@ const lightConfig = {
|
||||
showTooltip: false,
|
||||
};
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
interface Config extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
name?: string;
|
||||
theme?: string;
|
||||
|
@ -6,10 +6,11 @@ import Leaflet from "leaflet";
|
||||
import "../../map/ha-entity-marker";
|
||||
|
||||
import setupLeafletMap from "../../../common/dom/setup-leaflet-map";
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import computeStateDomain from "../../../common/entity/compute_state_domain";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import debounce from "../../../common/util/debounce";
|
||||
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||
|
||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
|
||||
|
||||
@ -97,7 +98,15 @@ class HuiMapCard extends PolymerElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.root.style.paddingTop = this._config.aspect_ratio || "100%";
|
||||
const ratio = parseAspectRatio(this._config.aspect_ratio);
|
||||
|
||||
if (ratio && ratio.w > 0 && ratio.h > 0) {
|
||||
this.$.root.style.paddingBottom = `${((100 * ratio.h) / ratio.w).toFixed(
|
||||
2
|
||||
)}%`;
|
||||
} else {
|
||||
this.$.root.style.paddingBottom = "100%";
|
||||
}
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
@ -110,8 +119,13 @@ class HuiMapCard extends PolymerElement {
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
let ar = this._config.aspect_ratio || "100%";
|
||||
ar = ar.substr(0, ar.length - 1);
|
||||
const ratio = parseAspectRatio(this._config.aspect_ratio);
|
||||
let ar;
|
||||
if (ratio && ratio.w > 0 && ratio.h > 0) {
|
||||
ar = `${((100 * ratio.h) / ratio.w).toFixed(2)}`;
|
||||
} else {
|
||||
ar = "100";
|
||||
}
|
||||
return 1 + Math.floor(ar / 25) || 3;
|
||||
}
|
||||
|
||||
|
@ -4,10 +4,11 @@ import { classMap } from "lit-html/directives/classMap";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-markdown";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
interface Config extends LovelaceCardConfig {
|
||||
content: string;
|
||||
title?: string;
|
||||
}
|
||||
|
@ -2,17 +2,18 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
import { classMap } from "lit-html/directives/classMap";
|
||||
import { handleClick } from "../common/handle-click";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
interface Config extends LovelaceCardConfig {
|
||||
image?: string;
|
||||
navigation_path?: string;
|
||||
service?: string;
|
||||
service_data?: object;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export class HuiPictureCard extends LitElement implements LovelaceCard {
|
||||
@ -45,11 +46,13 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card
|
||||
@click="${this.handleClick}"
|
||||
@ha-click="${this._handleTap}"
|
||||
@ha-hold="${this._handleHold}"
|
||||
.longPress="${longPress()}"
|
||||
class="${
|
||||
classMap({
|
||||
clickable: Boolean(
|
||||
this._config.navigation_path || this._config.service
|
||||
this._config.tap_action || this._config.hold_action
|
||||
),
|
||||
})
|
||||
}"
|
||||
@ -76,14 +79,12 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
private handleClick(): void {
|
||||
if (this._config!.navigation_path) {
|
||||
navigate(this, this._config!.navigation_path!);
|
||||
}
|
||||
if (this._config!.service) {
|
||||
const [domain, service] = this._config!.service!.split(".", 2);
|
||||
this.hass!.callService(domain, service, this._config!.service_data);
|
||||
}
|
||||
private _handleTap() {
|
||||
handleClick(this, this.hass!, this._config!, false);
|
||||
}
|
||||
|
||||
private _handleHold() {
|
||||
handleClick(this, this.hass!, this._config!, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,13 +3,18 @@ import { TemplateResult } from "lit-html";
|
||||
|
||||
import createHuiElement from "../common/create-hui-element";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
interface Config extends LovelaceCardConfig {
|
||||
title?: string;
|
||||
image: string;
|
||||
image?: string;
|
||||
camera_image?: string;
|
||||
state_image?: {};
|
||||
aspect_ratio?: string;
|
||||
entity?: string;
|
||||
elements: LovelaceElementConfig[];
|
||||
}
|
||||
|
||||
@ -38,7 +43,10 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
||||
public setConfig(config: Config): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid Configuration");
|
||||
} else if (!config.image) {
|
||||
} else if (
|
||||
!(config.image || config.camera_image || config.state_image) ||
|
||||
(config.state_image && !config.entity)
|
||||
) {
|
||||
throw new Error("Invalid Configuration: image required");
|
||||
} else if (!Array.isArray(config.elements)) {
|
||||
throw new Error("Invalid Configuration: elements required");
|
||||
@ -55,13 +63,19 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card .header="${this._config.title}">
|
||||
<div id="root">
|
||||
<img src="${this._config.image}" /> ${
|
||||
this._config.elements.map((elementConfig: LovelaceElementConfig) =>
|
||||
this._createHuiElement(elementConfig)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<hui-image
|
||||
.hass="${this._hass}"
|
||||
.image="${this._config.image}"
|
||||
.stateImage="${this._config.state_image}"
|
||||
.cameraImage="${this._config.camera_image}"
|
||||
.entity="${this._config.entity}"
|
||||
.aspectRatio="${this._config.aspect_ratio}"
|
||||
></hui-image>
|
||||
${
|
||||
this._config.elements.map((elementConfig: LovelaceElementConfig) =>
|
||||
this._createHuiElement(elementConfig)
|
||||
)
|
||||
}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
@ -71,14 +85,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
||||
<style>
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#root img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.element {
|
||||
position: absolute;
|
||||
|
@ -1,197 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../components/hui-image";
|
||||
|
||||
import computeDomain from "../../../common/entity/compute_domain";
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import toggleEntity from "../common/entity/toggle-entity";
|
||||
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import { longPressBind } from "../common/directives/long-press-directive";
|
||||
|
||||
const UNAVAILABLE = "Unavailable";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HuiPictureEntityCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
min-height: 75px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
ha-card.canInteract {
|
||||
cursor: pointer;
|
||||
}
|
||||
.footer {
|
||||
@apply --paper-font-common-nowrap;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
color: white;
|
||||
}
|
||||
.both {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.state {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card id="card">
|
||||
<hui-image
|
||||
hass="[[hass]]"
|
||||
image="[[_config.image]]"
|
||||
state-image="[[_config.state_image]]"
|
||||
camera-image="[[_getCameraImage(_config)]]"
|
||||
entity="[[_config.entity]]"
|
||||
aspect-ratio="[[_config.aspect_ratio]]"
|
||||
></hui-image>
|
||||
<template is="dom-if" if="[[_showNameAndState(_config)]]">
|
||||
<div class="footer both">
|
||||
<div>[[_name]]</div>
|
||||
<div>[[_state]]</div>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showName(_config)]]">
|
||||
<div class="footer">[[_name]]</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showState(_config)]]">
|
||||
<div class="footer state">[[_state]]</div>
|
||||
</template>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_hassChanged",
|
||||
},
|
||||
_config: Object,
|
||||
_name: String,
|
||||
_state: String,
|
||||
};
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
this._entityDomain = computeDomain(config.entity);
|
||||
if (
|
||||
this._entityDomain !== "camera" &&
|
||||
(!config.image && !config.state_image && !config.camera_image)
|
||||
) {
|
||||
throw new Error("No image source configured.");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
const card = this.shadowRoot.querySelector("#card");
|
||||
longPressBind(card);
|
||||
card.addEventListener("ha-click", () => this._cardClicked(false));
|
||||
card.addEventListener("ha-hold", () => this._cardClicked(true));
|
||||
}
|
||||
|
||||
_hassChanged(hass) {
|
||||
const config = this._config;
|
||||
const entityId = config.entity;
|
||||
const stateObj = hass.states[entityId];
|
||||
|
||||
// Nothing changed
|
||||
if (
|
||||
(!stateObj && this._oldState === UNAVAILABLE) ||
|
||||
(stateObj && stateObj.state === this._oldState)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name;
|
||||
let state;
|
||||
let stateLabel;
|
||||
let available;
|
||||
|
||||
if (stateObj) {
|
||||
name = config.name || computeStateName(stateObj);
|
||||
state = stateObj.state;
|
||||
stateLabel = computeStateDisplay(this.localize, stateObj);
|
||||
available = true;
|
||||
} else {
|
||||
name = config.name || entityId;
|
||||
state = UNAVAILABLE;
|
||||
stateLabel = this.localize("state.default.unavailable");
|
||||
available = false;
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
_name: name,
|
||||
_state: stateLabel,
|
||||
_oldState: state,
|
||||
});
|
||||
|
||||
this.$.card.classList.toggle("canInteract", available);
|
||||
}
|
||||
|
||||
_showNameAndState(config) {
|
||||
return config.show_name !== false && config.show_state !== false;
|
||||
}
|
||||
|
||||
_showName(config) {
|
||||
return config.show_name !== false && config.show_state === false;
|
||||
}
|
||||
|
||||
_showState(config) {
|
||||
return config.show_name === false && config.show_state !== false;
|
||||
}
|
||||
|
||||
_cardClicked(hold) {
|
||||
const config = this._config;
|
||||
const entityId = config.entity;
|
||||
|
||||
if (!(entityId in this.hass.states)) return;
|
||||
|
||||
const action = hold ? config.hold_action : config.tap_action || "more-info";
|
||||
|
||||
switch (action) {
|
||||
case "toggle":
|
||||
toggleEntity(this.hass, entityId);
|
||||
break;
|
||||
case "more-info":
|
||||
this.fire("hass-more-info", { entityId });
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
_getCameraImage(config) {
|
||||
return this._entityDomain === "camera"
|
||||
? config.entity
|
||||
: config.camera_image;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-picture-entity-card", HuiPictureEntityCard);
|
172
src/panels/lovelace/cards/hui-picture-entity-card.ts
Normal file
172
src/panels/lovelace/cards/hui-picture-entity-card.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html/lib/shady-render";
|
||||
import { classMap } from "lit-html/directives/classMap";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../components/hui-image";
|
||||
|
||||
import computeDomain from "../../../common/entity/compute_domain";
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { handleClick } from "../common/handle-click";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
|
||||
interface Config extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
name?: string;
|
||||
image?: string;
|
||||
camera_image?: string;
|
||||
state_image?: {};
|
||||
aspect_ratio?: string;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
show_name?: boolean;
|
||||
show_state?: boolean;
|
||||
}
|
||||
|
||||
class HuiPictureEntityCard extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceCard {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 3;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Invalid Configuration: 'entity' required");
|
||||
}
|
||||
|
||||
if (
|
||||
computeDomain(config.entity) !== "camera" &&
|
||||
(!config.image && !config.state_image && !config.camera_image)
|
||||
) {
|
||||
throw new Error("No image source configured.");
|
||||
}
|
||||
|
||||
this._config = { show_name: true, show_state: true, ...config };
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config || !this.hass || !this.hass.states[this._config.entity]) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
const name = this._config.name || computeStateName(stateObj);
|
||||
const state = computeStateDisplay(
|
||||
this.localize,
|
||||
stateObj,
|
||||
this.hass.language
|
||||
);
|
||||
|
||||
let footer: TemplateResult | string = "";
|
||||
if (this._config.show_name && this._config.show_state) {
|
||||
footer = html`
|
||||
<div class="footer both">
|
||||
<div>${name}</div>
|
||||
<div>${state}</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (this._config.show_name) {
|
||||
footer = html`
|
||||
<div class="footer">${name}</div>
|
||||
`;
|
||||
} else if (this._config.show_state) {
|
||||
footer = html`
|
||||
<div class="footer state">${state}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card>
|
||||
<hui-image
|
||||
.hass="${this.hass}"
|
||||
.image="${this._config.image}"
|
||||
.stateImage="${this._config.state_image}"
|
||||
.cameraImage="${
|
||||
computeDomain(this._config.entity) === "camera"
|
||||
? this._config.entity
|
||||
: this._config.camera_image
|
||||
}"
|
||||
.entity="${this._config.entity}"
|
||||
.aspectRatio="${this._config.aspect_ratio}"
|
||||
@ha-click="${this._handleTap}"
|
||||
@ha-hold="${this._handleHold}"
|
||||
.longPress="${longPress()}"
|
||||
class="${
|
||||
classMap({
|
||||
clickable: stateObj.state !== UNAVAILABLE,
|
||||
})
|
||||
}"
|
||||
></hui-image>
|
||||
${footer}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
min-height: 75px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
hui-image.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.footer {
|
||||
@apply --paper-font-common-nowrap;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
color: white;
|
||||
}
|
||||
.both {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.state {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleTap() {
|
||||
handleClick(this, this.hass!, this._config!, false);
|
||||
}
|
||||
|
||||
private _handleHold() {
|
||||
handleClick(this, this.hass!, this._config!, true);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-picture-entity-card": HuiPictureEntityCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-picture-entity-card", HuiPictureEntityCard);
|
@ -3,36 +3,37 @@ import { classMap } from "lit-html/directives/classMap";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
|
||||
import { EntityConfig } from "../entity-rows/types";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
import computeDomain from "../../../common/entity/compute_domain";
|
||||
import stateIcon from "../../../common/entity/state_icon";
|
||||
import toggleEntity from "../common/entity/toggle-entity";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import "../components/hui-image";
|
||||
import { handleClick } from "../common/handle-click";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { toggleEntity } from "../common/entity/toggle-entity";
|
||||
|
||||
const STATES_OFF = new Set(["closed", "locked", "not_home", "off"]);
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
interface Config extends LovelaceCardConfig {
|
||||
entities: EntityConfig[];
|
||||
title?: string;
|
||||
navigation_path?: string;
|
||||
image?: string;
|
||||
camera_image?: string;
|
||||
state_image?: {};
|
||||
aspect_ratio?: string;
|
||||
entity?: string;
|
||||
force_dialog?: boolean;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
}
|
||||
|
||||
class HuiPictureGlanceCard extends hassLocalizeLitMixin(LitElement)
|
||||
@ -87,19 +88,22 @@ class HuiPictureGlanceCard extends hassLocalizeLitMixin(LitElement)
|
||||
return html``;
|
||||
}
|
||||
|
||||
const isClickable =
|
||||
this._config.navigation_path || this._config.camera_image;
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card>
|
||||
<hui-image
|
||||
class="${
|
||||
classMap({
|
||||
clickable: Boolean(isClickable),
|
||||
clickable: Boolean(
|
||||
this._config.tap_action ||
|
||||
this._config.hold_action ||
|
||||
this._config.camera_image
|
||||
),
|
||||
})
|
||||
}"
|
||||
@click="${this._handleImageClick}"
|
||||
@ha-click="${this._handleTap}"
|
||||
@ha-hold="${this._handleHold}"
|
||||
.longPress="${longPress()}"
|
||||
.hass="${this.hass}"
|
||||
.image="${this._config.image}"
|
||||
.stateImage="${this._config.state_image}"
|
||||
@ -167,22 +171,20 @@ class HuiPictureGlanceCard extends hassLocalizeLitMixin(LitElement)
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleTap() {
|
||||
handleClick(this, this.hass!, this._config!, false);
|
||||
}
|
||||
|
||||
private _handleHold() {
|
||||
handleClick(this, this.hass!, this._config!, true);
|
||||
}
|
||||
|
||||
private _openDialog(ev: MouseEvent): void {
|
||||
fireEvent(this, "hass-more-info", { entityId: (ev.target as any).entity });
|
||||
}
|
||||
|
||||
private _callService(ev: MouseEvent): void {
|
||||
toggleEntity(this.hass, (ev.target as any).entity);
|
||||
}
|
||||
|
||||
private _handleImageClick(): void {
|
||||
if (this._config!.navigation_path) {
|
||||
navigate(this, this._config!.navigation_path!);
|
||||
} else if (this._config!.camera_image) {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: this._config!.camera_image,
|
||||
});
|
||||
}
|
||||
toggleEntity(this.hass!, (ev.target as any).entity);
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
|
@ -1,320 +0,0 @@
|
||||
import { LitElement, html, svg } from "@polymer/lit-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import stateIcon from "../../../common/entity/state_icon";
|
||||
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
|
||||
class HuiSensorCard extends EventsMixin(LitElement) {
|
||||
set hass(hass) {
|
||||
this._hass = hass;
|
||||
const entity = hass.states[this._config.entity];
|
||||
if (entity && this._entity !== entity) {
|
||||
this._entity = entity;
|
||||
if (
|
||||
this._config.graph !== "none" &&
|
||||
entity.attributes.unit_of_measurement
|
||||
) {
|
||||
this._getHistory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_hass: {},
|
||||
_config: {},
|
||||
_entity: {},
|
||||
_line: String,
|
||||
_min: Number,
|
||||
_max: Number,
|
||||
};
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config.entity || config.entity.split(".")[0] !== "sensor") {
|
||||
throw new Error("Specify an entity from within the sensor domain.");
|
||||
}
|
||||
|
||||
const cardConfig = {
|
||||
detail: 1,
|
||||
icon: false,
|
||||
height: 100,
|
||||
hours_to_show: 24,
|
||||
line_color: "var(--accent-color)",
|
||||
line_width: 5,
|
||||
...config,
|
||||
};
|
||||
cardConfig.hours_to_show = Number(cardConfig.hours_to_show);
|
||||
cardConfig.height = Number(cardConfig.height);
|
||||
cardConfig.line_width = Number(cardConfig.line_width);
|
||||
cardConfig.detail =
|
||||
cardConfig.detail === 1 || cardConfig.detail === 2
|
||||
? cardConfig.detail
|
||||
: 1;
|
||||
|
||||
this._config = cardConfig;
|
||||
}
|
||||
|
||||
shouldUpdate(changedProps) {
|
||||
const change = changedProps.has("_entity") || changedProps.has("_line");
|
||||
return change;
|
||||
}
|
||||
|
||||
render({ _config, _entity, _line } = this) {
|
||||
return html`
|
||||
${this._style()}
|
||||
<ha-card @click="${this._handleClick}">
|
||||
<div class="flex">
|
||||
<div class="icon">
|
||||
<ha-icon .icon="${this._computeIcon(_entity)}"></ha-icon>
|
||||
</div>
|
||||
<div class="header">
|
||||
<span class="name">${this._computeName(_entity)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex info">
|
||||
<span id="value">${_entity.state}</span>
|
||||
<span id="measurement">${this._computeUom(_entity)}</span>
|
||||
</div>
|
||||
<div class="graph">
|
||||
<div>
|
||||
${
|
||||
_line
|
||||
? svg`
|
||||
<svg width='100%' height='100%' viewBox='0 0 500 ${_config.height}'>
|
||||
<path d=${_line} fill='none' stroke=${_config.line_color}
|
||||
stroke-width=${_config.line_width}
|
||||
stroke-linecap='round' stroke-linejoin='round' />
|
||||
</svg>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
_handleClick() {
|
||||
this.fire("hass-more-info", { entityId: this._config.entity });
|
||||
}
|
||||
|
||||
_computeIcon(item) {
|
||||
return this._config.icon || stateIcon(item);
|
||||
}
|
||||
|
||||
_computeName(item) {
|
||||
return this._config.name || computeStateName(item);
|
||||
}
|
||||
|
||||
_computeUom(item) {
|
||||
return this._config.unit || item.attributes.unit_of_measurement;
|
||||
}
|
||||
|
||||
_coordinates(history, hours, width, detail = 1) {
|
||||
history = history.filter((item) => !Number.isNaN(Number(item.state)));
|
||||
this._min = Math.min.apply(Math, history.map((item) => Number(item.state)));
|
||||
this._max = Math.max.apply(Math, history.map((item) => Number(item.state)));
|
||||
const now = new Date().getTime();
|
||||
|
||||
const reduce = (res, item, min = false) => {
|
||||
const age = now - new Date(item.last_changed).getTime();
|
||||
let key = Math.abs(age / (1000 * 3600) - hours);
|
||||
if (min) {
|
||||
key = (key - Math.floor(key)) * 60;
|
||||
key = (Math.round(key / 10) * 10).toString()[0];
|
||||
} else {
|
||||
key = Math.floor(key);
|
||||
}
|
||||
if (!res[key]) res[key] = [];
|
||||
res[key].push(item);
|
||||
return res;
|
||||
};
|
||||
history = history.reduce((res, item) => reduce(res, item), []);
|
||||
if (detail > 1) {
|
||||
history = history.map((entry) =>
|
||||
entry.reduce((res, item) => reduce(res, item, true), [])
|
||||
);
|
||||
}
|
||||
return this._calcPoints(history, hours, width, detail);
|
||||
}
|
||||
|
||||
_calcPoints(history, hours, width, detail = 1) {
|
||||
const coords = [];
|
||||
const margin = this._config.line_width;
|
||||
const height = this._config.height - margin * 4;
|
||||
width -= margin * 2;
|
||||
let yRatio = (this._max - this._min) / height;
|
||||
yRatio = yRatio !== 0 ? yRatio : height;
|
||||
let xRatio = width / (hours - (detail === 1 ? 1 : 0));
|
||||
xRatio = isFinite(xRatio) ? xRatio : width;
|
||||
const getCoords = (item, i, offset = 0, depth = 1) => {
|
||||
if (depth > 1)
|
||||
return item.forEach((subItem, index) =>
|
||||
getCoords(subItem, i, index, depth - 1)
|
||||
);
|
||||
const average =
|
||||
item.reduce((sum, entry) => sum + parseFloat(entry.state), 0) /
|
||||
item.length;
|
||||
|
||||
const x = xRatio * (i + offset / 6) + margin;
|
||||
const y = height - (average - this._min) / yRatio + margin * 2;
|
||||
return coords.push([x, y]);
|
||||
};
|
||||
|
||||
history.forEach((item, i) => getCoords(item, i, 0, detail));
|
||||
if (coords.length === 1) coords[1] = [width + margin, coords[0][1]];
|
||||
coords.push([width + margin, coords[coords.length - 1][1]]);
|
||||
return coords;
|
||||
}
|
||||
|
||||
_getPath(coords) {
|
||||
let next;
|
||||
let Z;
|
||||
const X = 0;
|
||||
const Y = 1;
|
||||
let path = "";
|
||||
let last = coords.filter(Boolean)[0];
|
||||
|
||||
path += `M ${last[X]},${last[Y]}`;
|
||||
|
||||
for (let i = 0; i < coords.length; i++) {
|
||||
next = coords[i];
|
||||
Z = this._midPoint(last[X], last[Y], next[X], next[Y]);
|
||||
path += ` ${Z[X]},${Z[Y]}`;
|
||||
path += ` Q${next[X]},${next[Y]}`;
|
||||
last = next;
|
||||
}
|
||||
|
||||
path += ` ${next[X]},${next[Y]}`;
|
||||
return path;
|
||||
}
|
||||
|
||||
_midPoint(Ax, Ay, Bx, By) {
|
||||
const Zx = (Ax - Bx) / 2 + Bx;
|
||||
const Zy = (Ay - By) / 2 + By;
|
||||
return [Zx, Zy];
|
||||
}
|
||||
|
||||
async _getHistory() {
|
||||
const endTime = new Date();
|
||||
const startTime = new Date();
|
||||
startTime.setHours(endTime.getHours() - this._config.hours_to_show);
|
||||
const stateHistory = await this._fetchRecent(
|
||||
this._config.entity,
|
||||
startTime,
|
||||
endTime
|
||||
);
|
||||
|
||||
if (stateHistory[0].length < 1) return;
|
||||
const coords = this._coordinates(
|
||||
stateHistory[0],
|
||||
this._config.hours_to_show,
|
||||
500,
|
||||
this._config.detail
|
||||
);
|
||||
this._line = this._getPath(coords);
|
||||
}
|
||||
|
||||
async _fetchRecent(entityId, startTime, endTime) {
|
||||
let url = "history/period";
|
||||
if (startTime) url += "/" + startTime.toISOString();
|
||||
url += "?filter_entity_id=" + entityId;
|
||||
if (endTime) url += "&end_time=" + endTime.toISOString();
|
||||
|
||||
return await this._hass.callApi("GET", url);
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
_style() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
opacity: 0.8;
|
||||
position: relative;
|
||||
}
|
||||
.name {
|
||||
display: block;
|
||||
display: -webkit-box;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
max-height: 1.4rem;
|
||||
margin-top: 2px;
|
||||
opacity: 0.8;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
.icon {
|
||||
color: var(--paper-item-icon-color, #44739e);
|
||||
display: inline-block;
|
||||
flex: 0 0 40px;
|
||||
line-height: 40px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
}
|
||||
.info {
|
||||
flex-wrap: wrap;
|
||||
margin: 16px 0 16px 8px;
|
||||
}
|
||||
#value {
|
||||
display: inline-block;
|
||||
font-size: 2rem;
|
||||
font-weight: 400;
|
||||
line-height: 1em;
|
||||
margin-right: 4px;
|
||||
}
|
||||
#measurement {
|
||||
align-self: flex-end;
|
||||
display: inline-block;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.2em;
|
||||
margin-top: 0.1em;
|
||||
opacity: 0.6;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.graph {
|
||||
align-self: flex-end;
|
||||
margin: auto;
|
||||
margin-bottom: 0px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.graph > div {
|
||||
align-self: flex-end;
|
||||
margin: auto 8px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-sensor-card", HuiSensorCard);
|
412
src/panels/lovelace/cards/hui-sensor-card.ts
Executable file
412
src/panels/lovelace/cards/hui-sensor-card.ts
Executable file
@ -0,0 +1,412 @@
|
||||
import {
|
||||
html,
|
||||
svg,
|
||||
LitElement,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
} from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
import "@polymer/paper-spinner/paper-spinner";
|
||||
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import stateIcon from "../../../common/entity/state_icon";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import { fetchRecent } from "../../../data/history";
|
||||
|
||||
const midPoint = (
|
||||
_Ax: number,
|
||||
_Ay: number,
|
||||
_Bx: number,
|
||||
_By: number
|
||||
): number[] => {
|
||||
const _Zx = (_Ax - _Bx) / 2 + _Bx;
|
||||
const _Zy = (_Ay - _By) / 2 + _By;
|
||||
return [_Zx, _Zy];
|
||||
};
|
||||
|
||||
const getPath = (coords: number[][]): string => {
|
||||
let next;
|
||||
let Z;
|
||||
const X = 0;
|
||||
const Y = 1;
|
||||
let path = "";
|
||||
let last = coords.filter(Boolean)[0];
|
||||
|
||||
path += `M ${last[X]},${last[Y]}`;
|
||||
|
||||
for (const coord of coords) {
|
||||
next = coord;
|
||||
Z = midPoint(last[X], last[Y], next[X], next[Y]);
|
||||
path += ` ${Z[X]},${Z[Y]}`;
|
||||
path += ` Q${next[X]},${next[Y]}`;
|
||||
last = next;
|
||||
}
|
||||
|
||||
path += ` ${next[X]},${next[Y]}`;
|
||||
return path;
|
||||
};
|
||||
|
||||
const calcPoints = (
|
||||
history: any,
|
||||
hours: number,
|
||||
width: number,
|
||||
detail: number,
|
||||
min: number,
|
||||
max: number
|
||||
): number[][] => {
|
||||
const coords = [] as number[][];
|
||||
const margin = 5;
|
||||
const height = 80;
|
||||
width -= 10;
|
||||
let yRatio = (max - min) / height;
|
||||
yRatio = yRatio !== 0 ? yRatio : height;
|
||||
let xRatio = width / (hours - (detail === 1 ? 1 : 0));
|
||||
xRatio = isFinite(xRatio) ? xRatio : width;
|
||||
const getCoords = (item, i, offset = 0, depth = 1) => {
|
||||
if (depth > 1) {
|
||||
return item.forEach((subItem, index) =>
|
||||
getCoords(subItem, i, index, depth - 1)
|
||||
);
|
||||
}
|
||||
const average =
|
||||
item.reduce((sum, entry) => sum + parseFloat(entry.state), 0) /
|
||||
item.length;
|
||||
|
||||
const x = xRatio * (i + offset / 6) + margin;
|
||||
const y = height - (average - min) / yRatio + margin * 2;
|
||||
return coords.push([x, y]);
|
||||
};
|
||||
|
||||
history.forEach((item, i) => getCoords(item, i, 0, detail));
|
||||
if (coords.length === 1) {
|
||||
coords[1] = [width + margin, coords[0][1]];
|
||||
}
|
||||
|
||||
coords.push([width + margin, coords[coords.length - 1][1]]);
|
||||
return coords;
|
||||
};
|
||||
|
||||
const coordinates = (
|
||||
history: any,
|
||||
hours: number,
|
||||
width: number,
|
||||
detail: number
|
||||
): number[][] => {
|
||||
history.forEach((item) => (item.state = Number(item.state)));
|
||||
history = history.filter((item) => !Number.isNaN(item.state));
|
||||
|
||||
const min = Math.min.apply(Math, history.map((item) => item.state));
|
||||
const max = Math.max.apply(Math, history.map((item) => item.state));
|
||||
const now = new Date().getTime();
|
||||
|
||||
const reduce = (res, item, point) => {
|
||||
const age = now - new Date(item.last_changed).getTime();
|
||||
|
||||
let key = Math.abs(age / (1000 * 3600) - hours);
|
||||
if (point) {
|
||||
key = (key - Math.floor(key)) * 60;
|
||||
key = Number((Math.round(key / 10) * 10).toString()[0]);
|
||||
} else {
|
||||
key = Math.floor(key);
|
||||
}
|
||||
if (!res[key]) {
|
||||
res[key] = [];
|
||||
}
|
||||
res[key].push(item);
|
||||
return res;
|
||||
};
|
||||
|
||||
history = history.reduce((res, item) => reduce(res, item, false), []);
|
||||
if (detail > 1) {
|
||||
history = history.map((entry) =>
|
||||
entry.reduce((res, item) => reduce(res, item, true), [])
|
||||
);
|
||||
}
|
||||
return calcPoints(history, hours, width, detail, min, max);
|
||||
};
|
||||
|
||||
interface Config extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
graph?: string;
|
||||
unit?: string;
|
||||
detail?: number;
|
||||
theme?: string;
|
||||
hours_to_show?: number;
|
||||
}
|
||||
|
||||
class HuiSensorCard extends LitElement implements LovelaceCard {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
private _history?: any;
|
||||
private _date?: Date;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
_history: {},
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config.entity || config.entity.split(".")[0] !== "sensor") {
|
||||
throw new Error("Specify an entity from within the sensor domain.");
|
||||
}
|
||||
|
||||
const cardConfig = {
|
||||
detail: 1,
|
||||
theme: "default",
|
||||
hours_to_show: 24,
|
||||
...config,
|
||||
};
|
||||
|
||||
cardConfig.hours_to_show = Number(cardConfig.hours_to_show);
|
||||
cardConfig.detail =
|
||||
cardConfig.detail === 1 || cardConfig.detail === 2
|
||||
? cardConfig.detail
|
||||
: 1;
|
||||
|
||||
this._config = cardConfig;
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 3;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config || !this.hass) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
|
||||
let graph;
|
||||
|
||||
if (this._config.graph === "line") {
|
||||
if (!stateObj.attributes.unit_of_measurement) {
|
||||
graph = html`
|
||||
<div class="not-found">
|
||||
Entity: ${this._config.entity} - Has no Unit of Measurement and
|
||||
therefore can not display a line graph.
|
||||
</div>
|
||||
`;
|
||||
} else if (!this._history) {
|
||||
graph = svg`
|
||||
<svg width="100%" height="100%" viewBox="0 0 500 100"></svg>
|
||||
`;
|
||||
} else {
|
||||
graph = svg`
|
||||
<svg width="100%" height="100%" viewBox="0 0 500 100">
|
||||
<path
|
||||
d="${this._history}"
|
||||
fill="none"
|
||||
stroke="var(--accent-color)"
|
||||
stroke-width="5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
graph = "";
|
||||
}
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card @click="${this._handleClick}">
|
||||
${
|
||||
!stateObj
|
||||
? html`
|
||||
<div class="not-found">
|
||||
Entity not available: ${this._config.entity}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="flex">
|
||||
<div class="icon">
|
||||
<ha-icon
|
||||
.icon="${this._config.icon || stateIcon(stateObj)}"
|
||||
></ha-icon>
|
||||
</div>
|
||||
<div class="header">
|
||||
<span class="name"
|
||||
>${this._config.name || computeStateName(stateObj)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex info">
|
||||
<span id="value">${stateObj.state}</span>
|
||||
<span id="measurement"
|
||||
>${
|
||||
this._config.unit ||
|
||||
stateObj.attributes.unit_of_measurement
|
||||
}</span
|
||||
>
|
||||
</div>
|
||||
<div class="graph"><div>${graph}</div></div>
|
||||
`
|
||||
}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._date = new Date();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (!this._config || this._config.graph !== "line" || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.themes !== this.hass.themes) {
|
||||
applyThemesOnElement(this, this.hass.themes, this._config!.theme);
|
||||
}
|
||||
|
||||
const minute = 60000;
|
||||
if (changedProps.has("_config")) {
|
||||
this._getHistory();
|
||||
} else if (Date.now() - this._date!.getTime() >= minute) {
|
||||
this._getHistory();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleClick(): void {
|
||||
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
|
||||
}
|
||||
|
||||
private async _getHistory(): Promise<void> {
|
||||
const endTime = new Date();
|
||||
const startTime = new Date();
|
||||
startTime.setHours(endTime.getHours() - this._config!.hours_to_show!);
|
||||
|
||||
const stateHistory = await fetchRecent(
|
||||
this.hass,
|
||||
this._config!.entity,
|
||||
startTime,
|
||||
endTime
|
||||
);
|
||||
|
||||
if (stateHistory[0].length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coords = coordinates(
|
||||
stateHistory[0],
|
||||
this._config!.hours_to_show!,
|
||||
500,
|
||||
this._config!.detail!
|
||||
);
|
||||
|
||||
this._history = getPath(coords);
|
||||
this._date = new Date();
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
opacity: 0.8;
|
||||
position: relative;
|
||||
}
|
||||
.name {
|
||||
display: block;
|
||||
display: -webkit-box;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
max-height: 1.4rem;
|
||||
margin-top: 2px;
|
||||
opacity: 0.8;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
.icon {
|
||||
color: var(--paper-item-icon-color, #44739e);
|
||||
display: inline-block;
|
||||
flex: 0 0 40px;
|
||||
line-height: 40px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
}
|
||||
.info {
|
||||
flex-wrap: wrap;
|
||||
margin: 16px 0 16px 8px;
|
||||
}
|
||||
#value {
|
||||
display: inline-block;
|
||||
font-size: 2rem;
|
||||
font-weight: 400;
|
||||
line-height: 1em;
|
||||
margin-right: 4px;
|
||||
}
|
||||
#measurement {
|
||||
align-self: flex-end;
|
||||
display: inline-block;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.2em;
|
||||
margin-top: 0.1em;
|
||||
opacity: 0.6;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.graph {
|
||||
align-self: flex-end;
|
||||
margin: auto;
|
||||
margin-bottom: 0px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.graph > div {
|
||||
align-self: flex-end;
|
||||
margin: auto 8px;
|
||||
}
|
||||
.not-found {
|
||||
flex: 1;
|
||||
background-color: yellow;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-sensor-card": HuiSensorCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-sensor-card", HuiSensorCard);
|
@ -9,17 +9,17 @@ import "../../../components/ha-icon";
|
||||
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import {
|
||||
fetchItems,
|
||||
completeItem,
|
||||
saveEdit,
|
||||
updateItem,
|
||||
ShoppingListItem,
|
||||
clearItems,
|
||||
addItem,
|
||||
} from "../../../data/shopping-list";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
interface Config extends LovelaceCardConfig {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
@ -256,15 +256,15 @@ class HuiShoppingListCard extends hassLocalizeLitMixin(LitElement)
|
||||
}
|
||||
|
||||
private _completeItem(ev): void {
|
||||
completeItem(this.hass!, ev.target.itemId, ev.target.checked).catch(() =>
|
||||
this._fetchData()
|
||||
);
|
||||
updateItem(this.hass!, ev.target.itemId, {
|
||||
complete: ev.target.checked,
|
||||
}).catch(() => this._fetchData());
|
||||
}
|
||||
|
||||
private _saveEdit(ev): void {
|
||||
saveEdit(this.hass!, ev.target.itemId, ev.target.value).catch(() =>
|
||||
this._fetchData()
|
||||
);
|
||||
updateItem(this.hass!, ev.target.itemId, {
|
||||
name: ev.target.value,
|
||||
}).catch(() => this._fetchData());
|
||||
|
||||
ev.target.blur();
|
||||
}
|
||||
|
@ -3,11 +3,12 @@ import { TemplateResult } from "lit-html";
|
||||
|
||||
import createCardElement from "../common/create-card-element";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
cards: LovelaceConfig[];
|
||||
interface Config extends LovelaceCardConfig {
|
||||
cards: LovelaceCardConfig[];
|
||||
}
|
||||
|
||||
export abstract class HuiStackCard extends LitElement implements LovelaceCard {
|
||||
|
@ -15,7 +15,8 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
|
||||
import { HomeAssistant, ClimateEntity } from "../../../types";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
@ -43,9 +44,10 @@ const modeIcons = {
|
||||
idle: "hass:power-sleep",
|
||||
};
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
interface Config extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
theme?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
function formatTemp(temps: string[]): string {
|
||||
@ -96,7 +98,8 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
|
||||
<div id="root">
|
||||
<div id="thermostat"></div>
|
||||
<div id="tooltip">
|
||||
<div class="title">${computeStateName(stateObj)}</div>
|
||||
<div class="title">${this._config.name ||
|
||||
computeStateName(stateObj)}</div>
|
||||
<div class="current-temperature">
|
||||
<span class="current-temperature-text">
|
||||
${stateObj.attributes.current_temperature}
|
||||
|
@ -20,7 +20,7 @@ export const computeTooltip = (
|
||||
: config.entity;
|
||||
}
|
||||
|
||||
switch (config.tap_action) {
|
||||
switch (config.tap_action && config.tap_action.action) {
|
||||
case "navigate":
|
||||
tooltip = `Navigate to ${config.navigation_path}`;
|
||||
break;
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
import "../cards/hui-alarm-panel-card";
|
||||
import "../cards/hui-conditional-card.ts";
|
||||
import "../cards/hui-entities-card.ts";
|
||||
import "../cards/hui-entity-button-card.ts";
|
||||
import "../cards/hui-conditional-card";
|
||||
import "../cards/hui-entities-card";
|
||||
import "../cards/hui-entity-button-card";
|
||||
import "../cards/hui-entity-filter-card";
|
||||
import "../cards/hui-error-card.ts";
|
||||
import "../cards/hui-glance-card.ts";
|
||||
import "../cards/hui-error-card";
|
||||
import "../cards/hui-glance-card";
|
||||
import "../cards/hui-history-graph-card";
|
||||
import "../cards/hui-horizontal-stack-card.ts";
|
||||
import "../cards/hui-iframe-card.ts";
|
||||
import "../cards/hui-horizontal-stack-card";
|
||||
import "../cards/hui-iframe-card";
|
||||
import "../cards/hui-light-card";
|
||||
import "../cards/hui-map-card";
|
||||
import "../cards/hui-markdown-card.ts";
|
||||
import "../cards/hui-markdown-card";
|
||||
import "../cards/hui-media-control-card";
|
||||
import "../cards/hui-picture-card";
|
||||
import "../cards/hui-picture-elements-card";
|
||||
@ -20,9 +20,9 @@ import "../cards/hui-picture-entity-card";
|
||||
import "../cards/hui-picture-glance-card";
|
||||
import "../cards/hui-plant-status-card";
|
||||
import "../cards/hui-sensor-card";
|
||||
import "../cards/hui-vertical-stack-card.ts";
|
||||
import "../cards/hui-vertical-stack-card";
|
||||
import "../cards/hui-shopping-list-card";
|
||||
import "../cards/hui-thermostat-card.ts";
|
||||
import "../cards/hui-thermostat-card";
|
||||
import "../cards/hui-weather-forecast-card";
|
||||
import "../cards/hui-gauge-card";
|
||||
|
||||
|
@ -10,6 +10,7 @@ import "../entity-rows/hui-lock-entity-row";
|
||||
import "../entity-rows/hui-media-player-entity-row";
|
||||
import "../entity-rows/hui-scene-entity-row";
|
||||
import "../entity-rows/hui-script-entity-row";
|
||||
import "../entity-rows/hui-sensor-entity-row";
|
||||
import "../entity-rows/hui-text-entity-row";
|
||||
import "../entity-rows/hui-timer-entity-row";
|
||||
import "../entity-rows/hui-toggle-entity-row";
|
||||
@ -28,6 +29,7 @@ const SPECIAL_TYPES = new Set([
|
||||
"weblink",
|
||||
]);
|
||||
const DOMAIN_TO_ELEMENT_TYPE = {
|
||||
alert: "toggle",
|
||||
automation: "toggle",
|
||||
climate: "climate",
|
||||
cover: "cover",
|
||||
@ -42,6 +44,7 @@ const DOMAIN_TO_ELEMENT_TYPE = {
|
||||
lock: "lock",
|
||||
scene: "scene",
|
||||
script: "script",
|
||||
sensor: "sensor",
|
||||
timer: "timer",
|
||||
switch: "toggle",
|
||||
vacuum: "toggle",
|
||||
|
@ -1,29 +0,0 @@
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceConfig } from "../types";
|
||||
|
||||
export const getCardConfig = (
|
||||
hass: HomeAssistant,
|
||||
cardId: string
|
||||
): Promise<string> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/config/card/get",
|
||||
card_id: cardId,
|
||||
});
|
||||
|
||||
export const updateCardConfig = (
|
||||
hass: HomeAssistant,
|
||||
cardId: string,
|
||||
config: LovelaceConfig | string,
|
||||
configFormat: "json" | "yaml"
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/config/card/update",
|
||||
card_id: cardId,
|
||||
card_config: config,
|
||||
format: configFormat,
|
||||
});
|
||||
|
||||
export const migrateConfig = (hass: HomeAssistant): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/config/migrate",
|
||||
});
|
@ -19,6 +19,8 @@ class LongPress extends HTMLElement implements LongPress {
|
||||
protected ripple: any;
|
||||
protected timer: number | undefined;
|
||||
protected held: boolean;
|
||||
protected cooldownStart: boolean;
|
||||
protected cooldownEnd: boolean;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@ -26,6 +28,8 @@ class LongPress extends HTMLElement implements LongPress {
|
||||
this.ripple = document.createElement("mwc-ripple");
|
||||
this.timer = undefined;
|
||||
this.held = false;
|
||||
this.cooldownStart = false;
|
||||
this.cooldownEnd = false;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
@ -41,7 +45,8 @@ class LongPress extends HTMLElement implements LongPress {
|
||||
this.ripple.primary = true;
|
||||
|
||||
[
|
||||
isTouch ? "touchcancel" : "mouseout",
|
||||
"touchcancel",
|
||||
"mouseout",
|
||||
"mouseup",
|
||||
"touchmove",
|
||||
"mousewheel",
|
||||
@ -80,6 +85,9 @@ class LongPress extends HTMLElement implements LongPress {
|
||||
});
|
||||
|
||||
const clickStart = (ev: Event) => {
|
||||
if (this.cooldownStart) {
|
||||
return;
|
||||
}
|
||||
this.held = false;
|
||||
let x;
|
||||
let y;
|
||||
@ -94,30 +102,35 @@ class LongPress extends HTMLElement implements LongPress {
|
||||
this.startAnimation(x, y);
|
||||
this.held = true;
|
||||
}, this.holdTime);
|
||||
|
||||
this.cooldownStart = true;
|
||||
window.setTimeout(() => (this.cooldownStart = false), 100);
|
||||
};
|
||||
|
||||
const clickEnd = () => {
|
||||
clearTimeout(this.timer);
|
||||
this.stopAnimation();
|
||||
if (isTouch && this.timer === undefined) {
|
||||
const clickEnd = (ev: Event) => {
|
||||
if (
|
||||
this.cooldownEnd ||
|
||||
(ev instanceof TouchEvent && this.timer === undefined)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(this.timer);
|
||||
this.stopAnimation();
|
||||
this.timer = undefined;
|
||||
if (this.held) {
|
||||
element.dispatchEvent(new Event("ha-hold"));
|
||||
} else {
|
||||
element.dispatchEvent(new Event("ha-click"));
|
||||
}
|
||||
this.cooldownEnd = true;
|
||||
window.setTimeout(() => (this.cooldownEnd = false), 100);
|
||||
};
|
||||
|
||||
if (isTouch) {
|
||||
element.addEventListener("touchstart", clickStart, { passive: true });
|
||||
element.addEventListener("touchend", clickEnd);
|
||||
element.addEventListener("touchcancel", clickEnd);
|
||||
} else {
|
||||
element.addEventListener("mousedown", clickStart, { passive: true });
|
||||
element.addEventListener("click", clickEnd);
|
||||
}
|
||||
element.addEventListener("touchstart", clickStart, { passive: true });
|
||||
element.addEventListener("touchend", clickEnd);
|
||||
element.addEventListener("touchcancel", clickEnd);
|
||||
element.addEventListener("mousedown", clickStart, { passive: true });
|
||||
element.addEventListener("click", clickEnd);
|
||||
}
|
||||
|
||||
private startAnimation(x: number, y: number) {
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { STATES_OFF } from "../../../../common/const";
|
||||
import turnOnOffEntity from "./turn-on-off-entity";
|
||||
|
||||
export default function toggleEntity(hass, entityId) {
|
||||
const turnOn = STATES_OFF.includes(hass.states[entityId].state);
|
||||
turnOnOffEntity(hass, entityId, turnOn);
|
||||
}
|
10
src/panels/lovelace/common/entity/toggle-entity.ts
Normal file
10
src/panels/lovelace/common/entity/toggle-entity.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { STATES_OFF } from "../../../../common/const";
|
||||
import { turnOnOffEntity } from "./turn-on-off-entity";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
export const toggleEntity = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
): Promise<void> => {
|
||||
const turnOn = STATES_OFF.includes(hass.states[entityId].state);
|
||||
return turnOnOffEntity(hass, entityId, turnOn);
|
||||
};
|
@ -1,7 +1,12 @@
|
||||
import { STATES_OFF } from "../../../../common/const";
|
||||
import computeDomain from "../../../../common/entity/compute_domain";
|
||||
import { STATES_OFF } from "../../../../common/const";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
export default function turnOnOffEntities(hass, entityIds, turnOn = true) {
|
||||
export const turnOnOffEntities = (
|
||||
hass: HomeAssistant,
|
||||
entityIds: string[],
|
||||
turnOn = true
|
||||
): void => {
|
||||
const domainsToCall = {};
|
||||
entityIds.forEach((entityId) => {
|
||||
if (STATES_OFF.includes(hass.states[entityId].state) === turnOn) {
|
||||
@ -10,7 +15,9 @@ export default function turnOnOffEntities(hass, entityIds, turnOn = true) {
|
||||
? stateDomain
|
||||
: "homeassistant";
|
||||
|
||||
if (!(serviceDomain in domainsToCall)) domainsToCall[serviceDomain] = [];
|
||||
if (!(serviceDomain in domainsToCall)) {
|
||||
domainsToCall[serviceDomain] = [];
|
||||
}
|
||||
domainsToCall[serviceDomain].push(entityId);
|
||||
}
|
||||
});
|
||||
@ -31,4 +38,4 @@ export default function turnOnOffEntities(hass, entityIds, turnOn = true) {
|
||||
const entities = domainsToCall[domain];
|
||||
hass.callService(domain, service, { entity_id: entities });
|
||||
});
|
||||
}
|
||||
};
|
@ -1,6 +1,11 @@
|
||||
import computeDomain from "../../../../common/entity/compute_domain";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
export default function turnOnOffEntity(hass, entityId, turnOn = true) {
|
||||
export const turnOnOffEntity = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
turnOn = true
|
||||
): Promise<void> => {
|
||||
const stateDomain = computeDomain(entityId);
|
||||
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;
|
||||
|
||||
@ -16,5 +21,5 @@ export default function turnOnOffEntity(hass, entityId, turnOn = true) {
|
||||
service = turnOn ? "turn_on" : "turn_off";
|
||||
}
|
||||
|
||||
hass.callService(serviceDomain, service, { entity_id: entityId });
|
||||
}
|
||||
return hass.callService(serviceDomain, service, { entity_id: entityId });
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user