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);
}
}
diff --git a/src/panels/lovelace/cards/hui-history-graph-card.js b/src/panels/lovelace/cards/hui-history-graph-card.js
index 5a50a8c0d1..9def759e5a 100644
--- a/src/panels/lovelace/cards/hui-history-graph-card.js
+++ b/src/panels/lovelace/cards/hui-history-graph-card.js
@@ -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() {
diff --git a/src/panels/lovelace/cards/hui-iframe-card.ts b/src/panels/lovelace/cards/hui-iframe-card.ts
index 822f60f870..93a643fc53 100644
--- a/src/panels/lovelace/cards/hui-iframe-card.ts
+++ b/src/panels/lovelace/cards/hui-iframe-card.ts
@@ -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;
diff --git a/src/panels/lovelace/cards/hui-legacy-wrapper-card.js b/src/panels/lovelace/cards/hui-legacy-wrapper-card.js
index b9957c41bb..cdd4de0554 100644
--- a/src/panels/lovelace/cards/hui-legacy-wrapper-card.js
+++ b/src/panels/lovelace/cards/hui-legacy-wrapper-card.js
@@ -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(
diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts
index 1bbf158271..11204aa5fc 100644
--- a/src/panels/lovelace/cards/hui-light-card.ts
+++ b/src/panels/lovelace/cards/hui-light-card.ts
@@ -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;
diff --git a/src/panels/lovelace/cards/hui-map-card.js b/src/panels/lovelace/cards/hui-map-card.js
index d49686e2c4..cb52efe46d 100644
--- a/src/panels/lovelace/cards/hui-map-card.js
+++ b/src/panels/lovelace/cards/hui-map-card.js
@@ -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;
}
diff --git a/src/panels/lovelace/cards/hui-markdown-card.ts b/src/panels/lovelace/cards/hui-markdown-card.ts
index 5cedf5873b..e30fadaa40 100644
--- a/src/panels/lovelace/cards/hui-markdown-card.ts
+++ b/src/panels/lovelace/cards/hui-markdown-card.ts
@@ -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;
}
diff --git a/src/panels/lovelace/cards/hui-picture-card.ts b/src/panels/lovelace/cards/hui-picture-card.ts
index e988c0ea97..43ddd1a14e 100644
--- a/src/panels/lovelace/cards/hui-picture-card.ts
+++ b/src/panels/lovelace/cards/hui-picture-card.ts
@@ -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()}
-
-
${
- this._config.elements.map((elementConfig: LovelaceElementConfig) =>
- this._createHuiElement(elementConfig)
- )
- }
-
+
+ ${
+ this._config.elements.map((elementConfig: LovelaceElementConfig) =>
+ this._createHuiElement(elementConfig)
+ )
+ }
`;
}
@@ -71,14 +85,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
- }
-
- 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);
diff --git a/src/panels/lovelace/cards/hui-picture-entity-card.ts b/src/panels/lovelace/cards/hui-picture-entity-card.ts
new file mode 100644
index 0000000000..81be371ed9
--- /dev/null
+++ b/src/panels/lovelace/cards/hui-picture-entity-card.ts
@@ -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`
+
+ `;
+ } else if (this._config.show_name) {
+ footer = html`
+
+ `;
+ } else if (this._config.show_state) {
+ footer = html`
+
+ `;
+ }
+
+ return html`
+ ${this.renderStyle()}
+
+
+ ${footer}
+
+ `;
+ }
+
+ private renderStyle(): TemplateResult {
+ return html`
+
+ `;
+ }
+
+ 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);
diff --git a/src/panels/lovelace/cards/hui-picture-glance-card.ts b/src/panels/lovelace/cards/hui-picture-glance-card.ts
index e12e1c22e6..1a98be568a 100644
--- a/src/panels/lovelace/cards/hui-picture-glance-card.ts
+++ b/src/panels/lovelace/cards/hui-picture-glance-card.ts
@@ -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()}
-
-
- ${_entity.state}
- ${this._computeUom(_entity)}
-
-
-
- ${
- _line
- ? svg`
-
-
- `
- : ""
- }
-
-
-
- `;
- }
-
- _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`
-
- `;
- }
-}
-
-customElements.define("hui-sensor-card", HuiSensorCard);
diff --git a/src/panels/lovelace/cards/hui-sensor-card.ts b/src/panels/lovelace/cards/hui-sensor-card.ts
new file mode 100755
index 0000000000..2434527bcb
--- /dev/null
+++ b/src/panels/lovelace/cards/hui-sensor-card.ts
@@ -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`
+
+ Entity: ${this._config.entity} - Has no Unit of Measurement and
+ therefore can not display a line graph.
+
+ `;
+ } else if (!this._history) {
+ graph = svg`
+
+ `;
+ } else {
+ graph = svg`
+
+
+
+ `;
+ }
+ } else {
+ graph = "";
+ }
+ return html`
+ ${this.renderStyle()}
+
+ ${
+ !stateObj
+ ? html`
+
+ Entity not available: ${this._config.entity}
+
+ `
+ : html`
+
+
+ ${stateObj.state}
+ ${
+ this._config.unit ||
+ stateObj.attributes.unit_of_measurement
+ }
+
+
+ `
+ }
+
+ `;
+ }
+
+ 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
{
+ 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`
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-sensor-card": HuiSensorCard;
+ }
+}
+
+customElements.define("hui-sensor-card", HuiSensorCard);
diff --git a/src/panels/lovelace/cards/hui-shopping-list-card.ts b/src/panels/lovelace/cards/hui-shopping-list-card.ts
index a9191f1dcb..547386901b 100644
--- a/src/panels/lovelace/cards/hui-shopping-list-card.ts
+++ b/src/panels/lovelace/cards/hui-shopping-list-card.ts
@@ -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();
}
diff --git a/src/panels/lovelace/cards/hui-stack-card.ts b/src/panels/lovelace/cards/hui-stack-card.ts
index 2bc6ca5022..f246ed8c74 100644
--- a/src/panels/lovelace/cards/hui-stack-card.ts
+++ b/src/panels/lovelace/cards/hui-stack-card.ts
@@ -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 {
diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts
index a9491043eb..792f68134f 100644
--- a/src/panels/lovelace/cards/hui-thermostat-card.ts
+++ b/src/panels/lovelace/cards/hui-thermostat-card.ts
@@ -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)