Merge pull request #2190 from home-assistant/dev

20181205.0
This commit is contained in:
Paulus Schoutsen 2018-12-05 22:40:18 +01:00 committed by GitHub
commit f5022f4e1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
184 changed files with 6477 additions and 2541 deletions

View File

@ -69,7 +69,7 @@ class DemoAlarmPanelEntity extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -74,7 +74,7 @@ class DemoConditional extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -188,7 +188,7 @@ class DemoEntities extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -89,7 +89,7 @@ class DemoEntityButtonEntity extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -105,7 +105,7 @@ class DemoFilter extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -89,10 +89,10 @@ const CONFIGS = [
`,
},
{
heading: "Custom column width",
heading: "Custom number of columns",
config: `
- type: glance
column_width: calc(100% / 7)
columns: 7
entities:
- device_tracker.demo_paulus
- media_player.living_room
@ -230,7 +230,7 @@ class DemoPicEntity extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -38,7 +38,7 @@ class DemoLightEntity extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -139,7 +139,7 @@ class DemoMap extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -95,7 +95,7 @@ class DemoHuiMediaPlayerRows extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -93,7 +93,7 @@ class DemoPicElements extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -36,7 +36,7 @@ class DemoShoppingListEntity extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);

View File

@ -104,7 +104,7 @@ class DemoStack extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -75,7 +75,7 @@ class DemoThermostatEntity extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -8,16 +8,7 @@ import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import "../components/demo-more-infos";
/* eslint-disable no-unused-vars */
const SUPPORT_BRIGHTNESS = 1;
const SUPPORT_COLOR_TEMP = 2;
const SUPPORT_EFFECT = 4;
const SUPPORT_FLASH = 8;
const SUPPORT_COLOR = 16;
const SUPPORT_TRANSITION = 32;
const SUPPORT_WHITE_VALUE = 128;
import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
const ENTITIES = [
getEntity("light", "bed_light", "on", {
@ -49,7 +40,7 @@ class DemoMoreInfoLight extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this);
hass.addEntities(ENTITIES);

View 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);

View File

@ -11,7 +11,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../src/managers/notification-manager";
const DEMOS = require.context("./demos", true, /^(.*\.(js$))[^.]*$/im);
const DEMOS = require.context("./demos", true, /^(.*\.(ts$))[^.]*$/im);
const fixPath = (path) => path.substr(2, path.length - 5);
@ -118,6 +118,22 @@ class HaGallery extends PolymerElement {
</a>
</template>
</paper-card>
<paper-card heading="Util demos">
<div class='card-content intro'>
<p>
Test pages for our utility functions.
</p>
</div>
<template is='dom-repeat' items='[[_utilDemos]]'>
<a href='#[[item]]'>
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
<iron-icon icon="hass:chevron-right"></iron-icon>
</paper-item>
</a>
</template>
</paper-card>
</div>
</template>
</div>
@ -145,6 +161,10 @@ class HaGallery extends PolymerElement {
type: Array,
computed: "_computeMoreInfos(_demos)",
},
_utilDemos: {
type: Array,
computed: "_computeUtil(_demos)",
},
};
}
@ -178,7 +198,7 @@ class HaGallery extends PolymerElement {
while (root.lastChild) root.removeChild(root.lastChild);
if (demo) {
DEMOS(`./${demo}.js`);
DEMOS(`./${demo}.ts`);
const el = document.createElement(demo);
root.appendChild(el);
}
@ -199,6 +219,10 @@ class HaGallery extends PolymerElement {
_computeMoreInfos(demos) {
return demos.filter((demo) => demo.includes("more-info"));
}
_computeUtil(demos) {
return demos.filter((demo) => demo.includes("util"));
}
}
customElements.define("ha-gallery", HaGallery);

View File

@ -2,6 +2,7 @@ import "@polymer/paper-button/paper-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
import "../../../src/resources/ha-style";
@ -15,10 +16,13 @@ class HassioAddonLogs extends PolymerElement {
}
pre {
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
</style>
${ANSI_HTML_STYLE}
<paper-card heading="Log">
<div class="card-content"><pre>[[log]]</pre></div>
<div class="card-content" id="content"></div>
<div class="card-actions">
<paper-button on-click="refresh">Refresh</paper-button>
</div>
@ -33,7 +37,6 @@ class HassioAddonLogs extends PolymerElement {
type: String,
observer: "addonSlugChanged",
},
log: String,
};
}
@ -51,8 +54,11 @@ class HassioAddonLogs extends PolymerElement {
refresh() {
this.hass
.callApi("get", `hassio/addons/${this.addonSlug}/logs`)
.then((info) => {
this.log = info;
.then((text) => {
while (this.$.content.lastChild) {
this.$.content.removeChild(this.$.content.lastChild);
}
this.$.content.appendChild(parseTextToColoredPre(text));
});
}
}

203
hassio/src/ansi-to-html.js Normal file
View 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;
}

View File

@ -2,6 +2,7 @@ import "@polymer/paper-button/paper-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
class HassioSupervisorLog extends PolymerElement {
static get template() {
@ -12,12 +13,18 @@ class HassioSupervisorLog extends PolymerElement {
}
pre {
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.fg-green {
color: var(--primary-text-color) !important;
}
</style>
${ANSI_HTML_STYLE}
<paper-card>
<div class="card-content"><pre>[[log]]</pre></div>
<div class="card-content" id="content"></div>
<div class="card-actions">
<paper-button on-click="refreshTapped">Refresh</paper-button>
<paper-button on-click="refresh">Refresh</paper-button>
</div>
</paper-card>
`;
@ -26,7 +33,6 @@ class HassioSupervisorLog extends PolymerElement {
static get properties() {
return {
hass: Object,
log: String,
};
}
@ -37,16 +43,20 @@ class HassioSupervisorLog extends PolymerElement {
loadData() {
this.hass.callApi("get", "hassio/supervisor/logs").then(
(info) => {
this.log = info;
(text) => {
while (this.$.content.lastChild) {
this.$.content.removeChild(this.$.content.lastChild);
}
this.$.content.appendChild(parseTextToColoredPre(text));
},
() => {
this.log = "Error fetching logs";
this.$.content.innerHTML =
'<span class="fg-red bold">Error fetching logs</span>';
}
);
}
refreshTapped() {
refresh() {
this.loadData();
}
}

View File

@ -64,10 +64,10 @@
"@polymer/paper-toggle-button": "^3.0.1",
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "^3.0.5",
"@vaadin/vaadin-combo-box": "^4.2.0-beta2",
"@vaadin/vaadin-date-picker": "^3.3.0",
"@webcomponents/shadycss": "^1.5.2",
"@webcomponents/webcomponentsjs": "^2.1.3",
"@vaadin/vaadin-combo-box": "^4.2.0",
"@vaadin/vaadin-date-picker": "^3.3.1",
"@webcomponents/shadycss": "^1.6.0",
"@webcomponents/webcomponentsjs": "^2.2.0",
"chart.js": "~2.7.2",
"chartjs-chart-timeline": "^0.2.1",
"es6-object-assign": "^1.1.0",
@ -87,6 +87,7 @@
"react-big-calendar": "^0.19.2",
"regenerator-runtime": "^0.12.1",
"round-slider": "^1.3.2",
"superstruct": "^0.6.0",
"unfetch": "^4.0.1",
"web-animations-js": "^2.3.1",
"xss": "^1.0.3"
@ -152,15 +153,11 @@
"workbox-webpack-plugin": "^3.5.0"
},
"resolutions": {
"inherits": "2.0.3",
"samsam": "1.1.3",
"supports-color": "3.1.2",
"type-detect": "1.0.0",
"@polymer/polymer": "3.1.0",
"@webcomponents/webcomponentsjs": "2.1.3",
"@webcomponents/shadycss": "^1.5.2",
"@vaadin/vaadin-overlay": "3.2.0-alpha3",
"@vaadin/vaadin-lumo-styles": "1.2.0",
"@webcomponents/webcomponentsjs": "2.2.1",
"@webcomponents/shadycss": "^1.6.0",
"@vaadin/vaadin-overlay": "3.2.2",
"@vaadin/vaadin-lumo-styles": "1.3.0",
"fecha": "https://github.com/taylorhakes/fecha/archive/5e8fe08d982647fdb19fb403459838b02647813c.tar.gz",
"lit-html": "0.12.0",
"@polymer/lit-element": "0.6.2"

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20181121.1",
version="20181205.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",

View File

@ -103,6 +103,7 @@ class HaPlantCard extends EventsMixin(PolymerElement) {
return {
hass: Object,
stateObj: Object,
config: Object,
};
}
@ -118,7 +119,7 @@ class HaPlantCard extends EventsMixin(PolymerElement) {
}
computeTitle(stateObj) {
return computeStateName(stateObj);
return this.config.name || computeStateName(stateObj);
}
computeAttributes(data) {

View File

@ -1,6 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import computeStateName from "../common/entity/compute_state_name";
import "../components/ha-card";
import "../components/ha-icon";
@ -106,7 +108,7 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
<ha-card>
<div class="header">
[[computeState(stateObj.state, localize)]]
<div class="name">[[stateObj.attributes.friendly_name]]</div>
<div class="name">[[computeName(stateObj)]]</div>
</div>
<div class="content">
<div class="now">
@ -271,6 +273,10 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
return localize(`state.weather.${state}`) || state;
}
computeName(stateObj) {
return this.config.name || computeStateName(stateObj);
}
showWeatherIcon(condition) {
return condition in this.weatherIcons;
}

View File

@ -10,31 +10,43 @@ const langKey = ["second", "minute", "hour", "day"];
export default function relativeTime(
dateObj: Date,
localize: LocalizeFunc
localize: LocalizeFunc,
options: {
compareTime?: Date;
includeTense?: boolean;
} = {}
): string {
let delta = (new Date().getTime() - dateObj.getTime()) / 1000;
const compareTime = options.compareTime || new Date();
let delta = (compareTime.getTime() - dateObj.getTime()) / 1000;
const tense = delta >= 0 ? "past" : "future";
delta = Math.abs(delta);
let timeDesc;
for (let i = 0; i < tests.length; i++) {
if (delta < tests[i]) {
delta = Math.floor(delta);
const timeDesc = localize(
timeDesc = localize(
`ui.components.relative_time.duration.${langKey[i]}`,
"count",
delta
);
return localize(`ui.components.relative_time.${tense}`, "time", timeDesc);
break;
}
delta /= tests[i];
}
delta = Math.floor(delta);
const time = localize(
"ui.components.relative_time.duration.week",
"count",
delta
);
return localize(`ui.components.relative_time.${tense}`, "time", time);
if (timeDesc === undefined) {
delta = Math.floor(delta);
timeDesc = localize(
"ui.components.relative_time.duration.week",
"count",
delta
);
}
return options.includeTense === false
? timeDesc
: localize(`ui.components.relative_time.${tense}`, "time", timeDesc);
}

View File

@ -28,6 +28,17 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
declare global {
// tslint:disable-next-line
interface HASSDomEvents {}
}
export type ValidHassDomEvent = keyof HASSDomEvents;
export interface HASSDomEvent<T> extends Event {
detail: T;
}
/**
* Dispatches a custom event with an optional detail value.
*
@ -35,23 +46,33 @@
* @param {*=} detail Detail value containing event-specific
* payload.
* @param {{ bubbles: (boolean|undefined),
cancelable: (boolean|undefined),
composed: (boolean|undefined) }=}
* options Object specifying options. These may include:
* `bubbles` (boolean, defaults to `true`),
* `cancelable` (boolean, defaults to false), and
* `node` on which to fire the event (HTMLElement, defaults to `this`).
* @return {Event} The new event that was fired.
*/
export const fireEvent = (node, type, detail, options) => {
* cancelable: (boolean|undefined),
* composed: (boolean|undefined) }=}
* options Object specifying options. These may include:
* `bubbles` (boolean, defaults to `true`),
* `cancelable` (boolean, defaults to false), and
* `node` on which to fire the event (HTMLElement, defaults to `this`).
* @return {Event} The new event that was fired.
*/
export const fireEvent = <HassEvent extends ValidHassDomEvent>(
node: HTMLElement,
type: HassEvent,
detail?: HASSDomEvents[HassEvent],
options?: {
bubbles?: boolean;
cancelable?: boolean;
composed?: boolean;
}
) => {
options = options || {};
// @ts-ignore
detail = detail === null || detail === undefined ? {} : detail;
const event = new Event(type, {
bubbles: options.bubbles === undefined ? true : options.bubbles,
cancelable: Boolean(options.cancelable),
composed: options.composed === undefined ? true : options.composed,
});
event.detail = detail;
(event as any).detail = detail;
node.dispatchEvent(event);
return event;
};

View File

@ -2,6 +2,7 @@ import { HassEntity } from "home-assistant-js-websocket";
import canToggleDomain from "./can_toggle_domain";
import computeStateDomain from "./compute_state_domain";
import { HomeAssistant } from "../../types";
import { supportsFeature } from "./supports-feature";
export default function canToggleState(
hass: HomeAssistant,
@ -12,8 +13,7 @@ export default function canToggleState(
return stateObj.state === "on" || stateObj.state === "off";
}
if (domain === "climate") {
// tslint:disable-next-line
return (stateObj.attributes.supported_features! & 4096) !== 0;
return supportsFeature(stateObj, 4096);
}
return canToggleDomain(hass, domain);

View File

@ -1,14 +1,15 @@
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
import { HassEntities } from "home-assistant-js-websocket";
import { DEFAULT_VIEW_ENTITY_ID } from "../const";
import { GroupEntity } from "../../types";
// Return an ordered array of available views
export default function extractViews(entities: HassEntities): HassEntity[] {
const views: HassEntity[] = [];
export default function extractViews(entities: HassEntities): GroupEntity[] {
const views: GroupEntity[] = [];
Object.keys(entities).forEach((entityId) => {
const entity = entities[entityId];
if (entity.attributes.view) {
views.push(entity);
views.push(entity as GroupEntity);
}
});

View File

@ -1,4 +1,5 @@
import { HassEntity } from "home-assistant-js-websocket";
import { supportsFeature } from "./supports-feature";
// Expects classNames to be an object mapping feature-bit -> className
export default function featureClassNames(
@ -9,12 +10,9 @@ export default function featureClassNames(
return "";
}
const features = stateObj.attributes.supported_features;
return Object.keys(classNames)
.map((feature) =>
// tslint:disable-next-line
(features & Number(feature)) !== 0 ? classNames[feature] : ""
supportsFeature(stateObj, Number(feature)) ? classNames[feature] : ""
)
.filter((attr) => attr !== "")
.join(" ");

View File

@ -8,7 +8,7 @@ import { GroupEntity } from "../../types";
export default function getViewEntities(
entities: HassEntities,
view: GroupEntity
) {
): HassEntities {
const viewEntities = {};
view.attributes.entity_id.forEach((entityId) => {

View File

@ -1,18 +1,19 @@
import computeDomain from "./compute_domain";
import { HassEntity, HassEntities } from "home-assistant-js-websocket";
import { HassEntities } from "home-assistant-js-websocket";
import { GroupEntity } from "../../types";
// Split a collection into a list of groups and a 'rest' list of ungrouped
// entities.
// Returns { groups: [], ungrouped: {} }
export default function splitByGroups(entities: HassEntities) {
const groups: HassEntity[] = [];
const groups: GroupEntity[] = [];
const ungrouped: HassEntities = {};
Object.keys(entities).forEach((entityId) => {
const entity = entities[entityId];
if (computeDomain(entityId) === "group") {
groups.push(entity);
groups.push(entity as GroupEntity);
} else {
ungrouped[entityId] = entity;
}

View 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;
};

View 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
View 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();
}

View File

@ -4,6 +4,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../ha-relative-time";
import "./state-badge";
import computeStateName from "../../common/entity/compute_state_name";
import { computeRTL } from "../../common/util/compute_rtl";
class StateInfo extends PolymerElement {
static get template() {
@ -25,10 +26,20 @@ class StateInfo extends PolymerElement {
float: left;
}
:host([rtl]) state-badge {
float: right;
}
.info {
margin-left: 56px;
}
:host([rtl]) .info {
margin-right: 56px;
margin-left: 0;
text-align: right;
}
.name {
@apply --paper-font-common-nowrap;
color: var(--primary-text-color);
@ -87,12 +98,21 @@ class StateInfo extends PolymerElement {
hass: Object,
stateObj: Object,
inDialog: Boolean,
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "computeRTL(hass)",
},
};
}
computeStateName(stateObj) {
return computeStateName(stateObj);
}
computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("state-info", StateInfo);

View File

@ -12,6 +12,7 @@ class HaCard extends PolymerElement {
border-radius: 2px;
transition: all 0.3s ease-out;
background-color: var(--paper-card-background-color, white);
color: var(--primary-text-color);
}
.header {
@apply --paper-font-headline;

View File

@ -123,7 +123,7 @@ class HaCards extends PolymerElement {
</style>
<div id="main">
<template is="dom-if" if="[[cards.badges]]">
<template is="dom-if" if="[[cards.badges.length]]">
<div class="badges">
<template is="dom-if" if="[[cards.demo]]">
<ha-demo-badge></ha-demo-badge>
@ -159,7 +159,6 @@ class HaCards extends PolymerElement {
},
states: Object,
panelVisible: Boolean,
viewVisible: {
type: Boolean,
@ -173,19 +172,11 @@ class HaCards extends PolymerElement {
}
static get observers() {
return [
"updateCards(columns, states, panelVisible, viewVisible, orderedGroupEntities)",
];
return ["updateCards(columns, states, viewVisible, orderedGroupEntities)"];
}
updateCards(
columns,
states,
panelVisible,
viewVisible,
orderedGroupEntities
) {
if (!panelVisible || !viewVisible) {
updateCards(columns, states, viewVisible, orderedGroupEntities) {
if (!viewVisible) {
if (this.$.main.parentNode) {
this.$.main._parentNode = this.$.main.parentNode;
this.$.main.parentNode.removeChild(this.$.main);
@ -200,7 +191,7 @@ class HaCards extends PolymerElement {
timeOut.after(10),
() => {
// Things might have changed since it got scheduled.
if (this.panelVisible && this.viewVisible) {
if (this.viewVisible) {
this.cards = this.computeCards(columns, states, orderedGroupEntities);
}
}

19
src/data/cloud.ts Normal file
View 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
View File

@ -0,0 +1 @@
export const UNAVAILABLE = "unavailable";

7
src/data/light.ts Normal file
View 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
View 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
View 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"];

View File

@ -11,31 +11,30 @@ export const fetchItems = (hass: HomeAssistant): Promise<ShoppingListItem[]> =>
type: "shopping_list/items",
});
export const saveEdit = (
export const updateItem = (
hass: HomeAssistant,
itemId: number,
name: string
item: {
name?: string;
complete?: boolean;
}
): Promise<ShoppingListItem> =>
hass.callApi("POST", "shopping_list/item/" + itemId, {
name,
});
export const completeItem = (
hass: HomeAssistant,
itemId: number,
complete: boolean
): Promise<void> =>
hass.callApi("POST", "shopping_list/item/" + itemId, {
complete,
hass.callWS({
type: "shopping_list/items/update",
item_id: itemId,
...item,
});
export const clearItems = (hass: HomeAssistant): Promise<void> =>
hass.callApi("POST", "shopping_list/clear_completed");
hass.callWS({
type: "shopping_list/items/clear",
});
export const addItem = (
hass: HomeAssistant,
name: string
): Promise<ShoppingListItem> =>
hass.callApi("POST", "shopping_list/item", {
hass.callWS({
type: "shopping_list/items/add",
name,
});

12
src/data/webhook.ts Normal file
View 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",
});

View File

@ -13,6 +13,7 @@ import "../../../components/ha-paper-slider";
import attributeClassNames from "../../../common/entity/attribute_class_names";
import featureClassNames from "../../../common/entity/feature_class_names";
import { supportsFeature } from "../../../common/entity/supports-feature";
import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
@ -385,45 +386,45 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
supportsTemperature(stateObj) {
return (
(stateObj.attributes.supported_features & 1) !== 0 &&
supportsFeature(stateObj, 1) &&
typeof stateObj.attributes.temperature === "number"
);
}
supportsTemperatureRange(stateObj) {
return (
(stateObj.attributes.supported_features & 6) !== 0 &&
supportsFeature(stateObj, 6) &&
(typeof stateObj.attributes.target_temp_low === "number" ||
typeof stateObj.attributes.target_temp_high === "number")
);
}
supportsHumidity(stateObj) {
return (stateObj.attributes.supported_features & 8) !== 0;
return supportsFeature(stateObj, 8);
}
supportsFanMode(stateObj) {
return (stateObj.attributes.supported_features & 64) !== 0;
return supportsFeature(stateObj, 64);
}
supportsOperationMode(stateObj) {
return (stateObj.attributes.supported_features & 128) !== 0;
return supportsFeature(stateObj, 128);
}
supportsSwingMode(stateObj) {
return (stateObj.attributes.supported_features & 512) !== 0;
return supportsFeature(stateObj, 512);
}
supportsAwayMode(stateObj) {
return (stateObj.attributes.supported_features & 1024) !== 0;
return supportsFeature(stateObj, 1024);
}
supportsAuxHeat(stateObj) {
return (stateObj.attributes.supported_features & 2048) !== 0;
return supportsFeature(stateObj, 2048);
}
supportsOn(stateObj) {
return (stateObj.attributes.supported_features & 4096) !== 0;
return supportsFeature(stateObj, 4096);
}
computeClassNames(stateObj) {

View File

@ -8,6 +8,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-attributes";
import { supportsFeature } from "../../../common/entity/supports-feature";
class MoreInfoVacuum extends PolymerElement {
static get template() {
@ -158,57 +159,53 @@ class MoreInfoVacuum extends PolymerElement {
};
}
/* eslint-disable no-bitwise */
supportsPause(stateObj) {
return (stateObj.attributes.supported_features & 4) !== 0;
return supportsFeature(stateObj, 4);
}
supportsStop(stateObj) {
return (stateObj.attributes.supported_features & 8) !== 0;
return supportsFeature(stateObj, 8);
}
supportsReturnHome(stateObj) {
return (stateObj.attributes.supported_features & 16) !== 0;
return supportsFeature(stateObj, 16);
}
supportsFanSpeed(stateObj) {
return (stateObj.attributes.supported_features & 32) !== 0;
return supportsFeature(stateObj, 32);
}
supportsBattery(stateObj) {
return (stateObj.attributes.supported_features & 64) !== 0;
return supportsFeature(stateObj, 64);
}
supportsStatus(stateObj) {
return (stateObj.attributes.supported_features & 128) !== 0;
return supportsFeature(stateObj, 128);
}
supportsLocate(stateObj) {
return (stateObj.attributes.supported_features & 512) !== 0;
return supportsFeature(stateObj, 512);
}
supportsCleanSpot(stateObj) {
return (stateObj.attributes.supported_features & 1024) !== 0;
return supportsFeature(stateObj, 1024);
}
supportsStart(stateObj) {
return (stateObj.attributes.supported_features & 8192) !== 0;
return supportsFeature(stateObj, 8192);
}
supportsCommandBar(stateObj) {
return (
((stateObj.attributes.supported_features & 4) !== 0) |
((stateObj.attributes.supported_features & 8) !== 0) |
((stateObj.attributes.supported_features & 16) !== 0) |
((stateObj.attributes.supported_features & 512) !== 0) |
((stateObj.attributes.supported_features & 1024) !== 0) |
((stateObj.attributes.supported_features & 8192) !== 0)
supportsFeature(stateObj, 4) |
supportsFeature(stateObj, 8) |
supportsFeature(stateObj, 16) |
supportsFeature(stateObj, 512) |
supportsFeature(stateObj, 1024) |
supportsFeature(stateObj, 8192)
);
}
/* eslint-enable no-bitwise */
fanSpeedChanged(fanSpeedIndex) {
var fanSpeedInput;
// Selected Option will transition to '' before transitioning to new value

View File

@ -12,6 +12,7 @@ import "../../../components/ha-water_heater-control";
import "../../../components/ha-paper-slider";
import featureClassNames from "../../../common/entity/feature_class_names";
import { supportsFeature } from "../../../common/entity/supports-feature";
import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
@ -198,17 +199,17 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
supportsTemperature(stateObj) {
return (
(stateObj.attributes.supported_features & 1) !== 0 &&
supportsFeature(stateObj, 1) &&
typeof stateObj.attributes.temperature === "number"
);
}
supportsOperationMode(stateObj) {
return (stateObj.attributes.supported_features & 2) !== 0;
return supportsFeature(stateObj, 2);
}
supportsAwayMode(stateObj) {
return (stateObj.attributes.supported_features & 4) !== 0;
return supportsFeature(stateObj, 4);
}
computeClassNames(stateObj) {

View File

@ -16,6 +16,7 @@ import computeStateDomain from "../../common/entity/compute_state_domain";
import isComponentLoaded from "../../common/config/is_component_loaded";
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
import EventsMixin from "../../mixins/events-mixin";
import { computeRTL } from "../../common/util/compute_rtl";
const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"];
/*
@ -58,6 +59,11 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
:host([domain="camera"]) paper-dialog-scrollable {
margin: 0 -24px -21px;
}
:host([rtl]) app-toolbar {
direction: rtl;
text-align: right;
}
</style>
<app-toolbar>
@ -147,6 +153,11 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
hoursToShow: 24,
},
},
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "_computeRTL(hass)",
},
};
}
@ -190,5 +201,9 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
_gotoSettings() {
this.fire("more-info-page", { page: "settings" });
}
_computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("more-info-controls", MoreInfoControls);

View File

@ -33,7 +33,7 @@
Home Assistant
</div>
<ha-onboarding>Initializing</ha-onboarding>
<ha-onboarding>Initializing</ha-onboarding>
</div>
<% if (!latestBuild) { %>
<script src="/static/custom-elements-es5-adapter.js"></script>

View File

@ -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));
});
}
};

View 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)
);
});
}
};

View File

@ -4,6 +4,7 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { afterNextRender } from "@polymer/polymer/lib/utils/render-status";
import { html as litHtml, LitElement } from "@polymer/lit-element";
import "../home-assistant-main";
import "../ha-init-page";
@ -16,11 +17,13 @@ import TranslationsMixin from "./translations-mixin";
import ThemesMixin from "./themes-mixin";
import MoreInfoMixin from "./more-info-mixin";
import SidebarMixin from "./sidebar-mixin";
import DialogManagerMixin from "./dialog-manager-mixin";
import { dialogManagerMixin } from "./dialog-manager-mixin";
import ConnectionMixin from "./connection-mixin";
import NotificationMixin from "./notification-mixin";
import DisconnectToastMixin from "./disconnect-toast-mixin";
LitElement.prototype.html = litHtml;
const ext = (baseClass, mixins) =>
mixins.reduceRight((base, mixin) => mixin(base), baseClass);
@ -33,7 +36,7 @@ class HomeAssistant extends ext(PolymerElement, [
DisconnectToastMixin,
ConnectionMixin,
NotificationMixin,
DialogManagerMixin,
dialogManagerMixin,
HassBaseMixin,
]) {
static get template() {
@ -91,7 +94,7 @@ class HomeAssistant extends ext(PolymerElement, [
}
computePanelUrl(routeData) {
return (routeData && routeData.panel) || "states";
return (routeData && routeData.panel) || "lovelace";
}
panelUrlChanged(newPanelUrl) {

View File

@ -2,16 +2,15 @@ import "@polymer/app-layout/app-drawer-layout/app-drawer-layout";
import "@polymer/app-layout/app-drawer/app-drawer";
import "@polymer/app-route/app-route";
import "@polymer/iron-media-query/iron-media-query";
import "@polymer/iron-pages/iron-pages";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../util/ha-url-sync";
import "./partial-cards";
import "./partial-panel-resolver";
import EventsMixin from "../mixins/events-mixin";
import NavigateMixin from "../mixins/navigate-mixin";
import { computeRTL } from "../common/util/compute_rtl";
import(/* webpackChunkName: "ha-sidebar" */ "../components/ha-sidebar");
import(/* webpackChunkName: "voice-command-dialog" */ "../dialogs/ha-voice-command-dialog");
@ -30,21 +29,16 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
:host([rtl]) {
direction: rtl;
}
iron-pages,
partial-panel-resolver,
ha-sidebar {
/* allow a light tap highlight on the actual interface elements */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}
iron-pages {
partial-panel-resolver {
height: 100%;
}
</style>
<ha-url-sync hass="[[hass]]"></ha-url-sync>
<app-route
route="{{route}}"
pattern="/states"
tail="{{statesRouteTail}}"
></app-route>
<ha-voice-command-dialog
hass="[[hass]]"
id="voiceDialog"
@ -72,29 +66,12 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
></ha-sidebar>
</app-drawer>
<iron-pages
attr-for-selected="id"
fallback-selection="panel-resolver"
selected="[[hass.panelUrl]]"
selected-attribute="panel-visible"
>
<partial-cards
id="states"
narrow="[[narrow]]"
hass="[[hass]]"
show-menu="[[dockedSidebar]]"
route="[[statesRouteTail]]"
show-tabs=""
></partial-cards>
<partial-panel-resolver
id="panel-resolver"
narrow="[[narrow]]"
hass="[[hass]]"
route="[[route]]"
show-menu="[[dockedSidebar]]"
></partial-panel-resolver>
</iron-pages>
<partial-panel-resolver
narrow="[[narrow]]"
hass="[[hass]]"
route="[[route]]"
show-menu="[[dockedSidebar]]"
></partial-panel-resolver>
</app-drawer-layout>
`;
}
@ -107,7 +84,6 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
type: Object,
observer: "_routeChanged",
},
statesRouteTail: Object,
dockedSidebar: {
type: Boolean,
computed: "computeDockedSidebar(hass)",
@ -115,14 +91,14 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "computeRTL(hass)",
computed: "_computeRTL(hass)",
},
};
}
ready() {
super.ready();
this._defaultPage = localStorage.defaultPage || "states";
this._defaultPage = localStorage.defaultPage || "lovelace";
this.addEventListener("hass-open-menu", () => this.handleOpenMenu());
this.addEventListener("hass-close-menu", () => this.handleCloseMenu());
this.addEventListener("hass-start-voice", (ev) =>
@ -159,7 +135,7 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
connectedCallback() {
super.connectedCallback();
if (document.location.pathname === "/") {
this.navigate(`/${localStorage.defaultPage || "states"}`, true);
this.navigate(`/${localStorage.defaultPage || "lovelace"}`, true);
}
}
@ -175,12 +151,8 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
return NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1;
}
computeRTL(hass) {
var lang = hass.selectedLanguage || hass.language;
if (hass.translationMetadata.translations[lang]) {
return hass.translationMetadata.translations[lang].isRTL || false;
}
return false;
_computeRTL(hass) {
return computeRTL(hass);
}
}

View File

@ -54,6 +54,10 @@ function ensureLoaded(panel) {
imported = import(/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace");
break;
case "states":
imported = import(/* webpackChunkName: "panel-states" */ "../panels/states/ha-panel-states");
break;
case "history":
imported = import(/* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history");
break;

View File

@ -24,6 +24,10 @@ export class CloudAlexaPref extends LitElement {
}
protected render(): TemplateResult {
if (!this.cloudStatus) {
return html``;
}
const enabled = this.cloudStatus!.prefs.alexa_enabled;
return html`

View File

@ -25,7 +25,11 @@ export class CloudGooglePref extends LitElement {
}
protected render(): TemplateResult {
const { google_enabled, google_allow_unlock } = this.cloudStatus!.prefs;
if (!this.cloudStatus) {
return html``;
}
const { google_enabled, google_allow_unlock } = this.cloudStatus.prefs;
return html`
${this.renderStyle()}

View 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);

View 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);

View File

@ -10,15 +10,19 @@ import "../../../layouts/hass-subpage";
import "../../../resources/ha-style";
import "../ha-config-section";
import "./cloud-webhooks";
import formatDateTime from "../../../common/datetime/format_date_time";
import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import { fireEvent } from "../../../common/dom/fire_event";
import { fetchSubscriptionInfo } from "./data";
import "./cloud-alexa-pref";
import "./cloud-google-pref";
let registeredWebhookDialog = false;
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
@ -129,6 +133,11 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
hass="[[hass]]"
cloud-status="[[cloudStatus]]"
></cloud-google-pref>
<cloud-webhooks
hass="[[hass]]"
cloud-status="[[cloudStatus]]"
></cloud-webhooks>
</ha-config-section>
</div>
</hass-subpage>
@ -152,9 +161,26 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
this._fetchSubscriptionInfo();
}
connectedCallback() {
super.connectedCallback();
if (!registeredWebhookDialog) {
registeredWebhookDialog = true;
fireEvent(this, "register-dialog", {
dialogShowEvent: "manage-cloud-webhook",
dialogTag: "cloud-webhook-manage-dialog",
dialogImport: () => import("./cloud-webhook-manage-dialog"),
});
}
}
async _fetchSubscriptionInfo() {
this._subscription = await fetchSubscriptionInfo(this.hass);
if (this._subscription.provider && this.cloudStatus.cloud !== "connected") {
if (
this._subscription.provider &&
this.cloudStatus &&
this.cloudStatus.cloud !== "connected"
) {
this.fire("ha-refresh-cloud-status");
}
}

View File

@ -1,3 +1,6 @@
import { CloudWebhook } from "../../../data/cloud";
import { Webhook } from "../../../data/webhook";
export interface EntityFilter {
include_domains: string[];
include_entities: string[];
@ -19,6 +22,7 @@ export type CloudStatusLoggedIn = CloudStatusBase & {
google_enabled: boolean;
alexa_enabled: boolean;
google_allow_unlock: boolean;
cloudhooks: { [webhookId: string]: CloudWebhook };
};
};
@ -27,3 +31,9 @@ export type CloudStatus = CloudStatusBase | CloudStatusLoggedIn;
export interface SubscriptionInfo {
human_description: string;
}
export interface WebhookDialogParams {
webhook: Webhook;
cloudhook: CloudWebhook;
disableHook: () => void;
}

View File

@ -24,7 +24,18 @@ export default class NumericStateTrigger extends Component {
/* eslint-disable camelcase */
render({ trigger, hass, localize }) {
const { value_template, entity_id, below, above } = trigger;
let trgFor = trigger.for;
if (trgFor && (trgFor.hours || trgFor.minutes || trgFor.seconds)) {
// If the trigger was defined using the yaml dict syntax, convert it to
// the equivalent string format
let { hours = 0, minutes = 0, seconds = 0 } = trgFor;
hours = hours.toString();
minutes = minutes.toString().padStart(2, "0");
seconds = seconds.toString().padStart(2, "0");
trgFor = `${hours}:${minutes}:${seconds}`;
}
return (
<div>
<ha-entity-picker
@ -57,6 +68,14 @@ export default class NumericStateTrigger extends Component {
value={value_template}
onvalue-changed={this.onChange}
/>
<paper-input
label={localize(
"ui.panel.config.automation.editor.triggers.type.state.for"
)}
name="for"
value={trgFor}
onvalue-changed={this.onChange}
/>
</div>
);
}

View File

@ -164,7 +164,7 @@ class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) {
</template>
</p>
<p>
<a href='/lovelace'>Try out the new Lovelace UI (experimental)</a>
<a href='/states'>Go back to the old states page</a>
<div id="love" style="cursor:pointer;" on-click="_toggleDefaultPage">[[_defaultPageText()]]</div
</p>
</div>
@ -364,15 +364,15 @@ class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) {
_defaultPageText() {
return `>> ${
localStorage.defaultPage === "lovelace" ? "Remove" : "Set"
} lovelace as default page on this device <<`;
localStorage.defaultPage === "states" ? "Remove" : "Set"
} the old states as default page on this device <<`;
}
_toggleDefaultPage() {
if (localStorage.defaultPage === "lovelace") {
if (localStorage.defaultPage === "states") {
delete localStorage.defaultPage;
} else {
localStorage.defaultPage = "lovelace";
localStorage.defaultPage = "states";
}
this.$.love.innerText = this._defaultPageText();
}

View File

@ -1,18 +1,18 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../layouts/partial-cards";
import "../states/ha-panel-states";
class HaPanelKiosk extends PolymerElement {
static get template() {
return html`
<partial-cards
<ha-panel-states
id="kiosk-states"
hass="[[hass]]"
show-menu
route="[[route]]"
panel-visible
></partial-cards>
></ha-panel-states>
`;
}

View File

@ -209,8 +209,8 @@ class HuiAlarmPanelCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
_computeHeader(localize, stateObj) {
if (!stateObj) return "";
return this._config.title
? this._config.title
return this._config.name
? this._config.name
: this._label(localize, stateObj.state);
}

View File

@ -1,7 +1,8 @@
import createCardElement from "../common/create-card-element";
import { computeCardSize } from "../common/compute-card-size";
import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
interface Condition {
entity: string;
@ -9,8 +10,8 @@ interface Condition {
state_not?: string;
}
interface Config extends LovelaceConfig {
card: LovelaceConfig;
interface Config extends LovelaceCardConfig {
card: LovelaceCardConfig;
conditions: Condition[];
}

View File

@ -14,8 +14,9 @@ import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../types";
import { EntityConfig, EntityRow } from "../entity-rows/types";
import { LovelaceCard, LovelaceConfig, LovelaceCardEditor } from "../types";
import processConfigEntities from "../common/process-config-entities";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { processConfigEntities } from "../common/process-config-entities";
import createRowElement from "../common/create-row-element";
import computeDomain from "../../../common/entity/compute_domain";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
@ -29,7 +30,7 @@ export interface ConfigEntity extends EntityConfig {
url?: string;
}
export interface Config extends LovelaceConfig {
export interface Config extends LovelaceCardConfig {
show_header_toggle?: boolean;
title?: string;
entities: ConfigEntity[];
@ -42,6 +43,11 @@ class HuiEntitiesCard extends hassLocalizeLitMixin(LitElement)
await import("../editor/config-elements/hui-entities-card-editor");
return document.createElement("hui-entities-card-editor");
}
public static getStubConfig(): object {
return { entities: [] };
}
protected _hass?: HomeAssistant;
protected _config?: Config;
protected _configEntities?: ConfigEntity[];

View File

@ -10,7 +10,6 @@ import { styleMap } from "lit-html/directives/styleMap";
import "../../../components/ha-card";
import toggleEntity from "../common/entity/toggle-entity";
import isValidEntityId from "../../../common/entity/valid_entity_id";
import stateIcon from "../../../common/entity/state_icon";
import computeStateDomain from "../../../common/entity/compute_state_domain";
@ -18,19 +17,18 @@ import computeStateName from "../../../common/entity/compute_state_name";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { HomeAssistant, LightEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { longPress } from "../common/directives/long-press-directive";
import { fireEvent } from "../../../common/dom/fire_event";
import { handleClick } from "../common/handle-click";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
icon?: string;
theme?: string;
tap_action?: "toggle" | "call-service" | "more-info";
hold_action?: "toggle" | "call-service" | "more-info";
service?: string;
service_data?: object;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
}
class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
@ -81,8 +79,8 @@ class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
return html`
${this.renderStyle()}
<ha-card
@ha-click="${() => this.handleClick(false)}"
@ha-hold="${() => this.handleClick(true)}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
>
${
@ -186,34 +184,12 @@ class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
}
private handleClick(hold: boolean): void {
const config = this._config;
if (!config) {
return;
}
const stateObj = this.hass!.states[config.entity];
if (!stateObj) {
return;
}
const entityId = stateObj.entity_id;
const action = hold ? config.hold_action : config.tap_action || "more-info";
switch (action) {
case "toggle":
toggleEntity(this.hass, entityId);
break;
case "call-service":
if (!config.service) {
return;
}
const [domain, service] = config.service.split(".", 2);
const serviceData = { entity_id: entityId, ...config.service_data };
this.hass!.callService(domain, service, serviceData);
break;
case "more-info":
fireEvent(this, "hass-more-info", { entityId });
break;
default:
}
private _handleTap() {
handleClick(this, this.hass!, this._config!, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true);
}
}

View File

@ -1,7 +1,7 @@
import { PolymerElement } from "@polymer/polymer/polymer-element";
import createCardElement from "../common/create-card-element";
import processConfigEntities from "../common/process-config-entities";
import { processConfigEntities } from "../common/process-config-entities";
function getEntities(hass, filterState, entities) {
return entities.filter((entityConf) => {

View File

@ -1,11 +1,12 @@
import { html, LitElement } from "@polymer/lit-element";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { TemplateResult } from "lit-html";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
error: string;
origConfig: LovelaceConfig;
origConfig: LovelaceCardConfig;
}
class HuiErrorCard extends LitElement implements LovelaceCard {
@ -50,7 +51,7 @@ class HuiErrorCard extends LitElement implements LovelaceCard {
`;
}
private _toStr(config: LovelaceConfig): string {
private _toStr(config: LovelaceCardConfig): string {
return JSON.stringify(config, null, 2);
}
}

View File

@ -6,20 +6,22 @@ import {
} from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import isValidEntityId from "../../../common/entity/valid_entity_id";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import computeStateName from "../../../common/entity/compute_state_name";
import "../../../components/ha-card";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
entity: string;
title?: string;
unit_of_measurement?: string;
name?: string;
unit?: string;
min?: number;
max?: number;
severity?: object;
@ -87,12 +89,14 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
<div id="percent">
${stateObj.state}
${
this._config.unit_of_measurement ||
this._config.unit ||
stateObj.attributes.unit_of_measurement ||
""
}
</div>
<div id="title">${this._config.title}</div>
<div id="name">
${this._config.name || computeStateName(stateObj)}
</div>
</div>
</div>
`
@ -210,7 +214,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
.gauge-data #percent {
font-size: calc(var(--base-unit) * 0.55);
}
.gauge-data #title {
.gauge-data #name {
padding-top: calc(var(--base-unit) * 0.15);
font-size: calc(var(--base-unit) * 0.3);
}

View File

@ -7,31 +7,29 @@ import {
import { TemplateResult } from "lit-html";
import { classMap } from "lit-html/directives/classMap";
import { fireEvent } from "../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceConfig, LovelaceCardEditor } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { longPress } from "../common/directives/long-press-directive";
import { EntityConfig } from "../entity-rows/types";
import { processConfigEntities } from "../common/process-config-entities";
import computeStateDisplay from "../../../common/entity/compute_state_display";
import computeStateName from "../../../common/entity/compute_state_name";
import processConfigEntities from "../common/process-config-entities";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import toggleEntity from "../common/entity/toggle-entity";
import "../../../components/entity/state-badge";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import { handleClick } from "../common/handle-click";
export interface ConfigEntity extends EntityConfig {
tap_action?: "toggle" | "call-service" | "more-info";
hold_action?: "toggle" | "call-service" | "more-info";
service?: string;
service_data?: object;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
}
export interface Config extends LovelaceConfig {
export interface Config extends LovelaceCardConfig {
show_name?: boolean;
show_state?: boolean;
title?: string;
@ -46,6 +44,9 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
await import("../editor/config-elements/hui-glance-card-editor");
return document.createElement("hui-glance-card-editor");
}
public static getStubConfig(): object {
return { entities: [] };
}
public hass?: HomeAssistant;
private _config?: Config;
@ -67,13 +68,16 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
public setConfig(config: Config): void {
this._config = { theme: "default", ...config };
const entities = processConfigEntities(config.entities);
const entities = processConfigEntities<ConfigEntity>(config.entities);
for (const entity of entities) {
if (
(entity.tap_action === "call-service" ||
entity.hold_action === "call-service") &&
!entity.service
(entity.tap_action &&
entity.tap_action.action === "call-service" &&
!entity.tap_action.service) ||
(entity.hold_action &&
entity.hold_action.action === "call-service" &&
!entity.hold_action.service)
) {
throw new Error(
'Missing required property "service" when tap_action or hold_action is call-service'
@ -199,8 +203,8 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
<div
class="entity"
.entityConf="${entityConf}"
@ha-click="${(ev) => this.handleClick(ev, false)}"
@ha-hold="${(ev) => this.handleClick(ev, true)}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
>
${
@ -239,24 +243,14 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
`;
}
private handleClick(ev: MouseEvent, hold: boolean): void {
private _handleTap(ev: MouseEvent) {
const config = (ev.currentTarget as any).entityConf as ConfigEntity;
const entityId = config.entity;
const action = hold ? config.hold_action : config.tap_action || "more-info";
switch (action) {
case "toggle":
toggleEntity(this.hass, entityId);
break;
case "call-service":
const [domain, service] = config.service!.split(".", 2);
const serviceData = { entity_id: entityId, ...config.service_data };
this.hass!.callService(domain, service, serviceData);
break;
case "more-info":
fireEvent(this, "hass-more-info", { entityId });
break;
default:
}
handleClick(this, this.hass!, config, false);
}
private _handleHold(ev: MouseEvent) {
const config = (ev.currentTarget as any).entityConf as ConfigEntity;
handleClick(this, this.hass!, config, true);
}
}

View File

@ -5,7 +5,7 @@ import "../../../components/ha-card";
import "../../../components/state-history-charts";
import "../../../data/ha-state-history-data";
import processConfigEntities from "../common/process-config-entities";
import { processConfigEntities } from "../common/process-config-entities";
class HuiHistoryGraphCard extends PolymerElement {
static get template() {

View File

@ -2,11 +2,12 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "../../../components/ha-card";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { TemplateResult } from "lit-html";
import { styleMap } from "lit-html/directives/styleMap";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
aspect_ratio?: string;
title?: string;
url: string;

View File

@ -34,6 +34,7 @@ export default class LegacyWrapperCard extends HTMLElement {
this._ensureElement(this._tag);
this.lastChild.hass = hass;
this.lastChild.stateObj = hass.states[entityId];
this.lastChild.config = this._config;
} else {
this._ensureElement("HUI-ERROR-CARD");
this.lastChild.setConfig(

View File

@ -12,7 +12,8 @@ import { jQuery } from "../../../resources/jquery";
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
import { HomeAssistant, LightEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { longPress } from "../common/directives/long-press-directive";
import stateIcon from "../../../common/entity/state_icon";
@ -37,7 +38,7 @@ const lightConfig = {
showTooltip: false,
};
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
theme?: string;

View File

@ -6,10 +6,11 @@ import Leaflet from "leaflet";
import "../../map/ha-entity-marker";
import setupLeafletMap from "../../../common/dom/setup-leaflet-map";
import processConfigEntities from "../common/process-config-entities";
import { processConfigEntities } from "../common/process-config-entities";
import computeStateDomain from "../../../common/entity/compute_state_domain";
import computeStateName from "../../../common/entity/compute_state_name";
import debounce from "../../../common/util/debounce";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
@ -97,7 +98,15 @@ class HuiMapCard extends PolymerElement {
return;
}
this.$.root.style.paddingTop = this._config.aspect_ratio || "100%";
const ratio = parseAspectRatio(this._config.aspect_ratio);
if (ratio && ratio.w > 0 && ratio.h > 0) {
this.$.root.style.paddingBottom = `${((100 * ratio.h) / ratio.w).toFixed(
2
)}%`;
} else {
this.$.root.style.paddingBottom = "100%";
}
}
setConfig(config) {
@ -110,8 +119,13 @@ class HuiMapCard extends PolymerElement {
}
getCardSize() {
let ar = this._config.aspect_ratio || "100%";
ar = ar.substr(0, ar.length - 1);
const ratio = parseAspectRatio(this._config.aspect_ratio);
let ar;
if (ratio && ratio.w > 0 && ratio.h > 0) {
ar = `${((100 * ratio.h) / ratio.w).toFixed(2)}`;
} else {
ar = "100";
}
return 1 + Math.floor(ar / 25) || 3;
}

View File

@ -4,10 +4,11 @@ import { classMap } from "lit-html/directives/classMap";
import "../../../components/ha-card";
import "../../../components/ha-markdown";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { TemplateResult } from "lit-html";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
content: string;
title?: string;
}

View File

@ -2,17 +2,18 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "../../../components/ha-card";
import { LovelaceCard, LovelaceConfig } from "../types";
import { navigate } from "../../../common/navigate";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { TemplateResult } from "lit-html";
import { classMap } from "lit-html/directives/classMap";
import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
image?: string;
navigation_path?: string;
service?: string;
service_data?: object;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
}
export class HuiPictureCard extends LitElement implements LovelaceCard {
@ -45,11 +46,13 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
return html`
${this.renderStyle()}
<ha-card
@click="${this.handleClick}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
class="${
classMap({
clickable: Boolean(
this._config.navigation_path || this._config.service
this._config.tap_action || this._config.hold_action
),
})
}"
@ -76,14 +79,12 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
`;
}
private handleClick(): void {
if (this._config!.navigation_path) {
navigate(this, this._config!.navigation_path!);
}
if (this._config!.service) {
const [domain, service] = this._config!.service!.split(".", 2);
this.hass!.callService(domain, service, this._config!.service_data);
}
private _handleTap() {
handleClick(this, this.hass!, this._config!, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true);
}
}

View File

@ -3,13 +3,18 @@ import { TemplateResult } from "lit-html";
import createHuiElement from "../common/create-hui-element";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
title?: string;
image: string;
image?: string;
camera_image?: string;
state_image?: {};
aspect_ratio?: string;
entity?: string;
elements: LovelaceElementConfig[];
}
@ -38,7 +43,10 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
public setConfig(config: Config): void {
if (!config) {
throw new Error("Invalid Configuration");
} else if (!config.image) {
} else if (
!(config.image || config.camera_image || config.state_image) ||
(config.state_image && !config.entity)
) {
throw new Error("Invalid Configuration: image required");
} else if (!Array.isArray(config.elements)) {
throw new Error("Invalid Configuration: elements required");
@ -55,13 +63,19 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
return html`
${this.renderStyle()}
<ha-card .header="${this._config.title}">
<div id="root">
<img src="${this._config.image}" /> ${
this._config.elements.map((elementConfig: LovelaceElementConfig) =>
this._createHuiElement(elementConfig)
)
}
</div>
<hui-image
.hass="${this._hass}"
.image="${this._config.image}"
.stateImage="${this._config.state_image}"
.cameraImage="${this._config.camera_image}"
.entity="${this._config.entity}"
.aspectRatio="${this._config.aspect_ratio}"
></hui-image>
${
this._config.elements.map((elementConfig: LovelaceElementConfig) =>
this._createHuiElement(elementConfig)
)
}
</ha-card>
`;
}
@ -71,14 +85,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
<style>
ha-card {
overflow: hidden;
}
#root {
position: relative;
overflow: hidden;
}
#root img {
display: block;
width: 100%;
}
.element {
position: absolute;

View File

@ -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);

View 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);

View File

@ -3,36 +3,37 @@ import { classMap } from "lit-html/directives/classMap";
import { TemplateResult } from "lit-html";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { fireEvent } from "../../../common/dom/fire_event";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { EntityConfig } from "../entity-rows/types";
import { navigate } from "../../../common/navigate";
import { HomeAssistant } from "../../../types";
import { longPress } from "../common/directives/long-press-directive";
import { processConfigEntities } from "../common/process-config-entities";
import computeStateDisplay from "../../../common/entity/compute_state_display";
import computeStateName from "../../../common/entity/compute_state_name";
import processConfigEntities from "../common/process-config-entities";
import computeDomain from "../../../common/entity/compute_domain";
import stateIcon from "../../../common/entity/state_icon";
import toggleEntity from "../common/entity/toggle-entity";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../components/hui-image";
import { handleClick } from "../common/handle-click";
import { fireEvent } from "../../../common/dom/fire_event";
import { toggleEntity } from "../common/entity/toggle-entity";
const STATES_OFF = new Set(["closed", "locked", "not_home", "off"]);
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
entities: EntityConfig[];
title?: string;
navigation_path?: string;
image?: string;
camera_image?: string;
state_image?: {};
aspect_ratio?: string;
entity?: string;
force_dialog?: boolean;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
}
class HuiPictureGlanceCard extends hassLocalizeLitMixin(LitElement)
@ -87,19 +88,22 @@ class HuiPictureGlanceCard extends hassLocalizeLitMixin(LitElement)
return html``;
}
const isClickable =
this._config.navigation_path || this._config.camera_image;
return html`
${this.renderStyle()}
<ha-card>
<hui-image
class="${
classMap({
clickable: Boolean(isClickable),
clickable: Boolean(
this._config.tap_action ||
this._config.hold_action ||
this._config.camera_image
),
})
}"
@click="${this._handleImageClick}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
.hass="${this.hass}"
.image="${this._config.image}"
.stateImage="${this._config.state_image}"
@ -167,22 +171,20 @@ class HuiPictureGlanceCard extends hassLocalizeLitMixin(LitElement)
`;
}
private _handleTap() {
handleClick(this, this.hass!, this._config!, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true);
}
private _openDialog(ev: MouseEvent): void {
fireEvent(this, "hass-more-info", { entityId: (ev.target as any).entity });
}
private _callService(ev: MouseEvent): void {
toggleEntity(this.hass, (ev.target as any).entity);
}
private _handleImageClick(): void {
if (this._config!.navigation_path) {
navigate(this, this._config!.navigation_path!);
} else if (this._config!.camera_image) {
fireEvent(this, "hass-more-info", {
entityId: this._config!.camera_image,
});
}
toggleEntity(this.hass!, (ev.target as any).entity);
}
private renderStyle(): TemplateResult {

View File

@ -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);

View 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);

View File

@ -9,17 +9,17 @@ import "../../../components/ha-icon";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import {
fetchItems,
completeItem,
saveEdit,
updateItem,
ShoppingListItem,
clearItems,
addItem,
} from "../../../data/shopping-list";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
title?: string;
}
@ -256,15 +256,15 @@ class HuiShoppingListCard extends hassLocalizeLitMixin(LitElement)
}
private _completeItem(ev): void {
completeItem(this.hass!, ev.target.itemId, ev.target.checked).catch(() =>
this._fetchData()
);
updateItem(this.hass!, ev.target.itemId, {
complete: ev.target.checked,
}).catch(() => this._fetchData());
}
private _saveEdit(ev): void {
saveEdit(this.hass!, ev.target.itemId, ev.target.value).catch(() =>
this._fetchData()
);
updateItem(this.hass!, ev.target.itemId, {
name: ev.target.value,
}).catch(() => this._fetchData());
ev.target.blur();
}

View File

@ -3,11 +3,12 @@ import { TemplateResult } from "lit-html";
import createCardElement from "../common/create-card-element";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
interface Config extends LovelaceConfig {
cards: LovelaceConfig[];
interface Config extends LovelaceCardConfig {
cards: LovelaceCardConfig[];
}
export abstract class HuiStackCard extends LitElement implements LovelaceCard {

View File

@ -15,7 +15,8 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
import { HomeAssistant, ClimateEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import "../../../components/ha-card";
import "../../../components/ha-icon";
@ -43,9 +44,10 @@ const modeIcons = {
idle: "hass:power-sleep",
};
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
entity: string;
theme?: string;
name?: string;
}
function formatTemp(temps: string[]): string {
@ -96,7 +98,8 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
<div id="root">
<div id="thermostat"></div>
<div id="tooltip">
<div class="title">${computeStateName(stateObj)}</div>
<div class="title">${this._config.name ||
computeStateName(stateObj)}</div>
<div class="current-temperature">
<span class="current-temperature-text">
${stateObj.attributes.current_temperature}

View File

@ -20,7 +20,7 @@ export const computeTooltip = (
: config.entity;
}
switch (config.tap_action) {
switch (config.tap_action && config.tap_action.action) {
case "navigate":
tooltip = `Navigate to ${config.navigation_path}`;
break;

View File

@ -1,18 +1,18 @@
import { fireEvent } from "../../../common/dom/fire_event";
import "../cards/hui-alarm-panel-card";
import "../cards/hui-conditional-card.ts";
import "../cards/hui-entities-card.ts";
import "../cards/hui-entity-button-card.ts";
import "../cards/hui-conditional-card";
import "../cards/hui-entities-card";
import "../cards/hui-entity-button-card";
import "../cards/hui-entity-filter-card";
import "../cards/hui-error-card.ts";
import "../cards/hui-glance-card.ts";
import "../cards/hui-error-card";
import "../cards/hui-glance-card";
import "../cards/hui-history-graph-card";
import "../cards/hui-horizontal-stack-card.ts";
import "../cards/hui-iframe-card.ts";
import "../cards/hui-horizontal-stack-card";
import "../cards/hui-iframe-card";
import "../cards/hui-light-card";
import "../cards/hui-map-card";
import "../cards/hui-markdown-card.ts";
import "../cards/hui-markdown-card";
import "../cards/hui-media-control-card";
import "../cards/hui-picture-card";
import "../cards/hui-picture-elements-card";
@ -20,9 +20,9 @@ import "../cards/hui-picture-entity-card";
import "../cards/hui-picture-glance-card";
import "../cards/hui-plant-status-card";
import "../cards/hui-sensor-card";
import "../cards/hui-vertical-stack-card.ts";
import "../cards/hui-vertical-stack-card";
import "../cards/hui-shopping-list-card";
import "../cards/hui-thermostat-card.ts";
import "../cards/hui-thermostat-card";
import "../cards/hui-weather-forecast-card";
import "../cards/hui-gauge-card";

View File

@ -10,6 +10,7 @@ import "../entity-rows/hui-lock-entity-row";
import "../entity-rows/hui-media-player-entity-row";
import "../entity-rows/hui-scene-entity-row";
import "../entity-rows/hui-script-entity-row";
import "../entity-rows/hui-sensor-entity-row";
import "../entity-rows/hui-text-entity-row";
import "../entity-rows/hui-timer-entity-row";
import "../entity-rows/hui-toggle-entity-row";
@ -28,6 +29,7 @@ const SPECIAL_TYPES = new Set([
"weblink",
]);
const DOMAIN_TO_ELEMENT_TYPE = {
alert: "toggle",
automation: "toggle",
climate: "climate",
cover: "cover",
@ -42,6 +44,7 @@ const DOMAIN_TO_ELEMENT_TYPE = {
lock: "lock",
scene: "scene",
script: "script",
sensor: "sensor",
timer: "timer",
switch: "toggle",
vacuum: "toggle",

View File

@ -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",
});

View File

@ -19,6 +19,8 @@ class LongPress extends HTMLElement implements LongPress {
protected ripple: any;
protected timer: number | undefined;
protected held: boolean;
protected cooldownStart: boolean;
protected cooldownEnd: boolean;
constructor() {
super();
@ -26,6 +28,8 @@ class LongPress extends HTMLElement implements LongPress {
this.ripple = document.createElement("mwc-ripple");
this.timer = undefined;
this.held = false;
this.cooldownStart = false;
this.cooldownEnd = false;
}
public connectedCallback() {
@ -41,7 +45,8 @@ class LongPress extends HTMLElement implements LongPress {
this.ripple.primary = true;
[
isTouch ? "touchcancel" : "mouseout",
"touchcancel",
"mouseout",
"mouseup",
"touchmove",
"mousewheel",
@ -80,6 +85,9 @@ class LongPress extends HTMLElement implements LongPress {
});
const clickStart = (ev: Event) => {
if (this.cooldownStart) {
return;
}
this.held = false;
let x;
let y;
@ -94,30 +102,35 @@ class LongPress extends HTMLElement implements LongPress {
this.startAnimation(x, y);
this.held = true;
}, this.holdTime);
this.cooldownStart = true;
window.setTimeout(() => (this.cooldownStart = false), 100);
};
const clickEnd = () => {
clearTimeout(this.timer);
this.stopAnimation();
if (isTouch && this.timer === undefined) {
const clickEnd = (ev: Event) => {
if (
this.cooldownEnd ||
(ev instanceof TouchEvent && this.timer === undefined)
) {
return;
}
clearTimeout(this.timer);
this.stopAnimation();
this.timer = undefined;
if (this.held) {
element.dispatchEvent(new Event("ha-hold"));
} else {
element.dispatchEvent(new Event("ha-click"));
}
this.cooldownEnd = true;
window.setTimeout(() => (this.cooldownEnd = false), 100);
};
if (isTouch) {
element.addEventListener("touchstart", clickStart, { passive: true });
element.addEventListener("touchend", clickEnd);
element.addEventListener("touchcancel", clickEnd);
} else {
element.addEventListener("mousedown", clickStart, { passive: true });
element.addEventListener("click", clickEnd);
}
element.addEventListener("touchstart", clickStart, { passive: true });
element.addEventListener("touchend", clickEnd);
element.addEventListener("touchcancel", clickEnd);
element.addEventListener("mousedown", clickStart, { passive: true });
element.addEventListener("click", clickEnd);
}
private startAnimation(x: number, y: number) {

View File

@ -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);
}

View 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);
};

View File

@ -1,7 +1,12 @@
import { STATES_OFF } from "../../../../common/const";
import computeDomain from "../../../../common/entity/compute_domain";
import { STATES_OFF } from "../../../../common/const";
import { HomeAssistant } from "../../../../types";
export default function turnOnOffEntities(hass, entityIds, turnOn = true) {
export const turnOnOffEntities = (
hass: HomeAssistant,
entityIds: string[],
turnOn = true
): void => {
const domainsToCall = {};
entityIds.forEach((entityId) => {
if (STATES_OFF.includes(hass.states[entityId].state) === turnOn) {
@ -10,7 +15,9 @@ export default function turnOnOffEntities(hass, entityIds, turnOn = true) {
? stateDomain
: "homeassistant";
if (!(serviceDomain in domainsToCall)) domainsToCall[serviceDomain] = [];
if (!(serviceDomain in domainsToCall)) {
domainsToCall[serviceDomain] = [];
}
domainsToCall[serviceDomain].push(entityId);
}
});
@ -31,4 +38,4 @@ export default function turnOnOffEntities(hass, entityIds, turnOn = true) {
const entities = domainsToCall[domain];
hass.callService(domain, service, { entity_id: entities });
});
}
};

View File

@ -1,6 +1,11 @@
import computeDomain from "../../../../common/entity/compute_domain";
import { HomeAssistant } from "../../../../types";
export default function turnOnOffEntity(hass, entityId, turnOn = true) {
export const turnOnOffEntity = (
hass: HomeAssistant,
entityId: string,
turnOn = true
): Promise<void> => {
const stateDomain = computeDomain(entityId);
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;
@ -16,5 +21,5 @@ export default function turnOnOffEntity(hass, entityId, turnOn = true) {
service = turnOn ? "turn_on" : "turn_off";
}
hass.callService(serviceDomain, service, { entity_id: entityId });
}
return hass.callService(serviceDomain, service, { entity_id: entityId });
};

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