mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 03:36:44 +00:00
Convert Sensor Card to Typescript (#2140)
* Sensor Convert * Types * Forgot to check stateobj * Review updates * lint * Update for hass and add error handling * Review Updates * Graph only shown if graph: line - Breaking Change * Only rendering when updated * Date.Now() * Forgot to reset the date * Lint * Review updates * Forgot to take this out * Bram the god * Update to render right things * Check if line is being drawn * Make graph if's more readable
This commit is contained in:
parent
0e6f6ddbda
commit
8ae03dd1ff
@ -1,322 +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,
|
||||
hours_to_show: 24,
|
||||
...config,
|
||||
};
|
||||
cardConfig.hours_to_show = Number(cardConfig.hours_to_show);
|
||||
cardConfig.height = Number(cardConfig.height);
|
||||
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({ _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 100">
|
||||
<path
|
||||
d="${_line}"
|
||||
fill="none"
|
||||
stroke="var(--accent-color)"
|
||||
stroke-width="5"
|
||||
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 = 5;
|
||||
const height = 80;
|
||||
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);
|
404
src/panels/lovelace/cards/hui-sensor-card.ts
Executable file
404
src/panels/lovelace/cards/hui-sensor-card.ts
Executable file
@ -0,0 +1,404 @@
|
||||
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 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;
|
||||
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,
|
||||
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") {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
Loading…
x
Reference in New Issue
Block a user