mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-02 14:07:55 +00:00
commit
f5022f4e1e
@ -69,7 +69,7 @@ class DemoAlarmPanelEntity extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
public ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
const hass = provideHass(this.$.demos);
|
const hass = provideHass(this.$.demos);
|
||||||
hass.addEntities(ENTITIES);
|
hass.addEntities(ENTITIES);
|
@ -74,7 +74,7 @@ class DemoConditional extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
public ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
const hass = provideHass(this.$.demos);
|
const hass = provideHass(this.$.demos);
|
||||||
hass.addEntities(ENTITIES);
|
hass.addEntities(ENTITIES);
|
@ -188,7 +188,7 @@ class DemoEntities extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
public ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
const hass = provideHass(this.$.demos);
|
const hass = provideHass(this.$.demos);
|
||||||
hass.addEntities(ENTITIES);
|
hass.addEntities(ENTITIES);
|
@ -89,7 +89,7 @@ class DemoEntityButtonEntity extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
public ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
const hass = provideHass(this.$.demos);
|
const hass = provideHass(this.$.demos);
|
||||||
hass.addEntities(ENTITIES);
|
hass.addEntities(ENTITIES);
|
@ -105,7 +105,7 @@ class DemoFilter extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
public ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
const hass = provideHass(this.$.demos);
|
const hass = provideHass(this.$.demos);
|
||||||
hass.addEntities(ENTITIES);
|
hass.addEntities(ENTITIES);
|
@ -89,10 +89,10 @@ const CONFIGS = [
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "Custom column width",
|
heading: "Custom number of columns",
|
||||||
config: `
|
config: `
|
||||||
- type: glance
|
- type: glance
|
||||||
column_width: calc(100% / 7)
|
columns: 7
|
||||||
entities:
|
entities:
|
||||||
- device_tracker.demo_paulus
|
- device_tracker.demo_paulus
|
||||||
- media_player.living_room
|
- media_player.living_room
|
||||||
@ -230,7 +230,7 @@ class DemoPicEntity extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
public ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
const hass = provideHass(this.$.demos);
|
const hass = provideHass(this.$.demos);
|
||||||
hass.addEntities(ENTITIES);
|
hass.addEntities(ENTITIES);
|
@ -38,7 +38,7 @@ class DemoLightEntity extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
public ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
const hass = provideHass(this.$.demos);
|
const hass = provideHass(this.$.demos);
|
||||||
hass.addEntities(ENTITIES);
|
hass.addEntities(ENTITIES);
|
@ -139,7 +139,7 @@ class DemoMap extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
public ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
const hass = provideHass(this.$.demos);
|
const hass = provideHass(this.$.demos);
|
||||||
hass.addEntities(ENTITIES);
|
hass.addEntities(ENTITIES);
|
@ -95,7 +95,7 @@ class DemoHuiMediaPlayerRows extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
public ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
const hass = provideHass(this.$.demos);
|
const hass = provideHass(this.$.demos);
|
||||||
hass.addEntities(ENTITIES);
|
hass.addEntities(ENTITIES);
|
@ -93,7 +93,7 @@ class DemoPicElements extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
public ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
const hass = provideHass(this.$.demos);
|
const hass = provideHass(this.$.demos);
|
||||||
hass.addEntities(ENTITIES);
|
hass.addEntities(ENTITIES);
|
@ -36,7 +36,7 @@ class DemoShoppingListEntity extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
public ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
const hass = provideHass(this.$.demos);
|
const hass = provideHass(this.$.demos);
|
||||||
|
|
@ -104,7 +104,7 @@ class DemoStack extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
public ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
const hass = provideHass(this.$.demos);
|
const hass = provideHass(this.$.demos);
|
||||||
hass.addEntities(ENTITIES);
|
hass.addEntities(ENTITIES);
|
@ -75,7 +75,7 @@ class DemoThermostatEntity extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
public ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
const hass = provideHass(this.$.demos);
|
const hass = provideHass(this.$.demos);
|
||||||
hass.addEntities(ENTITIES);
|
hass.addEntities(ENTITIES);
|
@ -8,16 +8,7 @@ import getEntity from "../data/entity";
|
|||||||
import provideHass from "../data/provide_hass";
|
import provideHass from "../data/provide_hass";
|
||||||
|
|
||||||
import "../components/demo-more-infos";
|
import "../components/demo-more-infos";
|
||||||
|
import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
|
||||||
/* 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;
|
|
||||||
|
|
||||||
const ENTITIES = [
|
const ENTITIES = [
|
||||||
getEntity("light", "bed_light", "on", {
|
getEntity("light", "bed_light", "on", {
|
||||||
@ -49,7 +40,7 @@ class DemoMoreInfoLight extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
public ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
const hass = provideHass(this);
|
const hass = provideHass(this);
|
||||||
hass.addEntities(ENTITIES);
|
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";
|
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);
|
const fixPath = (path) => path.substr(2, path.length - 5);
|
||||||
|
|
||||||
@ -118,6 +118,22 @@ class HaGallery extends PolymerElement {
|
|||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</paper-card>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -145,6 +161,10 @@ class HaGallery extends PolymerElement {
|
|||||||
type: Array,
|
type: Array,
|
||||||
computed: "_computeMoreInfos(_demos)",
|
computed: "_computeMoreInfos(_demos)",
|
||||||
},
|
},
|
||||||
|
_utilDemos: {
|
||||||
|
type: Array,
|
||||||
|
computed: "_computeUtil(_demos)",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,7 +198,7 @@ class HaGallery extends PolymerElement {
|
|||||||
while (root.lastChild) root.removeChild(root.lastChild);
|
while (root.lastChild) root.removeChild(root.lastChild);
|
||||||
|
|
||||||
if (demo) {
|
if (demo) {
|
||||||
DEMOS(`./${demo}.js`);
|
DEMOS(`./${demo}.ts`);
|
||||||
const el = document.createElement(demo);
|
const el = document.createElement(demo);
|
||||||
root.appendChild(el);
|
root.appendChild(el);
|
||||||
}
|
}
|
||||||
@ -199,6 +219,10 @@ class HaGallery extends PolymerElement {
|
|||||||
_computeMoreInfos(demos) {
|
_computeMoreInfos(demos) {
|
||||||
return demos.filter((demo) => demo.includes("more-info"));
|
return demos.filter((demo) => demo.includes("more-info"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_computeUtil(demos) {
|
||||||
|
return demos.filter((demo) => demo.includes("util"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("ha-gallery", HaGallery);
|
customElements.define("ha-gallery", HaGallery);
|
||||||
|
@ -2,6 +2,7 @@ import "@polymer/paper-button/paper-button";
|
|||||||
import "@polymer/paper-card/paper-card";
|
import "@polymer/paper-card/paper-card";
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
|
||||||
|
|
||||||
import "../../../src/resources/ha-style";
|
import "../../../src/resources/ha-style";
|
||||||
|
|
||||||
@ -15,10 +16,13 @@ class HassioAddonLogs extends PolymerElement {
|
|||||||
}
|
}
|
||||||
pre {
|
pre {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
${ANSI_HTML_STYLE}
|
||||||
<paper-card heading="Log">
|
<paper-card heading="Log">
|
||||||
<div class="card-content"><pre>[[log]]</pre></div>
|
<div class="card-content" id="content"></div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<paper-button on-click="refresh">Refresh</paper-button>
|
<paper-button on-click="refresh">Refresh</paper-button>
|
||||||
</div>
|
</div>
|
||||||
@ -33,7 +37,6 @@ class HassioAddonLogs extends PolymerElement {
|
|||||||
type: String,
|
type: String,
|
||||||
observer: "addonSlugChanged",
|
observer: "addonSlugChanged",
|
||||||
},
|
},
|
||||||
log: String,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,8 +54,11 @@ class HassioAddonLogs extends PolymerElement {
|
|||||||
refresh() {
|
refresh() {
|
||||||
this.hass
|
this.hass
|
||||||
.callApi("get", `hassio/addons/${this.addonSlug}/logs`)
|
.callApi("get", `hassio/addons/${this.addonSlug}/logs`)
|
||||||
.then((info) => {
|
.then((text) => {
|
||||||
this.log = info;
|
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 "@polymer/paper-card/paper-card";
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
|
||||||
|
|
||||||
class HassioSupervisorLog extends PolymerElement {
|
class HassioSupervisorLog extends PolymerElement {
|
||||||
static get template() {
|
static get template() {
|
||||||
@ -12,12 +13,18 @@ class HassioSupervisorLog extends PolymerElement {
|
|||||||
}
|
}
|
||||||
pre {
|
pre {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
.fg-green {
|
||||||
|
color: var(--primary-text-color) !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
${ANSI_HTML_STYLE}
|
||||||
<paper-card>
|
<paper-card>
|
||||||
<div class="card-content"><pre>[[log]]</pre></div>
|
<div class="card-content" id="content"></div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<paper-button on-click="refreshTapped">Refresh</paper-button>
|
<paper-button on-click="refresh">Refresh</paper-button>
|
||||||
</div>
|
</div>
|
||||||
</paper-card>
|
</paper-card>
|
||||||
`;
|
`;
|
||||||
@ -26,7 +33,6 @@ class HassioSupervisorLog extends PolymerElement {
|
|||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
hass: Object,
|
hass: Object,
|
||||||
log: String,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,16 +43,20 @@ class HassioSupervisorLog extends PolymerElement {
|
|||||||
|
|
||||||
loadData() {
|
loadData() {
|
||||||
this.hass.callApi("get", "hassio/supervisor/logs").then(
|
this.hass.callApi("get", "hassio/supervisor/logs").then(
|
||||||
(info) => {
|
(text) => {
|
||||||
this.log = info;
|
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();
|
this.loadData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
package.json
21
package.json
@ -64,10 +64,10 @@
|
|||||||
"@polymer/paper-toggle-button": "^3.0.1",
|
"@polymer/paper-toggle-button": "^3.0.1",
|
||||||
"@polymer/paper-tooltip": "^3.0.1",
|
"@polymer/paper-tooltip": "^3.0.1",
|
||||||
"@polymer/polymer": "^3.0.5",
|
"@polymer/polymer": "^3.0.5",
|
||||||
"@vaadin/vaadin-combo-box": "^4.2.0-beta2",
|
"@vaadin/vaadin-combo-box": "^4.2.0",
|
||||||
"@vaadin/vaadin-date-picker": "^3.3.0",
|
"@vaadin/vaadin-date-picker": "^3.3.1",
|
||||||
"@webcomponents/shadycss": "^1.5.2",
|
"@webcomponents/shadycss": "^1.6.0",
|
||||||
"@webcomponents/webcomponentsjs": "^2.1.3",
|
"@webcomponents/webcomponentsjs": "^2.2.0",
|
||||||
"chart.js": "~2.7.2",
|
"chart.js": "~2.7.2",
|
||||||
"chartjs-chart-timeline": "^0.2.1",
|
"chartjs-chart-timeline": "^0.2.1",
|
||||||
"es6-object-assign": "^1.1.0",
|
"es6-object-assign": "^1.1.0",
|
||||||
@ -87,6 +87,7 @@
|
|||||||
"react-big-calendar": "^0.19.2",
|
"react-big-calendar": "^0.19.2",
|
||||||
"regenerator-runtime": "^0.12.1",
|
"regenerator-runtime": "^0.12.1",
|
||||||
"round-slider": "^1.3.2",
|
"round-slider": "^1.3.2",
|
||||||
|
"superstruct": "^0.6.0",
|
||||||
"unfetch": "^4.0.1",
|
"unfetch": "^4.0.1",
|
||||||
"web-animations-js": "^2.3.1",
|
"web-animations-js": "^2.3.1",
|
||||||
"xss": "^1.0.3"
|
"xss": "^1.0.3"
|
||||||
@ -152,15 +153,11 @@
|
|||||||
"workbox-webpack-plugin": "^3.5.0"
|
"workbox-webpack-plugin": "^3.5.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"inherits": "2.0.3",
|
|
||||||
"samsam": "1.1.3",
|
|
||||||
"supports-color": "3.1.2",
|
|
||||||
"type-detect": "1.0.0",
|
|
||||||
"@polymer/polymer": "3.1.0",
|
"@polymer/polymer": "3.1.0",
|
||||||
"@webcomponents/webcomponentsjs": "2.1.3",
|
"@webcomponents/webcomponentsjs": "2.2.1",
|
||||||
"@webcomponents/shadycss": "^1.5.2",
|
"@webcomponents/shadycss": "^1.6.0",
|
||||||
"@vaadin/vaadin-overlay": "3.2.0-alpha3",
|
"@vaadin/vaadin-overlay": "3.2.2",
|
||||||
"@vaadin/vaadin-lumo-styles": "1.2.0",
|
"@vaadin/vaadin-lumo-styles": "1.3.0",
|
||||||
"fecha": "https://github.com/taylorhakes/fecha/archive/5e8fe08d982647fdb19fb403459838b02647813c.tar.gz",
|
"fecha": "https://github.com/taylorhakes/fecha/archive/5e8fe08d982647fdb19fb403459838b02647813c.tar.gz",
|
||||||
"lit-html": "0.12.0",
|
"lit-html": "0.12.0",
|
||||||
"@polymer/lit-element": "0.6.2"
|
"@polymer/lit-element": "0.6.2"
|
||||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="home-assistant-frontend",
|
name="home-assistant-frontend",
|
||||||
version="20181121.1",
|
version="20181205.0",
|
||||||
description="The Home Assistant frontend",
|
description="The Home Assistant frontend",
|
||||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||||
author="The Home Assistant Authors",
|
author="The Home Assistant Authors",
|
||||||
|
@ -103,6 +103,7 @@ class HaPlantCard extends EventsMixin(PolymerElement) {
|
|||||||
return {
|
return {
|
||||||
hass: Object,
|
hass: Object,
|
||||||
stateObj: Object,
|
stateObj: Object,
|
||||||
|
config: Object,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +119,7 @@ class HaPlantCard extends EventsMixin(PolymerElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
computeTitle(stateObj) {
|
computeTitle(stateObj) {
|
||||||
return computeStateName(stateObj);
|
return this.config.name || computeStateName(stateObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
computeAttributes(data) {
|
computeAttributes(data) {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
|
||||||
|
import computeStateName from "../common/entity/compute_state_name";
|
||||||
|
|
||||||
import "../components/ha-card";
|
import "../components/ha-card";
|
||||||
import "../components/ha-icon";
|
import "../components/ha-icon";
|
||||||
|
|
||||||
@ -106,7 +108,7 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
|||||||
<ha-card>
|
<ha-card>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
[[computeState(stateObj.state, localize)]]
|
[[computeState(stateObj.state, localize)]]
|
||||||
<div class="name">[[stateObj.attributes.friendly_name]]</div>
|
<div class="name">[[computeName(stateObj)]]</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="now">
|
<div class="now">
|
||||||
@ -271,6 +273,10 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
|||||||
return localize(`state.weather.${state}`) || state;
|
return localize(`state.weather.${state}`) || state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
computeName(stateObj) {
|
||||||
|
return this.config.name || computeStateName(stateObj);
|
||||||
|
}
|
||||||
|
|
||||||
showWeatherIcon(condition) {
|
showWeatherIcon(condition) {
|
||||||
return condition in this.weatherIcons;
|
return condition in this.weatherIcons;
|
||||||
}
|
}
|
||||||
|
@ -10,31 +10,43 @@ const langKey = ["second", "minute", "hour", "day"];
|
|||||||
|
|
||||||
export default function relativeTime(
|
export default function relativeTime(
|
||||||
dateObj: Date,
|
dateObj: Date,
|
||||||
localize: LocalizeFunc
|
localize: LocalizeFunc,
|
||||||
|
options: {
|
||||||
|
compareTime?: Date;
|
||||||
|
includeTense?: boolean;
|
||||||
|
} = {}
|
||||||
): string {
|
): 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";
|
const tense = delta >= 0 ? "past" : "future";
|
||||||
delta = Math.abs(delta);
|
delta = Math.abs(delta);
|
||||||
|
|
||||||
|
let timeDesc;
|
||||||
|
|
||||||
for (let i = 0; i < tests.length; i++) {
|
for (let i = 0; i < tests.length; i++) {
|
||||||
if (delta < tests[i]) {
|
if (delta < tests[i]) {
|
||||||
delta = Math.floor(delta);
|
delta = Math.floor(delta);
|
||||||
const timeDesc = localize(
|
timeDesc = localize(
|
||||||
`ui.components.relative_time.duration.${langKey[i]}`,
|
`ui.components.relative_time.duration.${langKey[i]}`,
|
||||||
"count",
|
"count",
|
||||||
delta
|
delta
|
||||||
);
|
);
|
||||||
return localize(`ui.components.relative_time.${tense}`, "time", timeDesc);
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
delta /= tests[i];
|
delta /= tests[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (timeDesc === undefined) {
|
||||||
delta = Math.floor(delta);
|
delta = Math.floor(delta);
|
||||||
const time = localize(
|
timeDesc = localize(
|
||||||
"ui.components.relative_time.duration.week",
|
"ui.components.relative_time.duration.week",
|
||||||
"count",
|
"count",
|
||||||
delta
|
delta
|
||||||
);
|
);
|
||||||
return localize(`ui.components.relative_time.${tense}`, "time", time);
|
}
|
||||||
|
|
||||||
|
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
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// 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.
|
* Dispatches a custom event with an optional detail value.
|
||||||
*
|
*
|
||||||
@ -35,23 +46,33 @@
|
|||||||
* @param {*=} detail Detail value containing event-specific
|
* @param {*=} detail Detail value containing event-specific
|
||||||
* payload.
|
* payload.
|
||||||
* @param {{ bubbles: (boolean|undefined),
|
* @param {{ bubbles: (boolean|undefined),
|
||||||
cancelable: (boolean|undefined),
|
* cancelable: (boolean|undefined),
|
||||||
composed: (boolean|undefined) }=}
|
* composed: (boolean|undefined) }=}
|
||||||
* options Object specifying options. These may include:
|
* options Object specifying options. These may include:
|
||||||
* `bubbles` (boolean, defaults to `true`),
|
* `bubbles` (boolean, defaults to `true`),
|
||||||
* `cancelable` (boolean, defaults to false), and
|
* `cancelable` (boolean, defaults to false), and
|
||||||
* `node` on which to fire the event (HTMLElement, defaults to `this`).
|
* `node` on which to fire the event (HTMLElement, defaults to `this`).
|
||||||
* @return {Event} The new event that was fired.
|
* @return {Event} The new event that was fired.
|
||||||
*/
|
*/
|
||||||
export const fireEvent = (node, type, detail, options) => {
|
export const fireEvent = <HassEvent extends ValidHassDomEvent>(
|
||||||
|
node: HTMLElement,
|
||||||
|
type: HassEvent,
|
||||||
|
detail?: HASSDomEvents[HassEvent],
|
||||||
|
options?: {
|
||||||
|
bubbles?: boolean;
|
||||||
|
cancelable?: boolean;
|
||||||
|
composed?: boolean;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
// @ts-ignore
|
||||||
detail = detail === null || detail === undefined ? {} : detail;
|
detail = detail === null || detail === undefined ? {} : detail;
|
||||||
const event = new Event(type, {
|
const event = new Event(type, {
|
||||||
bubbles: options.bubbles === undefined ? true : options.bubbles,
|
bubbles: options.bubbles === undefined ? true : options.bubbles,
|
||||||
cancelable: Boolean(options.cancelable),
|
cancelable: Boolean(options.cancelable),
|
||||||
composed: options.composed === undefined ? true : options.composed,
|
composed: options.composed === undefined ? true : options.composed,
|
||||||
});
|
});
|
||||||
event.detail = detail;
|
(event as any).detail = detail;
|
||||||
node.dispatchEvent(event);
|
node.dispatchEvent(event);
|
||||||
return event;
|
return event;
|
||||||
};
|
};
|
@ -2,6 +2,7 @@ import { HassEntity } from "home-assistant-js-websocket";
|
|||||||
import canToggleDomain from "./can_toggle_domain";
|
import canToggleDomain from "./can_toggle_domain";
|
||||||
import computeStateDomain from "./compute_state_domain";
|
import computeStateDomain from "./compute_state_domain";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
|
import { supportsFeature } from "./supports-feature";
|
||||||
|
|
||||||
export default function canToggleState(
|
export default function canToggleState(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -12,8 +13,7 @@ export default function canToggleState(
|
|||||||
return stateObj.state === "on" || stateObj.state === "off";
|
return stateObj.state === "on" || stateObj.state === "off";
|
||||||
}
|
}
|
||||||
if (domain === "climate") {
|
if (domain === "climate") {
|
||||||
// tslint:disable-next-line
|
return supportsFeature(stateObj, 4096);
|
||||||
return (stateObj.attributes.supported_features! & 4096) !== 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return canToggleDomain(hass, domain);
|
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 { DEFAULT_VIEW_ENTITY_ID } from "../const";
|
||||||
|
import { GroupEntity } from "../../types";
|
||||||
|
|
||||||
// Return an ordered array of available views
|
// Return an ordered array of available views
|
||||||
export default function extractViews(entities: HassEntities): HassEntity[] {
|
export default function extractViews(entities: HassEntities): GroupEntity[] {
|
||||||
const views: HassEntity[] = [];
|
const views: GroupEntity[] = [];
|
||||||
|
|
||||||
Object.keys(entities).forEach((entityId) => {
|
Object.keys(entities).forEach((entityId) => {
|
||||||
const entity = entities[entityId];
|
const entity = entities[entityId];
|
||||||
if (entity.attributes.view) {
|
if (entity.attributes.view) {
|
||||||
views.push(entity);
|
views.push(entity as GroupEntity);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { supportsFeature } from "./supports-feature";
|
||||||
|
|
||||||
// Expects classNames to be an object mapping feature-bit -> className
|
// Expects classNames to be an object mapping feature-bit -> className
|
||||||
export default function featureClassNames(
|
export default function featureClassNames(
|
||||||
@ -9,12 +10,9 @@ export default function featureClassNames(
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const features = stateObj.attributes.supported_features;
|
|
||||||
|
|
||||||
return Object.keys(classNames)
|
return Object.keys(classNames)
|
||||||
.map((feature) =>
|
.map((feature) =>
|
||||||
// tslint:disable-next-line
|
supportsFeature(stateObj, Number(feature)) ? classNames[feature] : ""
|
||||||
(features & Number(feature)) !== 0 ? classNames[feature] : ""
|
|
||||||
)
|
)
|
||||||
.filter((attr) => attr !== "")
|
.filter((attr) => attr !== "")
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
@ -8,7 +8,7 @@ import { GroupEntity } from "../../types";
|
|||||||
export default function getViewEntities(
|
export default function getViewEntities(
|
||||||
entities: HassEntities,
|
entities: HassEntities,
|
||||||
view: GroupEntity
|
view: GroupEntity
|
||||||
) {
|
): HassEntities {
|
||||||
const viewEntities = {};
|
const viewEntities = {};
|
||||||
|
|
||||||
view.attributes.entity_id.forEach((entityId) => {
|
view.attributes.entity_id.forEach((entityId) => {
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import computeDomain from "./compute_domain";
|
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
|
// Split a collection into a list of groups and a 'rest' list of ungrouped
|
||||||
// entities.
|
// entities.
|
||||||
// Returns { groups: [], ungrouped: {} }
|
// Returns { groups: [], ungrouped: {} }
|
||||||
export default function splitByGroups(entities: HassEntities) {
|
export default function splitByGroups(entities: HassEntities) {
|
||||||
const groups: HassEntity[] = [];
|
const groups: GroupEntity[] = [];
|
||||||
const ungrouped: HassEntities = {};
|
const ungrouped: HassEntities = {};
|
||||||
|
|
||||||
Object.keys(entities).forEach((entityId) => {
|
Object.keys(entities).forEach((entityId) => {
|
||||||
const entity = entities[entityId];
|
const entity = entities[entityId];
|
||||||
|
|
||||||
if (computeDomain(entityId) === "group") {
|
if (computeDomain(entityId) === "group") {
|
||||||
groups.push(entity);
|
groups.push(entity as GroupEntity);
|
||||||
} else {
|
} else {
|
||||||
ungrouped[entityId] = entity;
|
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 "../ha-relative-time";
|
||||||
import "./state-badge";
|
import "./state-badge";
|
||||||
import computeStateName from "../../common/entity/compute_state_name";
|
import computeStateName from "../../common/entity/compute_state_name";
|
||||||
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
|
|
||||||
class StateInfo extends PolymerElement {
|
class StateInfo extends PolymerElement {
|
||||||
static get template() {
|
static get template() {
|
||||||
@ -25,10 +26,20 @@ class StateInfo extends PolymerElement {
|
|||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host([rtl]) state-badge {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
margin-left: 56px;
|
margin-left: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host([rtl]) .info {
|
||||||
|
margin-right: 56px;
|
||||||
|
margin-left: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
@apply --paper-font-common-nowrap;
|
@apply --paper-font-common-nowrap;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
@ -87,12 +98,21 @@ class StateInfo extends PolymerElement {
|
|||||||
hass: Object,
|
hass: Object,
|
||||||
stateObj: Object,
|
stateObj: Object,
|
||||||
inDialog: Boolean,
|
inDialog: Boolean,
|
||||||
|
rtl: {
|
||||||
|
type: Boolean,
|
||||||
|
reflectToAttribute: true,
|
||||||
|
computed: "computeRTL(hass)",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
computeStateName(stateObj) {
|
computeStateName(stateObj) {
|
||||||
return computeStateName(stateObj);
|
return computeStateName(stateObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
computeRTL(hass) {
|
||||||
|
return computeRTL(hass);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("state-info", StateInfo);
|
customElements.define("state-info", StateInfo);
|
||||||
|
@ -12,6 +12,7 @@ class HaCard extends PolymerElement {
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: all 0.3s ease-out;
|
transition: all 0.3s ease-out;
|
||||||
background-color: var(--paper-card-background-color, white);
|
background-color: var(--paper-card-background-color, white);
|
||||||
|
color: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
@apply --paper-font-headline;
|
@apply --paper-font-headline;
|
||||||
|
@ -123,7 +123,7 @@ class HaCards extends PolymerElement {
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<template is="dom-if" if="[[cards.badges]]">
|
<template is="dom-if" if="[[cards.badges.length]]">
|
||||||
<div class="badges">
|
<div class="badges">
|
||||||
<template is="dom-if" if="[[cards.demo]]">
|
<template is="dom-if" if="[[cards.demo]]">
|
||||||
<ha-demo-badge></ha-demo-badge>
|
<ha-demo-badge></ha-demo-badge>
|
||||||
@ -159,7 +159,6 @@ class HaCards extends PolymerElement {
|
|||||||
},
|
},
|
||||||
|
|
||||||
states: Object,
|
states: Object,
|
||||||
panelVisible: Boolean,
|
|
||||||
|
|
||||||
viewVisible: {
|
viewVisible: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -173,19 +172,11 @@ class HaCards extends PolymerElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get observers() {
|
static get observers() {
|
||||||
return [
|
return ["updateCards(columns, states, viewVisible, orderedGroupEntities)"];
|
||||||
"updateCards(columns, states, panelVisible, viewVisible, orderedGroupEntities)",
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCards(
|
updateCards(columns, states, viewVisible, orderedGroupEntities) {
|
||||||
columns,
|
if (!viewVisible) {
|
||||||
states,
|
|
||||||
panelVisible,
|
|
||||||
viewVisible,
|
|
||||||
orderedGroupEntities
|
|
||||||
) {
|
|
||||||
if (!panelVisible || !viewVisible) {
|
|
||||||
if (this.$.main.parentNode) {
|
if (this.$.main.parentNode) {
|
||||||
this.$.main._parentNode = this.$.main.parentNode;
|
this.$.main._parentNode = this.$.main.parentNode;
|
||||||
this.$.main.parentNode.removeChild(this.$.main);
|
this.$.main.parentNode.removeChild(this.$.main);
|
||||||
@ -200,7 +191,7 @@ class HaCards extends PolymerElement {
|
|||||||
timeOut.after(10),
|
timeOut.after(10),
|
||||||
() => {
|
() => {
|
||||||
// Things might have changed since it got scheduled.
|
// Things might have changed since it got scheduled.
|
||||||
if (this.panelVisible && this.viewVisible) {
|
if (this.viewVisible) {
|
||||||
this.cards = this.computeCards(columns, states, orderedGroupEntities);
|
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",
|
type: "shopping_list/items",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const saveEdit = (
|
export const updateItem = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
itemId: number,
|
itemId: number,
|
||||||
name: string
|
item: {
|
||||||
|
name?: string;
|
||||||
|
complete?: boolean;
|
||||||
|
}
|
||||||
): Promise<ShoppingListItem> =>
|
): Promise<ShoppingListItem> =>
|
||||||
hass.callApi("POST", "shopping_list/item/" + itemId, {
|
hass.callWS({
|
||||||
name,
|
type: "shopping_list/items/update",
|
||||||
});
|
item_id: itemId,
|
||||||
|
...item,
|
||||||
export const completeItem = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
itemId: number,
|
|
||||||
complete: boolean
|
|
||||||
): Promise<void> =>
|
|
||||||
hass.callApi("POST", "shopping_list/item/" + itemId, {
|
|
||||||
complete,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clearItems = (hass: HomeAssistant): Promise<void> =>
|
export const clearItems = (hass: HomeAssistant): Promise<void> =>
|
||||||
hass.callApi("POST", "shopping_list/clear_completed");
|
hass.callWS({
|
||||||
|
type: "shopping_list/items/clear",
|
||||||
|
});
|
||||||
|
|
||||||
export const addItem = (
|
export const addItem = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
name: string
|
name: string
|
||||||
): Promise<ShoppingListItem> =>
|
): Promise<ShoppingListItem> =>
|
||||||
hass.callApi("POST", "shopping_list/item", {
|
hass.callWS({
|
||||||
|
type: "shopping_list/items/add",
|
||||||
name,
|
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 attributeClassNames from "../../../common/entity/attribute_class_names";
|
||||||
import featureClassNames from "../../../common/entity/feature_class_names";
|
import featureClassNames from "../../../common/entity/feature_class_names";
|
||||||
|
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||||
|
|
||||||
import EventsMixin from "../../../mixins/events-mixin";
|
import EventsMixin from "../../../mixins/events-mixin";
|
||||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||||
@ -385,45 +386,45 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
|||||||
|
|
||||||
supportsTemperature(stateObj) {
|
supportsTemperature(stateObj) {
|
||||||
return (
|
return (
|
||||||
(stateObj.attributes.supported_features & 1) !== 0 &&
|
supportsFeature(stateObj, 1) &&
|
||||||
typeof stateObj.attributes.temperature === "number"
|
typeof stateObj.attributes.temperature === "number"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsTemperatureRange(stateObj) {
|
supportsTemperatureRange(stateObj) {
|
||||||
return (
|
return (
|
||||||
(stateObj.attributes.supported_features & 6) !== 0 &&
|
supportsFeature(stateObj, 6) &&
|
||||||
(typeof stateObj.attributes.target_temp_low === "number" ||
|
(typeof stateObj.attributes.target_temp_low === "number" ||
|
||||||
typeof stateObj.attributes.target_temp_high === "number")
|
typeof stateObj.attributes.target_temp_high === "number")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsHumidity(stateObj) {
|
supportsHumidity(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 8) !== 0;
|
return supportsFeature(stateObj, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsFanMode(stateObj) {
|
supportsFanMode(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 64) !== 0;
|
return supportsFeature(stateObj, 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsOperationMode(stateObj) {
|
supportsOperationMode(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 128) !== 0;
|
return supportsFeature(stateObj, 128);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsSwingMode(stateObj) {
|
supportsSwingMode(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 512) !== 0;
|
return supportsFeature(stateObj, 512);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsAwayMode(stateObj) {
|
supportsAwayMode(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 1024) !== 0;
|
return supportsFeature(stateObj, 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsAuxHeat(stateObj) {
|
supportsAuxHeat(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 2048) !== 0;
|
return supportsFeature(stateObj, 2048);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsOn(stateObj) {
|
supportsOn(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 4096) !== 0;
|
return supportsFeature(stateObj, 4096);
|
||||||
}
|
}
|
||||||
|
|
||||||
computeClassNames(stateObj) {
|
computeClassNames(stateObj) {
|
||||||
|
@ -8,6 +8,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
|
||||||
import "../../../components/ha-attributes";
|
import "../../../components/ha-attributes";
|
||||||
|
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||||
|
|
||||||
class MoreInfoVacuum extends PolymerElement {
|
class MoreInfoVacuum extends PolymerElement {
|
||||||
static get template() {
|
static get template() {
|
||||||
@ -158,57 +159,53 @@ class MoreInfoVacuum extends PolymerElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable no-bitwise */
|
|
||||||
|
|
||||||
supportsPause(stateObj) {
|
supportsPause(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 4) !== 0;
|
return supportsFeature(stateObj, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsStop(stateObj) {
|
supportsStop(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 8) !== 0;
|
return supportsFeature(stateObj, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsReturnHome(stateObj) {
|
supportsReturnHome(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 16) !== 0;
|
return supportsFeature(stateObj, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsFanSpeed(stateObj) {
|
supportsFanSpeed(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 32) !== 0;
|
return supportsFeature(stateObj, 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsBattery(stateObj) {
|
supportsBattery(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 64) !== 0;
|
return supportsFeature(stateObj, 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsStatus(stateObj) {
|
supportsStatus(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 128) !== 0;
|
return supportsFeature(stateObj, 128);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsLocate(stateObj) {
|
supportsLocate(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 512) !== 0;
|
return supportsFeature(stateObj, 512);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsCleanSpot(stateObj) {
|
supportsCleanSpot(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 1024) !== 0;
|
return supportsFeature(stateObj, 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsStart(stateObj) {
|
supportsStart(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 8192) !== 0;
|
return supportsFeature(stateObj, 8192);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsCommandBar(stateObj) {
|
supportsCommandBar(stateObj) {
|
||||||
return (
|
return (
|
||||||
((stateObj.attributes.supported_features & 4) !== 0) |
|
supportsFeature(stateObj, 4) |
|
||||||
((stateObj.attributes.supported_features & 8) !== 0) |
|
supportsFeature(stateObj, 8) |
|
||||||
((stateObj.attributes.supported_features & 16) !== 0) |
|
supportsFeature(stateObj, 16) |
|
||||||
((stateObj.attributes.supported_features & 512) !== 0) |
|
supportsFeature(stateObj, 512) |
|
||||||
((stateObj.attributes.supported_features & 1024) !== 0) |
|
supportsFeature(stateObj, 1024) |
|
||||||
((stateObj.attributes.supported_features & 8192) !== 0)
|
supportsFeature(stateObj, 8192)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-enable no-bitwise */
|
|
||||||
|
|
||||||
fanSpeedChanged(fanSpeedIndex) {
|
fanSpeedChanged(fanSpeedIndex) {
|
||||||
var fanSpeedInput;
|
var fanSpeedInput;
|
||||||
// Selected Option will transition to '' before transitioning to new value
|
// 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 "../../../components/ha-paper-slider";
|
||||||
|
|
||||||
import featureClassNames from "../../../common/entity/feature_class_names";
|
import featureClassNames from "../../../common/entity/feature_class_names";
|
||||||
|
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||||
|
|
||||||
import EventsMixin from "../../../mixins/events-mixin";
|
import EventsMixin from "../../../mixins/events-mixin";
|
||||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||||
@ -198,17 +199,17 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
|||||||
|
|
||||||
supportsTemperature(stateObj) {
|
supportsTemperature(stateObj) {
|
||||||
return (
|
return (
|
||||||
(stateObj.attributes.supported_features & 1) !== 0 &&
|
supportsFeature(stateObj, 1) &&
|
||||||
typeof stateObj.attributes.temperature === "number"
|
typeof stateObj.attributes.temperature === "number"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsOperationMode(stateObj) {
|
supportsOperationMode(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 2) !== 0;
|
return supportsFeature(stateObj, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsAwayMode(stateObj) {
|
supportsAwayMode(stateObj) {
|
||||||
return (stateObj.attributes.supported_features & 4) !== 0;
|
return supportsFeature(stateObj, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
computeClassNames(stateObj) {
|
computeClassNames(stateObj) {
|
||||||
|
@ -16,6 +16,7 @@ import computeStateDomain from "../../common/entity/compute_state_domain";
|
|||||||
import isComponentLoaded from "../../common/config/is_component_loaded";
|
import isComponentLoaded from "../../common/config/is_component_loaded";
|
||||||
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
|
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
|
||||||
import EventsMixin from "../../mixins/events-mixin";
|
import EventsMixin from "../../mixins/events-mixin";
|
||||||
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
|
|
||||||
const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"];
|
const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"];
|
||||||
/*
|
/*
|
||||||
@ -58,6 +59,11 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
|||||||
:host([domain="camera"]) paper-dialog-scrollable {
|
:host([domain="camera"]) paper-dialog-scrollable {
|
||||||
margin: 0 -24px -21px;
|
margin: 0 -24px -21px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host([rtl]) app-toolbar {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<app-toolbar>
|
<app-toolbar>
|
||||||
@ -147,6 +153,11 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
|||||||
hoursToShow: 24,
|
hoursToShow: 24,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
rtl: {
|
||||||
|
type: Boolean,
|
||||||
|
reflectToAttribute: true,
|
||||||
|
computed: "_computeRTL(hass)",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,5 +201,9 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
|||||||
_gotoSettings() {
|
_gotoSettings() {
|
||||||
this.fire("more-info-page", { page: "settings" });
|
this.fire("more-info-page", { page: "settings" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_computeRTL(hass) {
|
||||||
|
return computeRTL(hass);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
customElements.define("more-info-controls", MoreInfoControls);
|
customElements.define("more-info-controls", MoreInfoControls);
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
Home Assistant
|
Home Assistant
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ha-onboarding>Initializing…</ha-onboarding>
|
<ha-onboarding>Initializing</ha-onboarding>
|
||||||
</div>
|
</div>
|
||||||
<% if (!latestBuild) { %>
|
<% if (!latestBuild) { %>
|
||||||
<script src="/static/custom-elements-es5-adapter.js"></script>
|
<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 { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
import { afterNextRender } from "@polymer/polymer/lib/utils/render-status";
|
import { afterNextRender } from "@polymer/polymer/lib/utils/render-status";
|
||||||
|
import { html as litHtml, LitElement } from "@polymer/lit-element";
|
||||||
|
|
||||||
import "../home-assistant-main";
|
import "../home-assistant-main";
|
||||||
import "../ha-init-page";
|
import "../ha-init-page";
|
||||||
@ -16,11 +17,13 @@ import TranslationsMixin from "./translations-mixin";
|
|||||||
import ThemesMixin from "./themes-mixin";
|
import ThemesMixin from "./themes-mixin";
|
||||||
import MoreInfoMixin from "./more-info-mixin";
|
import MoreInfoMixin from "./more-info-mixin";
|
||||||
import SidebarMixin from "./sidebar-mixin";
|
import SidebarMixin from "./sidebar-mixin";
|
||||||
import DialogManagerMixin from "./dialog-manager-mixin";
|
import { dialogManagerMixin } from "./dialog-manager-mixin";
|
||||||
import ConnectionMixin from "./connection-mixin";
|
import ConnectionMixin from "./connection-mixin";
|
||||||
import NotificationMixin from "./notification-mixin";
|
import NotificationMixin from "./notification-mixin";
|
||||||
import DisconnectToastMixin from "./disconnect-toast-mixin";
|
import DisconnectToastMixin from "./disconnect-toast-mixin";
|
||||||
|
|
||||||
|
LitElement.prototype.html = litHtml;
|
||||||
|
|
||||||
const ext = (baseClass, mixins) =>
|
const ext = (baseClass, mixins) =>
|
||||||
mixins.reduceRight((base, mixin) => mixin(base), baseClass);
|
mixins.reduceRight((base, mixin) => mixin(base), baseClass);
|
||||||
|
|
||||||
@ -33,7 +36,7 @@ class HomeAssistant extends ext(PolymerElement, [
|
|||||||
DisconnectToastMixin,
|
DisconnectToastMixin,
|
||||||
ConnectionMixin,
|
ConnectionMixin,
|
||||||
NotificationMixin,
|
NotificationMixin,
|
||||||
DialogManagerMixin,
|
dialogManagerMixin,
|
||||||
HassBaseMixin,
|
HassBaseMixin,
|
||||||
]) {
|
]) {
|
||||||
static get template() {
|
static get template() {
|
||||||
@ -91,7 +94,7 @@ class HomeAssistant extends ext(PolymerElement, [
|
|||||||
}
|
}
|
||||||
|
|
||||||
computePanelUrl(routeData) {
|
computePanelUrl(routeData) {
|
||||||
return (routeData && routeData.panel) || "states";
|
return (routeData && routeData.panel) || "lovelace";
|
||||||
}
|
}
|
||||||
|
|
||||||
panelUrlChanged(newPanelUrl) {
|
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-layout/app-drawer/app-drawer";
|
||||||
import "@polymer/app-route/app-route";
|
import "@polymer/app-route/app-route";
|
||||||
import "@polymer/iron-media-query/iron-media-query";
|
import "@polymer/iron-media-query/iron-media-query";
|
||||||
import "@polymer/iron-pages/iron-pages";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
|
||||||
import "../util/ha-url-sync";
|
import "../util/ha-url-sync";
|
||||||
|
|
||||||
import "./partial-cards";
|
|
||||||
import "./partial-panel-resolver";
|
import "./partial-panel-resolver";
|
||||||
import EventsMixin from "../mixins/events-mixin";
|
import EventsMixin from "../mixins/events-mixin";
|
||||||
import NavigateMixin from "../mixins/navigate-mixin";
|
import NavigateMixin from "../mixins/navigate-mixin";
|
||||||
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
|
|
||||||
import(/* webpackChunkName: "ha-sidebar" */ "../components/ha-sidebar");
|
import(/* webpackChunkName: "ha-sidebar" */ "../components/ha-sidebar");
|
||||||
import(/* webpackChunkName: "voice-command-dialog" */ "../dialogs/ha-voice-command-dialog");
|
import(/* webpackChunkName: "voice-command-dialog" */ "../dialogs/ha-voice-command-dialog");
|
||||||
@ -30,21 +29,16 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
|||||||
:host([rtl]) {
|
:host([rtl]) {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
}
|
}
|
||||||
iron-pages,
|
partial-panel-resolver,
|
||||||
ha-sidebar {
|
ha-sidebar {
|
||||||
/* allow a light tap highlight on the actual interface elements */
|
/* allow a light tap highlight on the actual interface elements */
|
||||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
iron-pages {
|
partial-panel-resolver {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<ha-url-sync hass="[[hass]]"></ha-url-sync>
|
<ha-url-sync hass="[[hass]]"></ha-url-sync>
|
||||||
<app-route
|
|
||||||
route="{{route}}"
|
|
||||||
pattern="/states"
|
|
||||||
tail="{{statesRouteTail}}"
|
|
||||||
></app-route>
|
|
||||||
<ha-voice-command-dialog
|
<ha-voice-command-dialog
|
||||||
hass="[[hass]]"
|
hass="[[hass]]"
|
||||||
id="voiceDialog"
|
id="voiceDialog"
|
||||||
@ -72,29 +66,12 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
|||||||
></ha-sidebar>
|
></ha-sidebar>
|
||||||
</app-drawer>
|
</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
|
<partial-panel-resolver
|
||||||
id="panel-resolver"
|
|
||||||
narrow="[[narrow]]"
|
narrow="[[narrow]]"
|
||||||
hass="[[hass]]"
|
hass="[[hass]]"
|
||||||
route="[[route]]"
|
route="[[route]]"
|
||||||
show-menu="[[dockedSidebar]]"
|
show-menu="[[dockedSidebar]]"
|
||||||
></partial-panel-resolver>
|
></partial-panel-resolver>
|
||||||
</iron-pages>
|
|
||||||
</app-drawer-layout>
|
</app-drawer-layout>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -107,7 +84,6 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
|||||||
type: Object,
|
type: Object,
|
||||||
observer: "_routeChanged",
|
observer: "_routeChanged",
|
||||||
},
|
},
|
||||||
statesRouteTail: Object,
|
|
||||||
dockedSidebar: {
|
dockedSidebar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
computed: "computeDockedSidebar(hass)",
|
computed: "computeDockedSidebar(hass)",
|
||||||
@ -115,14 +91,14 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
|||||||
rtl: {
|
rtl: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
reflectToAttribute: true,
|
reflectToAttribute: true,
|
||||||
computed: "computeRTL(hass)",
|
computed: "_computeRTL(hass)",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
this._defaultPage = localStorage.defaultPage || "states";
|
this._defaultPage = localStorage.defaultPage || "lovelace";
|
||||||
this.addEventListener("hass-open-menu", () => this.handleOpenMenu());
|
this.addEventListener("hass-open-menu", () => this.handleOpenMenu());
|
||||||
this.addEventListener("hass-close-menu", () => this.handleCloseMenu());
|
this.addEventListener("hass-close-menu", () => this.handleCloseMenu());
|
||||||
this.addEventListener("hass-start-voice", (ev) =>
|
this.addEventListener("hass-start-voice", (ev) =>
|
||||||
@ -159,7 +135,7 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
|||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
if (document.location.pathname === "/") {
|
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;
|
return NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
computeRTL(hass) {
|
_computeRTL(hass) {
|
||||||
var lang = hass.selectedLanguage || hass.language;
|
return computeRTL(hass);
|
||||||
if (hass.translationMetadata.translations[lang]) {
|
|
||||||
return hass.translationMetadata.translations[lang].isRTL || false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +54,10 @@ function ensureLoaded(panel) {
|
|||||||
imported = import(/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace");
|
imported = import(/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "states":
|
||||||
|
imported = import(/* webpackChunkName: "panel-states" */ "../panels/states/ha-panel-states");
|
||||||
|
break;
|
||||||
|
|
||||||
case "history":
|
case "history":
|
||||||
imported = import(/* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history");
|
imported = import(/* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history");
|
||||||
break;
|
break;
|
||||||
|
@ -24,6 +24,10 @@ export class CloudAlexaPref extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
if (!this.cloudStatus) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
const enabled = this.cloudStatus!.prefs.alexa_enabled;
|
const enabled = this.cloudStatus!.prefs.alexa_enabled;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
@ -25,7 +25,11 @@ export class CloudGooglePref extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
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`
|
return html`
|
||||||
${this.renderStyle()}
|
${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 "../../../resources/ha-style";
|
||||||
|
|
||||||
import "../ha-config-section";
|
import "../ha-config-section";
|
||||||
|
import "./cloud-webhooks";
|
||||||
|
|
||||||
import formatDateTime from "../../../common/datetime/format_date_time";
|
import formatDateTime from "../../../common/datetime/format_date_time";
|
||||||
import EventsMixin from "../../../mixins/events-mixin";
|
import EventsMixin from "../../../mixins/events-mixin";
|
||||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
|
||||||
import { fetchSubscriptionInfo } from "./data";
|
import { fetchSubscriptionInfo } from "./data";
|
||||||
import "./cloud-alexa-pref";
|
import "./cloud-alexa-pref";
|
||||||
import "./cloud-google-pref";
|
import "./cloud-google-pref";
|
||||||
|
|
||||||
|
let registeredWebhookDialog = false;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @appliesMixin EventsMixin
|
* @appliesMixin EventsMixin
|
||||||
* @appliesMixin LocalizeMixin
|
* @appliesMixin LocalizeMixin
|
||||||
@ -129,6 +133,11 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|||||||
hass="[[hass]]"
|
hass="[[hass]]"
|
||||||
cloud-status="[[cloudStatus]]"
|
cloud-status="[[cloudStatus]]"
|
||||||
></cloud-google-pref>
|
></cloud-google-pref>
|
||||||
|
|
||||||
|
<cloud-webhooks
|
||||||
|
hass="[[hass]]"
|
||||||
|
cloud-status="[[cloudStatus]]"
|
||||||
|
></cloud-webhooks>
|
||||||
</ha-config-section>
|
</ha-config-section>
|
||||||
</div>
|
</div>
|
||||||
</hass-subpage>
|
</hass-subpage>
|
||||||
@ -152,9 +161,26 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|||||||
this._fetchSubscriptionInfo();
|
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() {
|
async _fetchSubscriptionInfo() {
|
||||||
this._subscription = await fetchSubscriptionInfo(this.hass);
|
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");
|
this.fire("ha-refresh-cloud-status");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { CloudWebhook } from "../../../data/cloud";
|
||||||
|
import { Webhook } from "../../../data/webhook";
|
||||||
|
|
||||||
export interface EntityFilter {
|
export interface EntityFilter {
|
||||||
include_domains: string[];
|
include_domains: string[];
|
||||||
include_entities: string[];
|
include_entities: string[];
|
||||||
@ -19,6 +22,7 @@ export type CloudStatusLoggedIn = CloudStatusBase & {
|
|||||||
google_enabled: boolean;
|
google_enabled: boolean;
|
||||||
alexa_enabled: boolean;
|
alexa_enabled: boolean;
|
||||||
google_allow_unlock: boolean;
|
google_allow_unlock: boolean;
|
||||||
|
cloudhooks: { [webhookId: string]: CloudWebhook };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -27,3 +31,9 @@ export type CloudStatus = CloudStatusBase | CloudStatusLoggedIn;
|
|||||||
export interface SubscriptionInfo {
|
export interface SubscriptionInfo {
|
||||||
human_description: string;
|
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 */
|
/* eslint-disable camelcase */
|
||||||
render({ trigger, hass, localize }) {
|
render({ trigger, hass, localize }) {
|
||||||
const { value_template, entity_id, below, above } = trigger;
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ha-entity-picker
|
<ha-entity-picker
|
||||||
@ -57,6 +68,14 @@ export default class NumericStateTrigger extends Component {
|
|||||||
value={value_template}
|
value={value_template}
|
||||||
onvalue-changed={this.onChange}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -164,7 +164,7 @@ class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|||||||
</template>
|
</template>
|
||||||
</p>
|
</p>
|
||||||
<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
|
<div id="love" style="cursor:pointer;" on-click="_toggleDefaultPage">[[_defaultPageText()]]</div
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -364,15 +364,15 @@ class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|||||||
|
|
||||||
_defaultPageText() {
|
_defaultPageText() {
|
||||||
return `>> ${
|
return `>> ${
|
||||||
localStorage.defaultPage === "lovelace" ? "Remove" : "Set"
|
localStorage.defaultPage === "states" ? "Remove" : "Set"
|
||||||
} lovelace as default page on this device <<`;
|
} the old states as default page on this device <<`;
|
||||||
}
|
}
|
||||||
|
|
||||||
_toggleDefaultPage() {
|
_toggleDefaultPage() {
|
||||||
if (localStorage.defaultPage === "lovelace") {
|
if (localStorage.defaultPage === "states") {
|
||||||
delete localStorage.defaultPage;
|
delete localStorage.defaultPage;
|
||||||
} else {
|
} else {
|
||||||
localStorage.defaultPage = "lovelace";
|
localStorage.defaultPage = "states";
|
||||||
}
|
}
|
||||||
this.$.love.innerText = this._defaultPageText();
|
this.$.love.innerText = this._defaultPageText();
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
|
||||||
import "../../layouts/partial-cards";
|
import "../states/ha-panel-states";
|
||||||
|
|
||||||
class HaPanelKiosk extends PolymerElement {
|
class HaPanelKiosk extends PolymerElement {
|
||||||
static get template() {
|
static get template() {
|
||||||
return html`
|
return html`
|
||||||
<partial-cards
|
<ha-panel-states
|
||||||
id="kiosk-states"
|
id="kiosk-states"
|
||||||
hass="[[hass]]"
|
hass="[[hass]]"
|
||||||
show-menu
|
show-menu
|
||||||
route="[[route]]"
|
route="[[route]]"
|
||||||
panel-visible
|
panel-visible
|
||||||
></partial-cards>
|
></ha-panel-states>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,8 +209,8 @@ class HuiAlarmPanelCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
|||||||
|
|
||||||
_computeHeader(localize, stateObj) {
|
_computeHeader(localize, stateObj) {
|
||||||
if (!stateObj) return "";
|
if (!stateObj) return "";
|
||||||
return this._config.title
|
return this._config.name
|
||||||
? this._config.title
|
? this._config.name
|
||||||
: this._label(localize, stateObj.state);
|
: this._label(localize, stateObj.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import createCardElement from "../common/create-card-element";
|
import createCardElement from "../common/create-card-element";
|
||||||
import { computeCardSize } from "../common/compute-card-size";
|
import { computeCardSize } from "../common/compute-card-size";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
import { LovelaceCard } from "../types";
|
||||||
|
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||||
|
|
||||||
interface Condition {
|
interface Condition {
|
||||||
entity: string;
|
entity: string;
|
||||||
@ -9,8 +10,8 @@ interface Condition {
|
|||||||
state_not?: string;
|
state_not?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Config extends LovelaceConfig {
|
interface Config extends LovelaceCardConfig {
|
||||||
card: LovelaceConfig;
|
card: LovelaceCardConfig;
|
||||||
conditions: Condition[];
|
conditions: Condition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,8 +14,9 @@ import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
|
|||||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { EntityConfig, EntityRow } from "../entity-rows/types";
|
import { EntityConfig, EntityRow } from "../entity-rows/types";
|
||||||
import { LovelaceCard, LovelaceConfig, LovelaceCardEditor } from "../types";
|
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||||
import processConfigEntities from "../common/process-config-entities";
|
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||||
|
import { processConfigEntities } from "../common/process-config-entities";
|
||||||
import createRowElement from "../common/create-row-element";
|
import createRowElement from "../common/create-row-element";
|
||||||
import computeDomain from "../../../common/entity/compute_domain";
|
import computeDomain from "../../../common/entity/compute_domain";
|
||||||
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
|
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
|
||||||
@ -29,7 +30,7 @@ export interface ConfigEntity extends EntityConfig {
|
|||||||
url?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config extends LovelaceConfig {
|
export interface Config extends LovelaceCardConfig {
|
||||||
show_header_toggle?: boolean;
|
show_header_toggle?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
entities: ConfigEntity[];
|
entities: ConfigEntity[];
|
||||||
@ -42,6 +43,11 @@ class HuiEntitiesCard extends hassLocalizeLitMixin(LitElement)
|
|||||||
await import("../editor/config-elements/hui-entities-card-editor");
|
await import("../editor/config-elements/hui-entities-card-editor");
|
||||||
return document.createElement("hui-entities-card-editor");
|
return document.createElement("hui-entities-card-editor");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static getStubConfig(): object {
|
||||||
|
return { entities: [] };
|
||||||
|
}
|
||||||
|
|
||||||
protected _hass?: HomeAssistant;
|
protected _hass?: HomeAssistant;
|
||||||
protected _config?: Config;
|
protected _config?: Config;
|
||||||
protected _configEntities?: ConfigEntity[];
|
protected _configEntities?: ConfigEntity[];
|
||||||
|
@ -10,7 +10,6 @@ import { styleMap } from "lit-html/directives/styleMap";
|
|||||||
|
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
|
|
||||||
import toggleEntity from "../common/entity/toggle-entity";
|
|
||||||
import isValidEntityId from "../../../common/entity/valid_entity_id";
|
import isValidEntityId from "../../../common/entity/valid_entity_id";
|
||||||
import stateIcon from "../../../common/entity/state_icon";
|
import stateIcon from "../../../common/entity/state_icon";
|
||||||
import computeStateDomain from "../../../common/entity/compute_state_domain";
|
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 applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
|
||||||
import { HomeAssistant, LightEntity } from "../../../types";
|
import { HomeAssistant, LightEntity } from "../../../types";
|
||||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
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 { 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;
|
entity: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
tap_action?: "toggle" | "call-service" | "more-info";
|
tap_action?: ActionConfig;
|
||||||
hold_action?: "toggle" | "call-service" | "more-info";
|
hold_action?: ActionConfig;
|
||||||
service?: string;
|
|
||||||
service_data?: object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
|
class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
|
||||||
@ -81,8 +79,8 @@ class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
|
|||||||
return html`
|
return html`
|
||||||
${this.renderStyle()}
|
${this.renderStyle()}
|
||||||
<ha-card
|
<ha-card
|
||||||
@ha-click="${() => this.handleClick(false)}"
|
@ha-click="${this._handleTap}"
|
||||||
@ha-hold="${() => this.handleClick(true)}"
|
@ha-hold="${this._handleHold}"
|
||||||
.longPress="${longPress()}"
|
.longPress="${longPress()}"
|
||||||
>
|
>
|
||||||
${
|
${
|
||||||
@ -186,34 +184,12 @@ class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
|
|||||||
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
|
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleClick(hold: boolean): void {
|
private _handleTap() {
|
||||||
const config = this._config;
|
handleClick(this, this.hass!, this._config!, false);
|
||||||
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 _handleHold() {
|
||||||
|
handleClick(this, this.hass!, this._config!, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
|
||||||
import createCardElement from "../common/create-card-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) {
|
function getEntities(hass, filterState, entities) {
|
||||||
return entities.filter((entityConf) => {
|
return entities.filter((entityConf) => {
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { html, LitElement } from "@polymer/lit-element";
|
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";
|
import { TemplateResult } from "lit-html";
|
||||||
|
|
||||||
interface Config extends LovelaceConfig {
|
interface Config extends LovelaceCardConfig {
|
||||||
error: string;
|
error: string;
|
||||||
origConfig: LovelaceConfig;
|
origConfig: LovelaceCardConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
class HuiErrorCard extends LitElement implements LovelaceCard {
|
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);
|
return JSON.stringify(config, null, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,20 +6,22 @@ import {
|
|||||||
} from "@polymer/lit-element";
|
} from "@polymer/lit-element";
|
||||||
import { TemplateResult } from "lit-html";
|
import { TemplateResult } from "lit-html";
|
||||||
|
|
||||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
import { LovelaceCard } from "../types";
|
||||||
|
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||||
|
|
||||||
import isValidEntityId from "../../../common/entity/valid_entity_id";
|
import isValidEntityId from "../../../common/entity/valid_entity_id";
|
||||||
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
|
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";
|
import "../../../components/ha-card";
|
||||||
|
|
||||||
interface Config extends LovelaceConfig {
|
interface Config extends LovelaceCardConfig {
|
||||||
entity: string;
|
entity: string;
|
||||||
title?: string;
|
name?: string;
|
||||||
unit_of_measurement?: string;
|
unit?: string;
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
severity?: object;
|
severity?: object;
|
||||||
@ -87,12 +89,14 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
|||||||
<div id="percent">
|
<div id="percent">
|
||||||
${stateObj.state}
|
${stateObj.state}
|
||||||
${
|
${
|
||||||
this._config.unit_of_measurement ||
|
this._config.unit ||
|
||||||
stateObj.attributes.unit_of_measurement ||
|
stateObj.attributes.unit_of_measurement ||
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div id="title">${this._config.title}</div>
|
<div id="name">
|
||||||
|
${this._config.name || computeStateName(stateObj)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@ -210,7 +214,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
|||||||
.gauge-data #percent {
|
.gauge-data #percent {
|
||||||
font-size: calc(var(--base-unit) * 0.55);
|
font-size: calc(var(--base-unit) * 0.55);
|
||||||
}
|
}
|
||||||
.gauge-data #title {
|
.gauge-data #name {
|
||||||
padding-top: calc(var(--base-unit) * 0.15);
|
padding-top: calc(var(--base-unit) * 0.15);
|
||||||
font-size: calc(var(--base-unit) * 0.3);
|
font-size: calc(var(--base-unit) * 0.3);
|
||||||
}
|
}
|
||||||
|
@ -7,31 +7,29 @@ import {
|
|||||||
import { TemplateResult } from "lit-html";
|
import { TemplateResult } from "lit-html";
|
||||||
import { classMap } from "lit-html/directives/classMap";
|
import { classMap } from "lit-html/directives/classMap";
|
||||||
|
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
|
||||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||||
import { HomeAssistant } from "../../../types";
|
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 { longPress } from "../common/directives/long-press-directive";
|
||||||
import { EntityConfig } from "../entity-rows/types";
|
import { EntityConfig } from "../entity-rows/types";
|
||||||
|
import { processConfigEntities } from "../common/process-config-entities";
|
||||||
|
|
||||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||||
import computeStateName from "../../../common/entity/compute_state_name";
|
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 applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
|
||||||
import toggleEntity from "../common/entity/toggle-entity";
|
|
||||||
|
|
||||||
import "../../../components/entity/state-badge";
|
import "../../../components/entity/state-badge";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-icon";
|
import "../../../components/ha-icon";
|
||||||
|
import { handleClick } from "../common/handle-click";
|
||||||
|
|
||||||
export interface ConfigEntity extends EntityConfig {
|
export interface ConfigEntity extends EntityConfig {
|
||||||
tap_action?: "toggle" | "call-service" | "more-info";
|
tap_action?: ActionConfig;
|
||||||
hold_action?: "toggle" | "call-service" | "more-info";
|
hold_action?: ActionConfig;
|
||||||
service?: string;
|
|
||||||
service_data?: object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config extends LovelaceConfig {
|
export interface Config extends LovelaceCardConfig {
|
||||||
show_name?: boolean;
|
show_name?: boolean;
|
||||||
show_state?: boolean;
|
show_state?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
@ -46,6 +44,9 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
|
|||||||
await import("../editor/config-elements/hui-glance-card-editor");
|
await import("../editor/config-elements/hui-glance-card-editor");
|
||||||
return document.createElement("hui-glance-card-editor");
|
return document.createElement("hui-glance-card-editor");
|
||||||
}
|
}
|
||||||
|
public static getStubConfig(): object {
|
||||||
|
return { entities: [] };
|
||||||
|
}
|
||||||
|
|
||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
private _config?: Config;
|
private _config?: Config;
|
||||||
@ -67,13 +68,16 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
|
|||||||
|
|
||||||
public setConfig(config: Config): void {
|
public setConfig(config: Config): void {
|
||||||
this._config = { theme: "default", ...config };
|
this._config = { theme: "default", ...config };
|
||||||
const entities = processConfigEntities(config.entities);
|
const entities = processConfigEntities<ConfigEntity>(config.entities);
|
||||||
|
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
if (
|
if (
|
||||||
(entity.tap_action === "call-service" ||
|
(entity.tap_action &&
|
||||||
entity.hold_action === "call-service") &&
|
entity.tap_action.action === "call-service" &&
|
||||||
!entity.service
|
!entity.tap_action.service) ||
|
||||||
|
(entity.hold_action &&
|
||||||
|
entity.hold_action.action === "call-service" &&
|
||||||
|
!entity.hold_action.service)
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Missing required property "service" when tap_action or hold_action is call-service'
|
'Missing required property "service" when tap_action or hold_action is call-service'
|
||||||
@ -199,8 +203,8 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
|
|||||||
<div
|
<div
|
||||||
class="entity"
|
class="entity"
|
||||||
.entityConf="${entityConf}"
|
.entityConf="${entityConf}"
|
||||||
@ha-click="${(ev) => this.handleClick(ev, false)}"
|
@ha-click="${this._handleTap}"
|
||||||
@ha-hold="${(ev) => this.handleClick(ev, true)}"
|
@ha-hold="${this._handleHold}"
|
||||||
.longPress="${longPress()}"
|
.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 config = (ev.currentTarget as any).entityConf as ConfigEntity;
|
||||||
const entityId = config.entity;
|
handleClick(this, this.hass!, config, false);
|
||||||
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:
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 "../../../components/state-history-charts";
|
||||||
import "../../../data/ha-state-history-data";
|
import "../../../data/ha-state-history-data";
|
||||||
|
|
||||||
import processConfigEntities from "../common/process-config-entities";
|
import { processConfigEntities } from "../common/process-config-entities";
|
||||||
|
|
||||||
class HuiHistoryGraphCard extends PolymerElement {
|
class HuiHistoryGraphCard extends PolymerElement {
|
||||||
static get template() {
|
static get template() {
|
||||||
|
@ -2,11 +2,12 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
|||||||
|
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
|
|
||||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
import { LovelaceCard } from "../types";
|
||||||
|
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||||
import { TemplateResult } from "lit-html";
|
import { TemplateResult } from "lit-html";
|
||||||
import { styleMap } from "lit-html/directives/styleMap";
|
import { styleMap } from "lit-html/directives/styleMap";
|
||||||
|
|
||||||
interface Config extends LovelaceConfig {
|
interface Config extends LovelaceCardConfig {
|
||||||
aspect_ratio?: string;
|
aspect_ratio?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -34,6 +34,7 @@ export default class LegacyWrapperCard extends HTMLElement {
|
|||||||
this._ensureElement(this._tag);
|
this._ensureElement(this._tag);
|
||||||
this.lastChild.hass = hass;
|
this.lastChild.hass = hass;
|
||||||
this.lastChild.stateObj = hass.states[entityId];
|
this.lastChild.stateObj = hass.states[entityId];
|
||||||
|
this.lastChild.config = this._config;
|
||||||
} else {
|
} else {
|
||||||
this._ensureElement("HUI-ERROR-CARD");
|
this._ensureElement("HUI-ERROR-CARD");
|
||||||
this.lastChild.setConfig(
|
this.lastChild.setConfig(
|
||||||
|
@ -12,7 +12,8 @@ import { jQuery } from "../../../resources/jquery";
|
|||||||
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
|
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
|
||||||
import { HomeAssistant, LightEntity } from "../../../types";
|
import { HomeAssistant, LightEntity } from "../../../types";
|
||||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
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 { longPress } from "../common/directives/long-press-directive";
|
||||||
|
|
||||||
import stateIcon from "../../../common/entity/state_icon";
|
import stateIcon from "../../../common/entity/state_icon";
|
||||||
@ -37,7 +38,7 @@ const lightConfig = {
|
|||||||
showTooltip: false,
|
showTooltip: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Config extends LovelaceConfig {
|
interface Config extends LovelaceCardConfig {
|
||||||
entity: string;
|
entity: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
|
@ -6,10 +6,11 @@ import Leaflet from "leaflet";
|
|||||||
import "../../map/ha-entity-marker";
|
import "../../map/ha-entity-marker";
|
||||||
|
|
||||||
import setupLeafletMap from "../../../common/dom/setup-leaflet-map";
|
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 computeStateDomain from "../../../common/entity/compute_state_domain";
|
||||||
import computeStateName from "../../../common/entity/compute_state_name";
|
import computeStateName from "../../../common/entity/compute_state_name";
|
||||||
import debounce from "../../../common/util/debounce";
|
import debounce from "../../../common/util/debounce";
|
||||||
|
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||||
|
|
||||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
|
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
|
||||||
|
|
||||||
@ -97,7 +98,15 @@ class HuiMapCard extends PolymerElement {
|
|||||||
return;
|
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) {
|
setConfig(config) {
|
||||||
@ -110,8 +119,13 @@ class HuiMapCard extends PolymerElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCardSize() {
|
getCardSize() {
|
||||||
let ar = this._config.aspect_ratio || "100%";
|
const ratio = parseAspectRatio(this._config.aspect_ratio);
|
||||||
ar = ar.substr(0, ar.length - 1);
|
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;
|
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-card";
|
||||||
import "../../../components/ha-markdown";
|
import "../../../components/ha-markdown";
|
||||||
|
|
||||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
import { LovelaceCard } from "../types";
|
||||||
|
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||||
import { TemplateResult } from "lit-html";
|
import { TemplateResult } from "lit-html";
|
||||||
|
|
||||||
interface Config extends LovelaceConfig {
|
interface Config extends LovelaceCardConfig {
|
||||||
content: string;
|
content: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,18 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
|||||||
|
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
|
|
||||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
import { LovelaceCard } from "../types";
|
||||||
import { navigate } from "../../../common/navigate";
|
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { TemplateResult } from "lit-html";
|
import { TemplateResult } from "lit-html";
|
||||||
import { classMap } from "lit-html/directives/classMap";
|
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;
|
image?: string;
|
||||||
navigation_path?: string;
|
tap_action?: ActionConfig;
|
||||||
service?: string;
|
hold_action?: ActionConfig;
|
||||||
service_data?: object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HuiPictureCard extends LitElement implements LovelaceCard {
|
export class HuiPictureCard extends LitElement implements LovelaceCard {
|
||||||
@ -45,11 +46,13 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
|||||||
return html`
|
return html`
|
||||||
${this.renderStyle()}
|
${this.renderStyle()}
|
||||||
<ha-card
|
<ha-card
|
||||||
@click="${this.handleClick}"
|
@ha-click="${this._handleTap}"
|
||||||
|
@ha-hold="${this._handleHold}"
|
||||||
|
.longPress="${longPress()}"
|
||||||
class="${
|
class="${
|
||||||
classMap({
|
classMap({
|
||||||
clickable: Boolean(
|
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 {
|
private _handleTap() {
|
||||||
if (this._config!.navigation_path) {
|
handleClick(this, this.hass!, this._config!, false);
|
||||||
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 _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 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 { HomeAssistant } from "../../../types";
|
||||||
import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
|
import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
|
||||||
|
|
||||||
interface Config extends LovelaceConfig {
|
interface Config extends LovelaceCardConfig {
|
||||||
title?: string;
|
title?: string;
|
||||||
image: string;
|
image?: string;
|
||||||
|
camera_image?: string;
|
||||||
|
state_image?: {};
|
||||||
|
aspect_ratio?: string;
|
||||||
|
entity?: string;
|
||||||
elements: LovelaceElementConfig[];
|
elements: LovelaceElementConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +43,10 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
|||||||
public setConfig(config: Config): void {
|
public setConfig(config: Config): void {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
throw new Error("Invalid Configuration");
|
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");
|
throw new Error("Invalid Configuration: image required");
|
||||||
} else if (!Array.isArray(config.elements)) {
|
} else if (!Array.isArray(config.elements)) {
|
||||||
throw new Error("Invalid Configuration: elements required");
|
throw new Error("Invalid Configuration: elements required");
|
||||||
@ -55,13 +63,19 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
|||||||
return html`
|
return html`
|
||||||
${this.renderStyle()}
|
${this.renderStyle()}
|
||||||
<ha-card .header="${this._config.title}">
|
<ha-card .header="${this._config.title}">
|
||||||
<div id="root">
|
<hui-image
|
||||||
<img src="${this._config.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._config.elements.map((elementConfig: LovelaceElementConfig) =>
|
||||||
this._createHuiElement(elementConfig)
|
this._createHuiElement(elementConfig)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -71,14 +85,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
|||||||
<style>
|
<style>
|
||||||
ha-card {
|
ha-card {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
|
||||||
#root {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#root img {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
.element {
|
.element {
|
||||||
position: absolute;
|
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 { TemplateResult } from "lit-html";
|
||||||
|
|
||||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
|
||||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
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 { EntityConfig } from "../entity-rows/types";
|
||||||
import { navigate } from "../../../common/navigate";
|
|
||||||
import { HomeAssistant } from "../../../types";
|
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 computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||||
import computeStateName from "../../../common/entity/compute_state_name";
|
import computeStateName from "../../../common/entity/compute_state_name";
|
||||||
import processConfigEntities from "../common/process-config-entities";
|
|
||||||
import computeDomain from "../../../common/entity/compute_domain";
|
import computeDomain from "../../../common/entity/compute_domain";
|
||||||
import stateIcon from "../../../common/entity/state_icon";
|
import stateIcon from "../../../common/entity/state_icon";
|
||||||
import toggleEntity from "../common/entity/toggle-entity";
|
|
||||||
|
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-icon";
|
import "../../../components/ha-icon";
|
||||||
import "../components/hui-image";
|
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"]);
|
const STATES_OFF = new Set(["closed", "locked", "not_home", "off"]);
|
||||||
|
|
||||||
interface Config extends LovelaceConfig {
|
interface Config extends LovelaceCardConfig {
|
||||||
entities: EntityConfig[];
|
entities: EntityConfig[];
|
||||||
title?: string;
|
title?: string;
|
||||||
navigation_path?: string;
|
|
||||||
image?: string;
|
image?: string;
|
||||||
camera_image?: string;
|
camera_image?: string;
|
||||||
state_image?: {};
|
state_image?: {};
|
||||||
aspect_ratio?: string;
|
aspect_ratio?: string;
|
||||||
entity?: string;
|
entity?: string;
|
||||||
force_dialog?: boolean;
|
tap_action?: ActionConfig;
|
||||||
|
hold_action?: ActionConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
class HuiPictureGlanceCard extends hassLocalizeLitMixin(LitElement)
|
class HuiPictureGlanceCard extends hassLocalizeLitMixin(LitElement)
|
||||||
@ -87,19 +88,22 @@ class HuiPictureGlanceCard extends hassLocalizeLitMixin(LitElement)
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isClickable =
|
|
||||||
this._config.navigation_path || this._config.camera_image;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this.renderStyle()}
|
${this.renderStyle()}
|
||||||
<ha-card>
|
<ha-card>
|
||||||
<hui-image
|
<hui-image
|
||||||
class="${
|
class="${
|
||||||
classMap({
|
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}"
|
.hass="${this.hass}"
|
||||||
.image="${this._config.image}"
|
.image="${this._config.image}"
|
||||||
.stateImage="${this._config.state_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 {
|
private _openDialog(ev: MouseEvent): void {
|
||||||
fireEvent(this, "hass-more-info", { entityId: (ev.target as any).entity });
|
fireEvent(this, "hass-more-info", { entityId: (ev.target as any).entity });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _callService(ev: MouseEvent): void {
|
private _callService(ev: MouseEvent): void {
|
||||||
toggleEntity(this.hass, (ev.target as any).entity);
|
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderStyle(): TemplateResult {
|
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 { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
import { LovelaceCard } from "../types";
|
||||||
|
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||||
import {
|
import {
|
||||||
fetchItems,
|
fetchItems,
|
||||||
completeItem,
|
updateItem,
|
||||||
saveEdit,
|
|
||||||
ShoppingListItem,
|
ShoppingListItem,
|
||||||
clearItems,
|
clearItems,
|
||||||
addItem,
|
addItem,
|
||||||
} from "../../../data/shopping-list";
|
} from "../../../data/shopping-list";
|
||||||
|
|
||||||
interface Config extends LovelaceConfig {
|
interface Config extends LovelaceCardConfig {
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,15 +256,15 @@ class HuiShoppingListCard extends hassLocalizeLitMixin(LitElement)
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _completeItem(ev): void {
|
private _completeItem(ev): void {
|
||||||
completeItem(this.hass!, ev.target.itemId, ev.target.checked).catch(() =>
|
updateItem(this.hass!, ev.target.itemId, {
|
||||||
this._fetchData()
|
complete: ev.target.checked,
|
||||||
);
|
}).catch(() => this._fetchData());
|
||||||
}
|
}
|
||||||
|
|
||||||
private _saveEdit(ev): void {
|
private _saveEdit(ev): void {
|
||||||
saveEdit(this.hass!, ev.target.itemId, ev.target.value).catch(() =>
|
updateItem(this.hass!, ev.target.itemId, {
|
||||||
this._fetchData()
|
name: ev.target.value,
|
||||||
);
|
}).catch(() => this._fetchData());
|
||||||
|
|
||||||
ev.target.blur();
|
ev.target.blur();
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,12 @@ import { TemplateResult } from "lit-html";
|
|||||||
|
|
||||||
import createCardElement from "../common/create-card-element";
|
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";
|
import { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
interface Config extends LovelaceConfig {
|
interface Config extends LovelaceCardConfig {
|
||||||
cards: LovelaceConfig[];
|
cards: LovelaceCardConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class HuiStackCard extends LitElement implements LovelaceCard {
|
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 { roundSliderStyle } from "../../../resources/jquery.roundslider";
|
||||||
import { HomeAssistant, ClimateEntity } from "../../../types";
|
import { HomeAssistant, ClimateEntity } from "../../../types";
|
||||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
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-card";
|
||||||
import "../../../components/ha-icon";
|
import "../../../components/ha-icon";
|
||||||
@ -43,9 +44,10 @@ const modeIcons = {
|
|||||||
idle: "hass:power-sleep",
|
idle: "hass:power-sleep",
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Config extends LovelaceConfig {
|
interface Config extends LovelaceCardConfig {
|
||||||
entity: string;
|
entity: string;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTemp(temps: string[]): string {
|
function formatTemp(temps: string[]): string {
|
||||||
@ -96,7 +98,8 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
|
|||||||
<div id="root">
|
<div id="root">
|
||||||
<div id="thermostat"></div>
|
<div id="thermostat"></div>
|
||||||
<div id="tooltip">
|
<div id="tooltip">
|
||||||
<div class="title">${computeStateName(stateObj)}</div>
|
<div class="title">${this._config.name ||
|
||||||
|
computeStateName(stateObj)}</div>
|
||||||
<div class="current-temperature">
|
<div class="current-temperature">
|
||||||
<span class="current-temperature-text">
|
<span class="current-temperature-text">
|
||||||
${stateObj.attributes.current_temperature}
|
${stateObj.attributes.current_temperature}
|
||||||
|
@ -20,7 +20,7 @@ export const computeTooltip = (
|
|||||||
: config.entity;
|
: config.entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (config.tap_action) {
|
switch (config.tap_action && config.tap_action.action) {
|
||||||
case "navigate":
|
case "navigate":
|
||||||
tooltip = `Navigate to ${config.navigation_path}`;
|
tooltip = `Navigate to ${config.navigation_path}`;
|
||||||
break;
|
break;
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
|
||||||
import "../cards/hui-alarm-panel-card";
|
import "../cards/hui-alarm-panel-card";
|
||||||
import "../cards/hui-conditional-card.ts";
|
import "../cards/hui-conditional-card";
|
||||||
import "../cards/hui-entities-card.ts";
|
import "../cards/hui-entities-card";
|
||||||
import "../cards/hui-entity-button-card.ts";
|
import "../cards/hui-entity-button-card";
|
||||||
import "../cards/hui-entity-filter-card";
|
import "../cards/hui-entity-filter-card";
|
||||||
import "../cards/hui-error-card.ts";
|
import "../cards/hui-error-card";
|
||||||
import "../cards/hui-glance-card.ts";
|
import "../cards/hui-glance-card";
|
||||||
import "../cards/hui-history-graph-card";
|
import "../cards/hui-history-graph-card";
|
||||||
import "../cards/hui-horizontal-stack-card.ts";
|
import "../cards/hui-horizontal-stack-card";
|
||||||
import "../cards/hui-iframe-card.ts";
|
import "../cards/hui-iframe-card";
|
||||||
import "../cards/hui-light-card";
|
import "../cards/hui-light-card";
|
||||||
import "../cards/hui-map-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-media-control-card";
|
||||||
import "../cards/hui-picture-card";
|
import "../cards/hui-picture-card";
|
||||||
import "../cards/hui-picture-elements-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-picture-glance-card";
|
||||||
import "../cards/hui-plant-status-card";
|
import "../cards/hui-plant-status-card";
|
||||||
import "../cards/hui-sensor-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-shopping-list-card";
|
||||||
import "../cards/hui-thermostat-card.ts";
|
import "../cards/hui-thermostat-card";
|
||||||
import "../cards/hui-weather-forecast-card";
|
import "../cards/hui-weather-forecast-card";
|
||||||
import "../cards/hui-gauge-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-media-player-entity-row";
|
||||||
import "../entity-rows/hui-scene-entity-row";
|
import "../entity-rows/hui-scene-entity-row";
|
||||||
import "../entity-rows/hui-script-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-text-entity-row";
|
||||||
import "../entity-rows/hui-timer-entity-row";
|
import "../entity-rows/hui-timer-entity-row";
|
||||||
import "../entity-rows/hui-toggle-entity-row";
|
import "../entity-rows/hui-toggle-entity-row";
|
||||||
@ -28,6 +29,7 @@ const SPECIAL_TYPES = new Set([
|
|||||||
"weblink",
|
"weblink",
|
||||||
]);
|
]);
|
||||||
const DOMAIN_TO_ELEMENT_TYPE = {
|
const DOMAIN_TO_ELEMENT_TYPE = {
|
||||||
|
alert: "toggle",
|
||||||
automation: "toggle",
|
automation: "toggle",
|
||||||
climate: "climate",
|
climate: "climate",
|
||||||
cover: "cover",
|
cover: "cover",
|
||||||
@ -42,6 +44,7 @@ const DOMAIN_TO_ELEMENT_TYPE = {
|
|||||||
lock: "lock",
|
lock: "lock",
|
||||||
scene: "scene",
|
scene: "scene",
|
||||||
script: "script",
|
script: "script",
|
||||||
|
sensor: "sensor",
|
||||||
timer: "timer",
|
timer: "timer",
|
||||||
switch: "toggle",
|
switch: "toggle",
|
||||||
vacuum: "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 ripple: any;
|
||||||
protected timer: number | undefined;
|
protected timer: number | undefined;
|
||||||
protected held: boolean;
|
protected held: boolean;
|
||||||
|
protected cooldownStart: boolean;
|
||||||
|
protected cooldownEnd: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -26,6 +28,8 @@ class LongPress extends HTMLElement implements LongPress {
|
|||||||
this.ripple = document.createElement("mwc-ripple");
|
this.ripple = document.createElement("mwc-ripple");
|
||||||
this.timer = undefined;
|
this.timer = undefined;
|
||||||
this.held = false;
|
this.held = false;
|
||||||
|
this.cooldownStart = false;
|
||||||
|
this.cooldownEnd = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
@ -41,7 +45,8 @@ class LongPress extends HTMLElement implements LongPress {
|
|||||||
this.ripple.primary = true;
|
this.ripple.primary = true;
|
||||||
|
|
||||||
[
|
[
|
||||||
isTouch ? "touchcancel" : "mouseout",
|
"touchcancel",
|
||||||
|
"mouseout",
|
||||||
"mouseup",
|
"mouseup",
|
||||||
"touchmove",
|
"touchmove",
|
||||||
"mousewheel",
|
"mousewheel",
|
||||||
@ -80,6 +85,9 @@ class LongPress extends HTMLElement implements LongPress {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const clickStart = (ev: Event) => {
|
const clickStart = (ev: Event) => {
|
||||||
|
if (this.cooldownStart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.held = false;
|
this.held = false;
|
||||||
let x;
|
let x;
|
||||||
let y;
|
let y;
|
||||||
@ -94,31 +102,36 @@ class LongPress extends HTMLElement implements LongPress {
|
|||||||
this.startAnimation(x, y);
|
this.startAnimation(x, y);
|
||||||
this.held = true;
|
this.held = true;
|
||||||
}, this.holdTime);
|
}, this.holdTime);
|
||||||
|
|
||||||
|
this.cooldownStart = true;
|
||||||
|
window.setTimeout(() => (this.cooldownStart = false), 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clickEnd = () => {
|
const clickEnd = (ev: Event) => {
|
||||||
clearTimeout(this.timer);
|
if (
|
||||||
this.stopAnimation();
|
this.cooldownEnd ||
|
||||||
if (isTouch && this.timer === undefined) {
|
(ev instanceof TouchEvent && this.timer === undefined)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
this.stopAnimation();
|
||||||
this.timer = undefined;
|
this.timer = undefined;
|
||||||
if (this.held) {
|
if (this.held) {
|
||||||
element.dispatchEvent(new Event("ha-hold"));
|
element.dispatchEvent(new Event("ha-hold"));
|
||||||
} else {
|
} else {
|
||||||
element.dispatchEvent(new Event("ha-click"));
|
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("touchstart", clickStart, { passive: true });
|
||||||
element.addEventListener("touchend", clickEnd);
|
element.addEventListener("touchend", clickEnd);
|
||||||
element.addEventListener("touchcancel", clickEnd);
|
element.addEventListener("touchcancel", clickEnd);
|
||||||
} else {
|
|
||||||
element.addEventListener("mousedown", clickStart, { passive: true });
|
element.addEventListener("mousedown", clickStart, { passive: true });
|
||||||
element.addEventListener("click", clickEnd);
|
element.addEventListener("click", clickEnd);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private startAnimation(x: number, y: number) {
|
private startAnimation(x: number, y: number) {
|
||||||
Object.assign(this.style, {
|
Object.assign(this.style, {
|
||||||
|
@ -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 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 = {};
|
const domainsToCall = {};
|
||||||
entityIds.forEach((entityId) => {
|
entityIds.forEach((entityId) => {
|
||||||
if (STATES_OFF.includes(hass.states[entityId].state) === turnOn) {
|
if (STATES_OFF.includes(hass.states[entityId].state) === turnOn) {
|
||||||
@ -10,7 +15,9 @@ export default function turnOnOffEntities(hass, entityIds, turnOn = true) {
|
|||||||
? stateDomain
|
? stateDomain
|
||||||
: "homeassistant";
|
: "homeassistant";
|
||||||
|
|
||||||
if (!(serviceDomain in domainsToCall)) domainsToCall[serviceDomain] = [];
|
if (!(serviceDomain in domainsToCall)) {
|
||||||
|
domainsToCall[serviceDomain] = [];
|
||||||
|
}
|
||||||
domainsToCall[serviceDomain].push(entityId);
|
domainsToCall[serviceDomain].push(entityId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -31,4 +38,4 @@ export default function turnOnOffEntities(hass, entityIds, turnOn = true) {
|
|||||||
const entities = domainsToCall[domain];
|
const entities = domainsToCall[domain];
|
||||||
hass.callService(domain, service, { entity_id: entities });
|
hass.callService(domain, service, { entity_id: entities });
|
||||||
});
|
});
|
||||||
}
|
};
|
@ -1,6 +1,11 @@
|
|||||||
import computeDomain from "../../../../common/entity/compute_domain";
|
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 stateDomain = computeDomain(entityId);
|
||||||
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;
|
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;
|
||||||
|
|
||||||
@ -16,5 +21,5 @@ export default function turnOnOffEntity(hass, entityId, turnOn = true) {
|
|||||||
service = turnOn ? "turn_on" : "turn_off";
|
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