1
0
mirror of https://github.com/home-assistant/frontend.git synced 2025-12-20 02:38:53 +00:00

Normalize all line endings

This commit is contained in:
Paulus Schoutsen
2018-11-02 16:00:25 +01:00
parent 727cfe92e3
commit fbc1a722bd
77 changed files with 7792 additions and 7792 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,24 +1,24 @@
export default function parseAspectRatio(input) { export default function parseAspectRatio(input) {
// Handle 16x9, 16:9, 1.78x1, 1.78:1, 1.78 // Handle 16x9, 16:9, 1.78x1, 1.78:1, 1.78
// Ignore everything else // Ignore everything else
function parseOrThrow(number) { function parseOrThrow(number) {
const parsed = parseFloat(number); const parsed = parseFloat(number);
if (isNaN(parsed)) throw new Error(`${number} is not a number`); if (isNaN(parsed)) throw new Error(`${number} is not a number`);
return parsed; return parsed;
} }
try { try {
if (input) { if (input) {
const arr = input.replace(":", "x").split("x"); const arr = input.replace(":", "x").split("x");
if (arr.length === 0) { if (arr.length === 0) {
return null; return null;
} }
return arr.length === 1 return arr.length === 1
? { w: parseOrThrow(arr[0]), h: 1 } ? { w: parseOrThrow(arr[0]), h: 1 }
: { w: parseOrThrow(arr[0]), h: parseOrThrow(arr[1]) }; : { w: parseOrThrow(arr[0]), h: parseOrThrow(arr[1]) };
} }
} catch (err) { } catch (err) {
// Ignore the error // Ignore the error
} }
return null; return null;
} }

View File

@@ -1,59 +1,59 @@
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-dialog/paper-dialog"; import "@polymer/paper-dialog/paper-dialog";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../resources/ha-style"; import "../../resources/ha-style";
import EventsMixin from "../../mixins/events-mixin"; import EventsMixin from "../../mixins/events-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
*/ */
class HaLoadedComponents extends EventsMixin(PolymerElement) { class HaLoadedComponents extends EventsMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
<style include="ha-style-dialog"> <style include="ha-style-dialog">
paper-dialog { paper-dialog {
max-width: 500px; max-width: 500px;
} }
</style> </style>
<paper-dialog id="dialog" with-backdrop="" opened="{{_opened}}"> <paper-dialog id="dialog" with-backdrop="" opened="{{_opened}}">
<h2>Loaded Components</h2> <h2>Loaded Components</h2>
<paper-dialog-scrollable id="scrollable"> <paper-dialog-scrollable id="scrollable">
<p>The following components are currently loaded:</p> <p>The following components are currently loaded:</p>
<ul> <ul>
<template is='dom-repeat' items='[[_components]]'> <template is='dom-repeat' items='[[_components]]'>
<li>[[item]]</li> <li>[[item]]</li>
</template> </template>
</ul> </ul>
</paper-dialog-scrollable> </paper-dialog-scrollable>
</paper-dialog> </paper-dialog>
`; `;
} }
static get properties() { static get properties() {
return { return {
_hass: Object, _hass: Object,
_components: Array, _components: Array,
_opened: { _opened: {
type: Boolean, type: Boolean,
value: false, value: false,
}, },
}; };
} }
ready() { ready() {
super.ready(); super.ready();
} }
showDialog({ hass }) { showDialog({ hass }) {
this.hass = hass; this.hass = hass;
this._opened = true; this._opened = true;
this._components = this.hass.config.components.sort(); this._components = this.hass.config.components.sort();
setTimeout(() => this.$.dialog.center(), 0); setTimeout(() => this.$.dialog.center(), 0);
} }
} }
customElements.define("ha-loaded-components", HaLoadedComponents); customElements.define("ha-loaded-components", HaLoadedComponents);

View File

@@ -1,258 +1,258 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card"; import "../../../components/ha-card";
import EventsMixin from "../../../mixins/events-mixin"; import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../components/ha-label-badge"; import "../../../components/ha-label-badge";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
*/ */
const Icons = { const Icons = {
armed_away: "hass:security-lock", armed_away: "hass:security-lock",
armed_custom_bypass: "hass:security", armed_custom_bypass: "hass:security",
armed_home: "hass:security-home", armed_home: "hass:security-home",
armed_night: "hass:security-home", armed_night: "hass:security-home",
disarmed: "hass:verified", disarmed: "hass:verified",
pending: "hass:shield-outline", pending: "hass:shield-outline",
triggered: "hass:bell-ring", triggered: "hass:bell-ring",
}; };
class HuiAlarmPanelCard extends LocalizeMixin(EventsMixin(PolymerElement)) { class HuiAlarmPanelCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() { static get template() {
return html` return html`
<style> <style>
ha-card { ha-card {
padding-bottom: 16px; padding-bottom: 16px;
position: relative; position: relative;
--alarm-color-disarmed: var(--label-badge-green); --alarm-color-disarmed: var(--label-badge-green);
--alarm-color-pending: var(--label-badge-yellow); --alarm-color-pending: var(--label-badge-yellow);
--alarm-color-triggered: var(--label-badge-red); --alarm-color-triggered: var(--label-badge-red);
--alarm-color-armed: var(--label-badge-red); --alarm-color-armed: var(--label-badge-red);
--alarm-color-autoarm: rgba(0, 153, 255, .1); --alarm-color-autoarm: rgba(0, 153, 255, .1);
--alarm-state-color: var(--alarm-color-armed); --alarm-state-color: var(--alarm-color-armed);
--base-unit: 15px; --base-unit: 15px;
font-size: calc(var(--base-unit)); font-size: calc(var(--base-unit));
} }
ha-label-badge { ha-label-badge {
--ha-label-badge-color: var(--alarm-state-color); --ha-label-badge-color: var(--alarm-state-color);
--label-badge-text-color: var(--alarm-state-color); --label-badge-text-color: var(--alarm-state-color);
color: var(--alarm-state-color); color: var(--alarm-state-color);
position: absolute; position: absolute;
right: 12px; right: 12px;
top: 12px; top: 12px;
} }
.disarmed { .disarmed {
--alarm-state-color: var(--alarm-color-disarmed); --alarm-state-color: var(--alarm-color-disarmed);
} }
.triggered { .triggered {
--alarm-state-color: var(--alarm-color-triggered); --alarm-state-color: var(--alarm-color-triggered);
animation: pulse 1s infinite; animation: pulse 1s infinite;
} }
.arming { .arming {
--alarm-state-color: var(--alarm-color-pending); --alarm-state-color: var(--alarm-color-pending);
animation: pulse 1s infinite; animation: pulse 1s infinite;
} }
.pending { .pending {
--alarm-state-color: var(--alarm-color-pending); --alarm-state-color: var(--alarm-color-pending);
animation: pulse 1s infinite; animation: pulse 1s infinite;
} }
@keyframes pulse { @keyframes pulse {
0% { 0% {
--ha-label-badge-color: var(--alarm-state-color); --ha-label-badge-color: var(--alarm-state-color);
} }
100% { 100% {
--ha-label-badge-color: rgba(255, 153, 0, 0.3); --ha-label-badge-color: rgba(255, 153, 0, 0.3);
} }
} }
paper-input { paper-input {
margin: auto; margin: auto;
max-width: 200px; max-width: 200px;
font-size: calc(var(--base-unit)); font-size: calc(var(--base-unit));
} }
.state { .state {
margin-left: 16px; margin-left: 16px;
font-size: calc(var(--base-unit) * 0.9); font-size: calc(var(--base-unit) * 0.9);
position: relative; position: relative;
bottom: 16px; bottom: 16px;
color: var(--alarm-state-color); color: var(--alarm-state-color);
animation: none; animation: none;
} }
#keypad { #keypad {
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
#keypad div { #keypad div {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#keypad paper-button { #keypad paper-button {
margin-bottom: 10%; margin-bottom: 10%;
position: relative; position: relative;
padding: calc(var(--base-unit)); padding: calc(var(--base-unit));
font-size: calc(var(--base-unit) * 1.1); font-size: calc(var(--base-unit) * 1.1);
} }
.actions { .actions {
margin: 0 8px; margin: 0 8px;
padding-top: 20px; padding-top: 20px;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
font-size: calc(var(--base-unit) * 1); font-size: calc(var(--base-unit) * 1);
} }
.actions paper-button { .actions paper-button {
min-width: calc(var(--base-unit) * 9); min-width: calc(var(--base-unit) * 9);
color: var(--primary-color); color: var(--primary-color);
} }
paper-button#disarm { paper-button#disarm {
color: var(--google-red-500); color: var(--google-red-500);
} }
.not-found { .not-found {
flex: 1; flex: 1;
background-color: yellow; background-color: yellow;
padding: 8px; padding: 8px;
} }
</style> </style>
<ha-card <ha-card
header$="[[_computeHeader(localize, _stateObj)]]" header$="[[_computeHeader(localize, _stateObj)]]"
class$="[[_computeClassName(_stateObj)]]" class$="[[_computeClassName(_stateObj)]]"
> >
<template is="dom-if" if="[[_stateObj]]"> <template is="dom-if" if="[[_stateObj]]">
<ha-label-badge <ha-label-badge
class$="[[_stateObj.state]]" class$="[[_stateObj.state]]"
icon="[[_computeIcon(_stateObj)]]" icon="[[_computeIcon(_stateObj)]]"
label="[[_stateIconLabel(_stateObj.state)]]" label="[[_stateIconLabel(_stateObj.state)]]"
></ha-label-badge> ></ha-label-badge>
<template is="dom-if" if="[[_showActionToggle(_stateObj.state)]]"> <template is="dom-if" if="[[_showActionToggle(_stateObj.state)]]">
<div id="armActions" class="actions"> <div id="armActions" class="actions">
<template is="dom-repeat" items="[[_config.states]]"> <template is="dom-repeat" items="[[_config.states]]">
<paper-button noink raised id="[[item]]" on-click="_handleActionClick">[[_label(localize, item)]]</paper-button> <paper-button noink raised id="[[item]]" on-click="_handleActionClick">[[_label(localize, item)]]</paper-button>
</template> </template>
</div> </div>
</template> </template>
<template is="dom-if" if="[[!_showActionToggle(_stateObj.state)]]"> <template is="dom-if" if="[[!_showActionToggle(_stateObj.state)]]">
<div id="disarmActions" class="actions"> <div id="disarmActions" class="actions">
<paper-button noink raised id="disarm" on-click="_handleActionClick">[[_label(localize, "disarm")]]</paper-button> <paper-button noink raised id="disarm" on-click="_handleActionClick">[[_label(localize, "disarm")]]</paper-button>
</div> </div>
</template> </template>
<paper-input label="Alarm Code" type="password" value="[[_value]]"></paper-input> <paper-input label="Alarm Code" type="password" value="[[_value]]"></paper-input>
<div id="keypad"> <div id="keypad">
<div> <div>
<paper-button noink raised value="1" on-click="_handlePadClick">1</paper-button> <paper-button noink raised value="1" on-click="_handlePadClick">1</paper-button>
<paper-button noink raised value="4" on-click="_handlePadClick">4</paper-button> <paper-button noink raised value="4" on-click="_handlePadClick">4</paper-button>
<paper-button noink raised value="7" on-click="_handlePadClick">7</paper-button> <paper-button noink raised value="7" on-click="_handlePadClick">7</paper-button>
</div> </div>
<div> <div>
<paper-button noink raised value="2" on-click="_handlePadClick">2</paper-button> <paper-button noink raised value="2" on-click="_handlePadClick">2</paper-button>
<paper-button noink raised value="5" on-click="_handlePadClick">5</paper-button> <paper-button noink raised value="5" on-click="_handlePadClick">5</paper-button>
<paper-button noink raised value="8" on-click="_handlePadClick">8</paper-button> <paper-button noink raised value="8" on-click="_handlePadClick">8</paper-button>
<paper-button noink raised value="0" on-click="_handlePadClick">0</paper-button> <paper-button noink raised value="0" on-click="_handlePadClick">0</paper-button>
</div> </div>
<div> <div>
<paper-button noink raised value="3" on-click="_handlePadClick">3</paper-button> <paper-button noink raised value="3" on-click="_handlePadClick">3</paper-button>
<paper-button noink raised value="6" on-click="_handlePadClick">6</paper-button> <paper-button noink raised value="6" on-click="_handlePadClick">6</paper-button>
<paper-button noink raised value="9" on-click="_handlePadClick">9</paper-button> <paper-button noink raised value="9" on-click="_handlePadClick">9</paper-button>
<paper-button noink raised value="clear" on-click="_handlePadClick">[[_label(localize, "clear_code")]]</paper-button> <paper-button noink raised value="clear" on-click="_handlePadClick">[[_label(localize, "clear_code")]]</paper-button>
</div> </div>
</template> </template>
<template is="dom-if" if="[[!_stateObj]]"> <template is="dom-if" if="[[!_stateObj]]">
<div>Entity not available: [[_config.entity]]</div> <div>Entity not available: [[_config.entity]]</div>
</template> </template>
</ha-card> </ha-card>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: { hass: {
type: Object, type: Object,
}, },
_config: Object, _config: Object,
_stateObj: { _stateObj: {
type: Object, type: Object,
computed: "_computeStateObj(hass.states, _config.entity)", computed: "_computeStateObj(hass.states, _config.entity)",
}, },
_value: { _value: {
type: String, type: String,
value: "", value: "",
}, },
}; };
} }
getCardSize() { getCardSize() {
return 4; return 4;
} }
setConfig(config) { setConfig(config) {
if ( if (
!config || !config ||
!config.entity || !config.entity ||
config.entity.split(".")[0] !== "alarm_control_panel" config.entity.split(".")[0] !== "alarm_control_panel"
) { ) {
throw new Error("Invalid card configuration"); throw new Error("Invalid card configuration");
} }
const defaults = { const defaults = {
states: ["arm_away", "arm_home"], states: ["arm_away", "arm_home"],
}; };
this._config = { ...defaults, ...config }; this._config = { ...defaults, ...config };
this._icons = Icons; this._icons = Icons;
} }
_computeStateObj(states, entityId) { _computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null; return states && entityId in states ? states[entityId] : null;
} }
_computeHeader(localize, stateObj) { _computeHeader(localize, stateObj) {
if (!stateObj) return ""; if (!stateObj) return "";
return this._config.title return this._config.title
? this._config.title ? this._config.title
: this._label(localize, stateObj.state); : this._label(localize, stateObj.state);
} }
_computeIcon(stateObj) { _computeIcon(stateObj) {
return this._icons[stateObj.state] || "hass:shield-outline"; return this._icons[stateObj.state] || "hass:shield-outline";
} }
_label(localize, state) { _label(localize, state) {
return ( return (
localize(`state.alarm_control_panel.${state}`) || localize(`state.alarm_control_panel.${state}`) ||
localize(`ui.card.alarm_control_panel.${state}`) localize(`ui.card.alarm_control_panel.${state}`)
); );
} }
_stateIconLabel(state) { _stateIconLabel(state) {
const stateLabel = state.split("_").pop(); const stateLabel = state.split("_").pop();
return stateLabel === "disarmed" || stateLabel === "triggered" return stateLabel === "disarmed" || stateLabel === "triggered"
? "" ? ""
: stateLabel; : stateLabel;
} }
_showActionToggle(state) { _showActionToggle(state) {
return state === "disarmed"; return state === "disarmed";
} }
_computeClassName(stateObj) { _computeClassName(stateObj) {
if (!stateObj) return "not-found"; if (!stateObj) return "not-found";
return ""; return "";
} }
_handlePadClick(e) { _handlePadClick(e) {
const val = e.target.getAttribute("value"); const val = e.target.getAttribute("value");
this._value = val === "clear" ? "" : this._value + val; this._value = val === "clear" ? "" : this._value + val;
} }
_handleActionClick(e) { _handleActionClick(e) {
this.hass.callService("alarm_control_panel", "alarm_" + e.target.id, { this.hass.callService("alarm_control_panel", "alarm_" + e.target.id, {
entity_id: this._stateObj.entity_id, entity_id: this._stateObj.entity_id,
code: this._value, code: this._value,
}); });
this._value = ""; this._value = "";
} }
} }
customElements.define("hui-alarm-panel-card", HuiAlarmPanelCard); customElements.define("hui-alarm-panel-card", HuiAlarmPanelCard);

View File

@@ -1,83 +1,83 @@
import computeCardSize from "../common/compute-card-size"; import computeCardSize from "../common/compute-card-size";
import createCardElement from "../common/create-card-element"; import createCardElement from "../common/create-card-element";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceConfig } from "../types"; import { LovelaceCard, LovelaceConfig } from "../types";
interface Condition { interface Condition {
entity: string; entity: string;
state?: string; state?: string;
state_not?: string; state_not?: string;
} }
interface Config extends LovelaceConfig { interface Config extends LovelaceConfig {
card: LovelaceConfig; card: LovelaceConfig;
conditions: Condition[]; conditions: Condition[];
} }
class HuiConditionalCard extends HTMLElement implements LovelaceCard { class HuiConditionalCard extends HTMLElement implements LovelaceCard {
private _hass?: HomeAssistant; private _hass?: HomeAssistant;
private _config?: Config; private _config?: Config;
private _card?: LovelaceCard; private _card?: LovelaceCard;
public setConfig(config) { public setConfig(config) {
if ( if (
!config.card || !config.card ||
!config.conditions || !config.conditions ||
!Array.isArray(config.conditions) || !Array.isArray(config.conditions) ||
!config.conditions.every((c) => c.entity && (c.state || c.state_not)) !config.conditions.every((c) => c.entity && (c.state || c.state_not))
) { ) {
throw new Error("Error in card configuration."); throw new Error("Error in card configuration.");
} }
if (this._card && this._card.parentElement) { if (this._card && this._card.parentElement) {
this.removeChild(this._card); this.removeChild(this._card);
} }
this._config = config; this._config = config;
this._card = createCardElement(config.card); this._card = createCardElement(config.card);
if (this._hass) { if (this._hass) {
this.hass = this._hass; this.hass = this._hass;
} }
} }
set hass(hass: HomeAssistant) { set hass(hass: HomeAssistant) {
this._hass = hass; this._hass = hass;
if (!this._card) { if (!this._card) {
return; return;
} }
const visible = const visible =
this._config && this._config &&
this._config.conditions.every((c) => { this._config.conditions.every((c) => {
if (!(c.entity in hass.states)) { if (!(c.entity in hass.states)) {
return false; return false;
} }
if (c.state) { if (c.state) {
return hass.states[c.entity].state === c.state; return hass.states[c.entity].state === c.state;
} }
return hass.states[c.entity].state !== c.state_not; return hass.states[c.entity].state !== c.state_not;
}); });
if (visible) { if (visible) {
this._card.hass = hass; this._card.hass = hass;
if (!this._card.parentElement) { if (!this._card.parentElement) {
this.appendChild(this._card); this.appendChild(this._card);
} }
} else if (this._card.parentElement) { } else if (this._card.parentElement) {
this.removeChild(this._card); this.removeChild(this._card);
} }
} }
public getCardSize() { public getCardSize() {
return computeCardSize(this._card); return computeCardSize(this._card);
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-conditional-card": HuiConditionalCard; "hui-conditional-card": HuiConditionalCard;
} }
} }
customElements.define("hui-conditional-card", HuiConditionalCard); customElements.define("hui-conditional-card", HuiConditionalCard);

View File

@@ -1,187 +1,187 @@
import { import {
html, html,
LitElement, LitElement,
PropertyDeclarations, PropertyDeclarations,
PropertyValues, PropertyValues,
} from "@polymer/lit-element"; } from "@polymer/lit-element";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../components/hui-entities-toggle"; import "../components/hui-entities-toggle";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const"; import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { EntityConfig, EntityRow } from "../entity-rows/types"; import { EntityConfig, EntityRow } from "../entity-rows/types";
import { LovelaceCard, LovelaceConfig } from "../types"; import { LovelaceCard, LovelaceConfig } from "../types";
import processConfigEntities from "../common/process-config-entities"; import processConfigEntities from "../common/process-config-entities";
import createRowElement from "../common/create-row-element"; import createRowElement from "../common/create-row-element";
import computeDomain from "../../../common/entity/compute_domain"; import computeDomain from "../../../common/entity/compute_domain";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element"; import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
interface ConfigEntity extends EntityConfig { interface ConfigEntity extends EntityConfig {
type?: string; type?: string;
secondary_info: "entity-id" | "last-changed"; secondary_info: "entity-id" | "last-changed";
action_name?: string; action_name?: string;
service?: string; service?: string;
service_data?: object; service_data?: object;
url?: string; url?: string;
} }
interface Config extends LovelaceConfig { interface Config extends LovelaceConfig {
show_header_toggle?: boolean; show_header_toggle?: boolean;
title?: string; title?: string;
entities: ConfigEntity[]; entities: ConfigEntity[];
theme?: string; theme?: string;
} }
class HuiEntitiesCard extends hassLocalizeLitMixin(LitElement) class HuiEntitiesCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard { implements LovelaceCard {
protected _hass?: HomeAssistant; protected _hass?: HomeAssistant;
protected _config?: Config; protected _config?: Config;
protected _configEntities?: ConfigEntity[]; protected _configEntities?: ConfigEntity[];
set hass(hass: HomeAssistant) { set hass(hass: HomeAssistant) {
this._hass = hass; this._hass = hass;
this.shadowRoot!.querySelectorAll("#states > div > *").forEach( this.shadowRoot!.querySelectorAll("#states > div > *").forEach(
(element: unknown) => { (element: unknown) => {
(element as EntityRow).hass = hass; (element as EntityRow).hass = hass;
} }
); );
const entitiesToggle = this.shadowRoot!.querySelector( const entitiesToggle = this.shadowRoot!.querySelector(
"hui-entities-toggle" "hui-entities-toggle"
); );
if (entitiesToggle) { if (entitiesToggle) {
(entitiesToggle as any).hass = hass; (entitiesToggle as any).hass = hass;
} }
} }
static get properties(): PropertyDeclarations { static get properties(): PropertyDeclarations {
return { return {
_config: {}, _config: {},
}; };
} }
public getCardSize(): number { public getCardSize(): number {
if (!this._config) { if (!this._config) {
return 0; return 0;
} }
// +1 for the header // +1 for the header
return (this._config.title ? 1 : 0) + this._config.entities.length; return (this._config.title ? 1 : 0) + this._config.entities.length;
} }
public setConfig(config: Config): void { public setConfig(config: Config): void {
const entities = processConfigEntities(config.entities); const entities = processConfigEntities(config.entities);
this._config = { theme: "default", ...config }; this._config = { theme: "default", ...config };
this._configEntities = entities; this._configEntities = entities;
} }
protected updated(_changedProperties: PropertyValues): void { protected updated(_changedProperties: PropertyValues): void {
if (this._hass && this._config) { if (this._hass && this._config) {
applyThemesOnElement(this, this._hass.themes, this._config.theme); applyThemesOnElement(this, this._hass.themes, this._config.theme);
} }
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config || !this._hass) { if (!this._config || !this._hass) {
return html``; return html``;
} }
const { show_header_toggle, title } = this._config; const { show_header_toggle, title } = this._config;
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<ha-card> <ha-card>
${ ${
!title && !show_header_toggle !title && !show_header_toggle
? html`` ? html``
: html` : html`
<div class='header'> <div class='header'>
<div class="name">${title}</div> <div class="name">${title}</div>
${ ${
show_header_toggle === false show_header_toggle === false
? html`` ? html``
: html` : html`
<hui-entities-toggle <hui-entities-toggle
.hass="${this._hass}" .hass="${this._hass}"
.entities="${this._configEntities!.map( .entities="${this._configEntities!.map(
(conf) => conf.entity (conf) => conf.entity
)}" )}"
></hui-entities-toggle>` ></hui-entities-toggle>`
} }
</div>` </div>`
} }
<div id="states"> <div id="states">
${this._configEntities!.map((entityConf) => ${this._configEntities!.map((entityConf) =>
this.renderEntity(entityConf) this.renderEntity(entityConf)
)} )}
</div> </div>
</ha-card> </ha-card>
`; `;
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
<style> <style>
ha-card { ha-card {
padding: 16px; padding: 16px;
} }
#states { #states {
margin: -4px 0; margin: -4px 0;
} }
#states > * { #states > * {
margin: 8px 0; margin: 8px 0;
} }
#states > div > * { #states > div > * {
overflow: hidden; overflow: hidden;
} }
.header { .header {
@apply --paper-font-headline; @apply --paper-font-headline;
/* overwriting line-height +8 because entity-toggle can be 40px height, /* overwriting line-height +8 because entity-toggle can be 40px height,
compensating this with reduced padding */ compensating this with reduced padding */
line-height: 40px; line-height: 40px;
color: var(--primary-text-color); color: var(--primary-text-color);
padding: 4px 0 12px; padding: 4px 0 12px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.header .name { .header .name {
@apply --paper-font-common-nowrap; @apply --paper-font-common-nowrap;
} }
.state-card-dialog { .state-card-dialog {
cursor: pointer; cursor: pointer;
} }
</style> </style>
`; `;
} }
private renderEntity(entityConf: ConfigEntity): TemplateResult { private renderEntity(entityConf: ConfigEntity): TemplateResult {
const element = createRowElement(entityConf); const element = createRowElement(entityConf);
if (this._hass) { if (this._hass) {
element.hass = this._hass; element.hass = this._hass;
} }
if ( if (
entityConf.entity && entityConf.entity &&
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(entityConf.entity)) !DOMAINS_HIDE_MORE_INFO.includes(computeDomain(entityConf.entity))
) { ) {
element.classList.add("state-card-dialog"); element.classList.add("state-card-dialog");
element.addEventListener("click", () => this._handleClick(entityConf)); element.addEventListener("click", () => this._handleClick(entityConf));
} }
return html`<div>${element}</div>`; return html`<div>${element}</div>`;
} }
private _handleClick(entityConf: ConfigEntity): void { private _handleClick(entityConf: ConfigEntity): void {
const entityId = entityConf.entity; const entityId = entityConf.entity;
fireEvent(this, "hass-more-info", { entityId }); fireEvent(this, "hass-more-info", { entityId });
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-entities-card": HuiEntitiesCard; "hui-entities-card": HuiEntitiesCard;
} }
} }
customElements.define("hui-entities-card", HuiEntitiesCard); customElements.define("hui-entities-card", HuiEntitiesCard);

View File

@@ -1,77 +1,77 @@
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import createCardElement from "../common/create-card-element"; import createCardElement from "../common/create-card-element";
import processConfigEntities from "../common/process-config-entities"; import processConfigEntities from "../common/process-config-entities";
function getEntities(hass, filterState, entities) { function getEntities(hass, filterState, entities) {
return entities.filter((entityConf) => { return entities.filter((entityConf) => {
const stateObj = hass.states[entityConf.entity]; const stateObj = hass.states[entityConf.entity];
return stateObj && filterState.includes(stateObj.state); return stateObj && filterState.includes(stateObj.state);
}); });
} }
class HuiEntitiesCard extends PolymerElement { class HuiEntitiesCard extends PolymerElement {
static get properties() { static get properties() {
return { return {
hass: { hass: {
type: Object, type: Object,
observer: "_hassChanged", observer: "_hassChanged",
}, },
}; };
} }
getCardSize() { getCardSize() {
return this.lastChild ? this.lastChild.getCardSize() : 1; return this.lastChild ? this.lastChild.getCardSize() : 1;
} }
setConfig(config) { setConfig(config) {
if (!config.state_filter || !Array.isArray(config.state_filter)) { if (!config.state_filter || !Array.isArray(config.state_filter)) {
throw new Error("Incorrect filter config."); throw new Error("Incorrect filter config.");
} }
this._config = config; this._config = config;
this._configEntities = processConfigEntities(config.entities); this._configEntities = processConfigEntities(config.entities);
if (this.lastChild) { if (this.lastChild) {
this.removeChild(this.lastChild); this.removeChild(this.lastChild);
this._element = null; this._element = null;
} }
const card = "card" in config ? { ...config.card } : {}; const card = "card" in config ? { ...config.card } : {};
if (!card.type) card.type = "entities"; if (!card.type) card.type = "entities";
card.entities = []; card.entities = [];
const element = createCardElement(card); const element = createCardElement(card);
element._filterRawConfig = card; element._filterRawConfig = card;
this._updateCardConfig(element); this._updateCardConfig(element);
this._element = element; this._element = element;
} }
_hassChanged() { _hassChanged() {
this._updateCardConfig(this._element); this._updateCardConfig(this._element);
} }
_updateCardConfig(element) { _updateCardConfig(element) {
if (!element || element.tagName === "HUI-ERROR-CARD" || !this.hass) return; if (!element || element.tagName === "HUI-ERROR-CARD" || !this.hass) return;
const entitiesList = getEntities( const entitiesList = getEntities(
this.hass, this.hass,
this._config.state_filter, this._config.state_filter,
this._configEntities this._configEntities
); );
if (entitiesList.length === 0 && this._config.show_empty === false) { if (entitiesList.length === 0 && this._config.show_empty === false) {
this.style.display = "none"; this.style.display = "none";
return; return;
} }
this.style.display = "block"; this.style.display = "block";
element.setConfig({ ...element._filterRawConfig, entities: entitiesList }); element.setConfig({ ...element._filterRawConfig, entities: entitiesList });
element.isPanel = this.isPanel; element.isPanel = this.isPanel;
element.hass = this.hass; element.hass = this.hass;
// Attach element if it has never been attached. // Attach element if it has never been attached.
if (!this.lastChild) this.appendChild(element); if (!this.lastChild) this.appendChild(element);
} }
} }
customElements.define("hui-entity-filter-card", HuiEntitiesCard); customElements.define("hui-entity-filter-card", HuiEntitiesCard);

View File

@@ -1,273 +1,273 @@
import { import {
html, html,
LitElement, LitElement,
PropertyDeclarations, PropertyDeclarations,
PropertyValues, PropertyValues,
} from "@polymer/lit-element"; } from "@polymer/lit-element";
import { LovelaceCard, LovelaceConfig } from "../types"; import { LovelaceCard, LovelaceConfig } from "../types";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
import isValidEntityId from "../../../common/entity/valid_entity_id"; import isValidEntityId from "../../../common/entity/valid_entity_id";
import "../../../components/ha-card"; import "../../../components/ha-card";
interface Config extends LovelaceConfig { interface Config extends LovelaceConfig {
entity: string; entity: string;
title?: string; title?: string;
unit_of_measurement?: string; unit_of_measurement?: string;
min?: number; min?: number;
max?: number; max?: number;
severity?: object; severity?: object;
} }
const severityMap = { const severityMap = {
red: "var(--label-badge-red)", red: "var(--label-badge-red)",
green: "var(--label-badge-green)", green: "var(--label-badge-green)",
yellow: "var(--label-badge-yellow)", yellow: "var(--label-badge-yellow)",
normal: "var(--label-badge-blue)", normal: "var(--label-badge-blue)",
}; };
class HuiGaugeCard extends LitElement implements LovelaceCard { class HuiGaugeCard extends LitElement implements LovelaceCard {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: Config; private _config?: Config;
static get properties(): PropertyDeclarations { static get properties(): PropertyDeclarations {
return { return {
hass: {}, hass: {},
_config: {}, _config: {},
}; };
} }
public getCardSize(): number { public getCardSize(): number {
return 2; return 2;
} }
public setConfig(config: Config): void { public setConfig(config: Config): void {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Invalid card configuration"); throw new Error("Invalid card configuration");
} }
if (!isValidEntityId(config.entity)) { if (!isValidEntityId(config.entity)) {
throw new Error("Invalid Entity"); throw new Error("Invalid Entity");
} }
this._config = { min: 0, max: 100, ...config }; this._config = { min: 0, max: 100, ...config };
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config || !this.hass) { if (!this._config || !this.hass) {
return html``; return html``;
} }
const stateObj = this.hass.states[this._config.entity]; const stateObj = this.hass.states[this._config.entity];
let error; let error;
if (!stateObj) { if (!stateObj) {
error = "Entity not available: " + this._config.entity; error = "Entity not available: " + this._config.entity;
} else if (isNaN(Number(stateObj.state))) { } else if (isNaN(Number(stateObj.state))) {
error = "Entity is non-numeric: " + this._config.entity; error = "Entity is non-numeric: " + this._config.entity;
} }
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<ha-card @click="${this._handleClick}"> <ha-card @click="${this._handleClick}">
${ ${
error error
? html`<div class="not-found">${error}</div>` ? html`<div class="not-found">${error}</div>`
: html` : html`
<div class='container'> <div class='container'>
<div class='gauge-a'></div> <div class='gauge-a'></div>
<div class='gauge-b'></div> <div class='gauge-b'></div>
<div class='gauge-c' id='gauge'></div> <div class='gauge-c' id='gauge'></div>
<div class='gauge-data'> <div class='gauge-data'>
<div id='percent'>${stateObj.state} <div id='percent'>${stateObj.state}
${this._config.unit_of_measurement || ${this._config.unit_of_measurement ||
stateObj.attributes.unit_of_measurement || stateObj.attributes.unit_of_measurement ||
""} ""}
</div> </div>
<div id='title'>${this._config.title} <div id='title'>${this._config.title}
</div> </div>
</div> </div>
</div> </div>
` `
} }
</ha-card> </ha-card>
`; `;
} }
protected shouldUpdate(changedProps: PropertyValues): boolean { protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.get("hass")) { if (changedProps.get("hass")) {
return ( return (
(changedProps.get("hass") as any).states[this._config!.entity] !== (changedProps.get("hass") as any).states[this._config!.entity] !==
this.hass!.states[this._config!.entity] this.hass!.states[this._config!.entity]
); );
} }
if (changedProps.get("_config")) { if (changedProps.get("_config")) {
return changedProps.get("_config") !== this._config; return changedProps.get("_config") !== this._config;
} }
return true; return true;
} }
protected updated(): void { protected updated(): void {
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.shadowRoot!.getElementById("gauge") !this.shadowRoot!.getElementById("gauge")
) { ) {
return; return;
} }
const stateObj = this.hass.states[this._config.entity]; const stateObj = this.hass.states[this._config.entity];
if (isNaN(Number(stateObj.state))) { if (isNaN(Number(stateObj.state))) {
return; return;
} }
const turn = this._translateTurn(Number(stateObj.state), this._config); const turn = this._translateTurn(Number(stateObj.state), this._config);
this.shadowRoot!.getElementById( this.shadowRoot!.getElementById(
"gauge" "gauge"
)!.style.cssText = `transform: rotate(${turn}turn); background-color: ${this._computeSeverity( )!.style.cssText = `transform: rotate(${turn}turn); background-color: ${this._computeSeverity(
stateObj.state, stateObj.state,
this._config.severity! this._config.severity!
)}`; )}`;
(this.shadowRoot!.querySelector( (this.shadowRoot!.querySelector(
"ha-card" "ha-card"
)! as HTMLElement).style.setProperty( )! as HTMLElement).style.setProperty(
"--base-unit", "--base-unit",
this._computeBaseUnit() this._computeBaseUnit()
); );
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
<style> <style>
ha-card { ha-card {
--base-unit: 50px; --base-unit: 50px;
height: calc(var(--base-unit)*3); height: calc(var(--base-unit)*3);
position: relative; position: relative;
cursor: pointer; cursor: pointer;
} }
.container{ .container{
width: calc(var(--base-unit) * 4); width: calc(var(--base-unit) * 4);
height: calc(var(--base-unit) * 2); height: calc(var(--base-unit) * 2);
position: absolute; position: absolute;
top: calc(var(--base-unit)*1.5); top: calc(var(--base-unit)*1.5);
left: 50%; left: 50%;
overflow: hidden; overflow: hidden;
text-align: center; text-align: center;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
.gauge-a{ .gauge-a{
z-index: 1; z-index: 1;
position: absolute; position: absolute;
background-color: var(--primary-background-color); background-color: var(--primary-background-color);
width: calc(var(--base-unit) * 4); width: calc(var(--base-unit) * 4);
height: calc(var(--base-unit) * 2); height: calc(var(--base-unit) * 2);
top: 0%; top: 0%;
border-radius:calc(var(--base-unit) * 2.5) calc(var(--base-unit) * 2.5) 0px 0px ; border-radius:calc(var(--base-unit) * 2.5) calc(var(--base-unit) * 2.5) 0px 0px ;
} }
.gauge-b{ .gauge-b{
z-index: 3; z-index: 3;
position: absolute; position: absolute;
background-color: var(--paper-card-background-color); background-color: var(--paper-card-background-color);
width: calc(var(--base-unit) * 2.5); width: calc(var(--base-unit) * 2.5);
height: calc(var(--base-unit) * 1.25); height: calc(var(--base-unit) * 1.25);
top: calc(var(--base-unit) * 0.75); top: calc(var(--base-unit) * 0.75);
margin-left: calc(var(--base-unit) * 0.75); margin-left: calc(var(--base-unit) * 0.75);
margin-right: auto; margin-right: auto;
border-radius: calc(var(--base-unit) * 2.5) calc(var(--base-unit) * 2.5) 0px 0px ; border-radius: calc(var(--base-unit) * 2.5) calc(var(--base-unit) * 2.5) 0px 0px ;
} }
.gauge-c{ .gauge-c{
z-index: 2; z-index: 2;
position: absolute; position: absolute;
background-color: var(--label-badge-blue); background-color: var(--label-badge-blue);
width: calc(var(--base-unit) * 4); width: calc(var(--base-unit) * 4);
height: calc(var(--base-unit) * 2); height: calc(var(--base-unit) * 2);
top: calc(var(--base-unit) * 2); top: calc(var(--base-unit) * 2);
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
border-radius: 0px 0px calc(var(--base-unit) * 2) calc(var(--base-unit) * 2) ; border-radius: 0px 0px calc(var(--base-unit) * 2) calc(var(--base-unit) * 2) ;
transform-origin: center top; transform-origin: center top;
transition: all 1.3s ease-in-out; transition: all 1.3s ease-in-out;
} }
.gauge-data{ .gauge-data{
z-index: 4; z-index: 4;
color: var(--primary-text-color); color: var(--primary-text-color);
line-height: calc(var(--base-unit) * 0.3); line-height: calc(var(--base-unit) * 0.3);
position: absolute; position: absolute;
width: calc(var(--base-unit) * 4); width: calc(var(--base-unit) * 4);
height: calc(var(--base-unit) * 2.1); height: calc(var(--base-unit) * 2.1);
top: calc(var(--base-unit) * 1.2); top: calc(var(--base-unit) * 1.2);
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
transition: all 1s ease-out; transition: all 1s ease-out;
} }
.gauge-data #percent{ .gauge-data #percent{
font-size: calc(var(--base-unit) * 0.55); font-size: calc(var(--base-unit) * 0.55);
} }
.gauge-data #title{ .gauge-data #title{
padding-top: calc(var(--base-unit) * 0.15); padding-top: calc(var(--base-unit) * 0.15);
font-size: calc(var(--base-unit) * 0.30); font-size: calc(var(--base-unit) * 0.30);
} }
.not-found { .not-found {
flex: 1; flex: 1;
background-color: yellow; background-color: yellow;
padding: 8px; padding: 8px;
} }
</style> </style>
`; `;
} }
private _computeSeverity(stateValue: string, sections: object): string { private _computeSeverity(stateValue: string, sections: object): string {
const numberValue = Number(stateValue); const numberValue = Number(stateValue);
if (!sections) { if (!sections) {
return severityMap.normal; return severityMap.normal;
} }
const sectionsArray = Object.keys(sections); const sectionsArray = Object.keys(sections);
const sortable = sectionsArray.map((severity) => [ const sortable = sectionsArray.map((severity) => [
severity, severity,
sections[severity], sections[severity],
]); ]);
for (const severity of sortable) { for (const severity of sortable) {
if (severityMap[severity[0]] == null || isNaN(severity[1])) { if (severityMap[severity[0]] == null || isNaN(severity[1])) {
return severityMap.normal; return severityMap.normal;
} }
} }
sortable.sort((a, b) => a[1] - b[1]); sortable.sort((a, b) => a[1] - b[1]);
if (numberValue >= sortable[0][1] && numberValue < sortable[1][1]) { if (numberValue >= sortable[0][1] && numberValue < sortable[1][1]) {
return severityMap[sortable[0][0]]; return severityMap[sortable[0][0]];
} }
if (numberValue >= sortable[1][1] && numberValue < sortable[2][1]) { if (numberValue >= sortable[1][1] && numberValue < sortable[2][1]) {
return severityMap[sortable[1][0]]; return severityMap[sortable[1][0]];
} }
if (numberValue >= sortable[2][1]) { if (numberValue >= sortable[2][1]) {
return severityMap[sortable[2][0]]; return severityMap[sortable[2][0]];
} }
return severityMap.normal; return severityMap.normal;
} }
private _translateTurn(value: number, config: Config): number { private _translateTurn(value: number, config: Config): number {
const maxTurnValue = Math.min(Math.max(value, config.min!), config.max!); const maxTurnValue = Math.min(Math.max(value, config.min!), config.max!);
return ( return (
(5 * (maxTurnValue - config.min!)) / (config.max! - config.min!) / 10 (5 * (maxTurnValue - config.min!)) / (config.max! - config.min!) / 10
); );
} }
private _computeBaseUnit(): string { private _computeBaseUnit(): string {
return this.clientWidth < 200 ? this.clientWidth / 5 + "px" : "50px"; return this.clientWidth < 200 ? this.clientWidth / 5 + "px" : "50px";
} }
private _handleClick(): void { private _handleClick(): void {
fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-gauge-card": HuiGaugeCard; "hui-gauge-card": HuiGaugeCard;
} }
} }
customElements.define("hui-gauge-card", HuiGaugeCard); customElements.define("hui-gauge-card", HuiGaugeCard);

View File

@@ -1,86 +1,86 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/state-history-charts"; import "../../../components/state-history-charts";
import "../../../data/ha-state-history-data"; import "../../../data/ha-state-history-data";
import processConfigEntities from "../common/process-config-entities"; import processConfigEntities from "../common/process-config-entities";
class HuiHistoryGraphCard extends PolymerElement { class HuiHistoryGraphCard extends PolymerElement {
static get template() { static get template() {
return html` return html`
<style> <style>
ha-card { ha-card {
padding: 16px; padding: 16px;
} }
ha-card[header] { ha-card[header] {
padding-top: 0; padding-top: 0;
} }
</style> </style>
<ha-card header$='[[_config.title]]'> <ha-card header$='[[_config.title]]'>
<ha-state-history-data <ha-state-history-data
hass="[[hass]]" hass="[[hass]]"
filter-type="recent-entity" filter-type="recent-entity"
entity-id="[[_entities]]" entity-id="[[_entities]]"
data="{{_stateHistory}}" data="{{_stateHistory}}"
is-loading="{{_stateHistoryLoading}}" is-loading="{{_stateHistoryLoading}}"
cache-config="[[_cacheConfig]]" cache-config="[[_cacheConfig]]"
></ha-state-history-data> ></ha-state-history-data>
<state-history-charts <state-history-charts
hass="[[hass]]" hass="[[hass]]"
history-data="[[_stateHistory]]" history-data="[[_stateHistory]]"
is-loading-data="[[_stateHistoryLoading]]" is-loading-data="[[_stateHistoryLoading]]"
names="[[_names]]" names="[[_names]]"
up-to-now up-to-now
no-single no-single
></state-history-charts> ></state-history-charts>
</ha-card> </ha-card>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
_names: Object, _names: Object,
_entities: Array, _entities: Array,
_stateHistory: Object, _stateHistory: Object,
_stateHistoryLoading: Boolean, _stateHistoryLoading: Boolean,
_cacheConfig: Object, _cacheConfig: Object,
}; };
} }
getCardSize() { getCardSize() {
return 4; return 4;
} }
setConfig(config) { setConfig(config) {
const entities = processConfigEntities(config.entities); const entities = processConfigEntities(config.entities);
this._config = config; this._config = config;
const _entities = []; const _entities = [];
const _names = {}; const _names = {};
for (const entity of entities) { for (const entity of entities) {
_entities.push(entity.entity); _entities.push(entity.entity);
if (entity.name) { if (entity.name) {
_names[entity.entity] = entity.name; _names[entity.entity] = entity.name;
} }
} }
this.setProperties({ this.setProperties({
_cacheConfig: { _cacheConfig: {
cacheKey: _entities.sort().join(), cacheKey: _entities.sort().join(),
hoursToShow: config.hours_to_show || 24, hoursToShow: config.hours_to_show || 24,
refresh: config.refresh_interval || 0, refresh: config.refresh_interval || 0,
}, },
_entities, _entities,
_names, _names,
}); });
} }
} }
customElements.define("hui-history-graph-card", HuiHistoryGraphCard); customElements.define("hui-history-graph-card", HuiHistoryGraphCard);

View File

@@ -1,50 +1,50 @@
import { html } from "@polymer/lit-element"; import { html } from "@polymer/lit-element";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
import computeCardSize from "../common/compute-card-size"; import computeCardSize from "../common/compute-card-size";
import { HuiStackCard } from "./hui-stack-card"; import { HuiStackCard } from "./hui-stack-card";
class HuiHorizontalStackCard extends HuiStackCard { class HuiHorizontalStackCard extends HuiStackCard {
public getCardSize(): number { public getCardSize(): number {
let totalSize = 0; let totalSize = 0;
if (this._cards) { if (this._cards) {
for (const element of this._cards) { for (const element of this._cards) {
const elementSize = computeCardSize(element); const elementSize = computeCardSize(element);
totalSize = elementSize > totalSize ? elementSize : totalSize; totalSize = elementSize > totalSize ? elementSize : totalSize;
} }
} }
return totalSize; return totalSize;
} }
protected renderStyle(): TemplateResult { protected renderStyle(): TemplateResult {
return html` return html`
<style> <style>
#root { #root {
display: flex; display: flex;
} }
#root > * { #root > * {
flex: 1 1 0; flex: 1 1 0;
margin: 0 4px; margin: 0 4px;
min-width: 0; min-width: 0;
} }
#root > *:first-child { #root > *:first-child {
margin-left: 0; margin-left: 0;
} }
#root > *:last-child { #root > *:last-child {
margin-right: 0; margin-right: 0;
} }
</style> </style>
`; `;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-horitzontal-stack-card": HuiHorizontalStackCard; "hui-horitzontal-stack-card": HuiHorizontalStackCard;
} }
} }
customElements.define("hui-horizontal-stack-card", HuiHorizontalStackCard); customElements.define("hui-horizontal-stack-card", HuiHorizontalStackCard);

View File

@@ -1,80 +1,80 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "../../../components/ha-card"; import "../../../components/ha-card";
import { LovelaceCard, LovelaceConfig } from "../types"; import { LovelaceCard, LovelaceConfig } from "../types";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
interface Config extends LovelaceConfig { interface Config extends LovelaceConfig {
aspect_ratio?: string; aspect_ratio?: string;
title?: string; title?: string;
url: string; url: string;
} }
export class HuiIframeCard extends LitElement implements LovelaceCard { export class HuiIframeCard extends LitElement implements LovelaceCard {
protected _config?: Config; protected _config?: Config;
static get properties(): PropertyDeclarations { static get properties(): PropertyDeclarations {
return { return {
_config: {}, _config: {},
}; };
} }
public getCardSize(): number { public getCardSize(): number {
return 1 + this.offsetHeight / 50; return 1 + this.offsetHeight / 50;
} }
public setConfig(config: Config): void { public setConfig(config: Config): void {
if (!config.url) { if (!config.url) {
throw new Error("URL required"); throw new Error("URL required");
} }
this._config = config; this._config = config;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config) { if (!this._config) {
return html``; return html``;
} }
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<ha-card .header="${this._config.title}"> <ha-card .header="${this._config.title}">
<div id="root"> <div id="root">
<iframe src="${this._config.url}"></iframe> <iframe src="${this._config.url}"></iframe>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
<style> <style>
ha-card { ha-card {
overflow: hidden; overflow: hidden;
} }
#root { #root {
width: 100%; width: 100%;
position: relative; position: relative;
padding-top: ${this._config!.aspect_ratio || "50%"}; padding-top: ${this._config!.aspect_ratio || "50%"};
} }
iframe { iframe {
position: absolute; position: absolute;
border: none; border: none;
width: 100%; width: 100%;
height: 100%; height: 100%;
top: 0; top: 0;
left: 0; left: 0;
} }
</style> </style>
`; `;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-iframe-card": HuiIframeCard; "hui-iframe-card": HuiIframeCard;
} }
} }
customElements.define("hui-iframe-card", HuiIframeCard); customElements.define("hui-iframe-card", HuiIframeCard);

View File

@@ -1,57 +1,57 @@
import createErrorCardConfig from "../common/create-error-card-config"; import createErrorCardConfig from "../common/create-error-card-config";
import computeDomain from "../../../common/entity/compute_domain"; import computeDomain from "../../../common/entity/compute_domain";
export default class LegacyWrapperCard extends HTMLElement { export default class LegacyWrapperCard extends HTMLElement {
constructor(tag, domain) { constructor(tag, domain) {
super(); super();
this._tag = tag.toUpperCase(); this._tag = tag.toUpperCase();
this._domain = domain; this._domain = domain;
this._element = null; this._element = null;
} }
getCardSize() { getCardSize() {
return 3; return 3;
} }
setConfig(config) { setConfig(config) {
if (!config.entity) { if (!config.entity) {
throw new Error("No entity specified"); throw new Error("No entity specified");
} }
if (computeDomain(config.entity) !== this._domain) { if (computeDomain(config.entity) !== this._domain) {
throw new Error( throw new Error(
`Specified entity needs to be of domain ${this._domain}.` `Specified entity needs to be of domain ${this._domain}.`
); );
} }
this._config = config; this._config = config;
} }
set hass(hass) { set hass(hass) {
const entityId = this._config.entity; const entityId = this._config.entity;
if (entityId in hass.states) { if (entityId in hass.states) {
this._ensureElement(this._tag); this._ensureElement(this._tag);
this.lastChild.hass = hass; this.lastChild.hass = hass;
this.lastChild.stateObj = hass.states[entityId]; this.lastChild.stateObj = hass.states[entityId];
} else { } else {
this._ensureElement("HUI-ERROR-CARD"); this._ensureElement("HUI-ERROR-CARD");
this.lastChild.setConfig( this.lastChild.setConfig(
createErrorCardConfig( createErrorCardConfig(
`No state available for ${entityId}`, `No state available for ${entityId}`,
this._config this._config
) )
); );
} }
} }
_ensureElement(tag) { _ensureElement(tag) {
if (this.lastChild && this.lastChild.tagName === tag) return; if (this.lastChild && this.lastChild.tagName === tag) return;
if (this.lastChild) { if (this.lastChild) {
this.removeChild(this.lastChild); this.removeChild(this.lastChild);
} }
this.appendChild(document.createElement(tag)); this.appendChild(document.createElement(tag));
} }
} }

View File

@@ -1,327 +1,327 @@
import { import {
html, html,
LitElement, LitElement,
PropertyValues, PropertyValues,
PropertyDeclarations, PropertyDeclarations,
} from "@polymer/lit-element"; } from "@polymer/lit-element";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { styleMap } from "lit-html/directives/styleMap"; import { styleMap } from "lit-html/directives/styleMap";
import computeStateName from "../../../common/entity/compute_state_name"; import computeStateName from "../../../common/entity/compute_state_name";
import stateIcon from "../../../common/entity/state_icon"; import stateIcon from "../../../common/entity/state_icon";
import { jQuery } from "../../../resources/jquery"; import { jQuery } from "../../../resources/jquery";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import { roundSliderStyle } from "../../../resources/jquery.roundslider"; import { roundSliderStyle } from "../../../resources/jquery.roundslider";
import { HomeAssistant, LightEntity } from "../../../types"; import { HomeAssistant, LightEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard, LovelaceConfig } from "../types"; import { LovelaceCard, LovelaceConfig } from "../types";
import { longPress } from "../common/directives/long-press-directive"; import { longPress } from "../common/directives/long-press-directive";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
const lightConfig = { const lightConfig = {
radius: 80, radius: 80,
step: 1, step: 1,
circleShape: "pie", circleShape: "pie",
startAngle: 315, startAngle: 315,
width: 5, width: 5,
min: 1, min: 1,
max: 100, max: 100,
sliderType: "min-range", sliderType: "min-range",
lineCap: "round", lineCap: "round",
handleSize: "+12", handleSize: "+12",
showTooltip: false, showTooltip: false,
}; };
interface Config extends LovelaceConfig { interface Config extends LovelaceConfig {
entity: string; entity: string;
name?: string; name?: string;
} }
export class HuiLightCard extends hassLocalizeLitMixin(LitElement) export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard { implements LovelaceCard {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: Config; private _config?: Config;
private _brightnessTimout?: number; private _brightnessTimout?: number;
static get properties(): PropertyDeclarations { static get properties(): PropertyDeclarations {
return { return {
hass: {}, hass: {},
_config: {}, _config: {},
}; };
} }
public getCardSize(): number { public getCardSize(): number {
return 2; return 2;
} }
public setConfig(config: Config): void { public setConfig(config: Config): void {
if (!config.entity || config.entity.split(".")[0] !== "light") { if (!config.entity || config.entity.split(".")[0] !== "light") {
throw new Error("Specify an entity from within the light domain."); throw new Error("Specify an entity from within the light domain.");
} }
this._config = config; this._config = config;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.hass || !this._config) { if (!this.hass || !this._config) {
return html``; return html``;
} }
const stateObj = this.hass.states[this._config!.entity] as LightEntity; const stateObj = this.hass.states[this._config!.entity] as LightEntity;
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<ha-card> <ha-card>
${ ${
!stateObj !stateObj
? html` ? html`
<div class="not-found">Entity not available: ${ <div class="not-found">Entity not available: ${
this._config.entity this._config.entity
}</div>` }</div>`
: html` : html`
<div id="light"></div> <div id="light"></div>
<div id="tooltip"> <div id="tooltip">
<div class="icon-state"> <div class="icon-state">
<ha-icon <ha-icon
data-state="${stateObj.state}" data-state="${stateObj.state}"
.icon="${stateIcon(stateObj)}" .icon="${stateIcon(stateObj)}"
style="${styleMap({ style="${styleMap({
filter: this._computeBrightness(stateObj), filter: this._computeBrightness(stateObj),
color: this._computeColor(stateObj), color: this._computeColor(stateObj),
})}" })}"
@ha-click="${() => this._handleClick(false)}" @ha-click="${() => this._handleClick(false)}"
@ha-hold="${() => this._handleClick(true)}" @ha-hold="${() => this._handleClick(true)}"
.longPress="${longPress()}" .longPress="${longPress()}"
></ha-icon> ></ha-icon>
<div <div
class="brightness" class="brightness"
@ha-click="${() => this._handleClick(false)}" @ha-click="${() => this._handleClick(false)}"
@ha-hold="${() => this._handleClick(true)}" @ha-hold="${() => this._handleClick(true)}"
.longPress="${longPress()}" .longPress="${longPress()}"
></div> ></div>
<div class="name">${this._config.name || <div class="name">${this._config.name ||
computeStateName(stateObj)}</div> computeStateName(stateObj)}</div>
</div> </div>
</div> </div>
` `
} }
</ha-card> </ha-card>
`; `;
} }
protected shouldUpdate(changedProps: PropertyValues): boolean { protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.get("hass")) { if (changedProps.get("hass")) {
return ( return (
(changedProps.get("hass") as any).states[this._config!.entity] !== (changedProps.get("hass") as any).states[this._config!.entity] !==
this.hass!.states[this._config!.entity] this.hass!.states[this._config!.entity]
); );
} }
return (changedProps as unknown) as boolean; return (changedProps as unknown) as boolean;
} }
protected firstUpdated(): void { protected firstUpdated(): void {
const brightness = this.hass!.states[this._config!.entity].attributes const brightness = this.hass!.states[this._config!.entity].attributes
.brightness; .brightness;
jQuery("#light", this.shadowRoot).roundSlider({ jQuery("#light", this.shadowRoot).roundSlider({
...lightConfig, ...lightConfig,
change: (value) => this._setBrightness(value), change: (value) => this._setBrightness(value),
drag: (value) => this._dragEvent(value), drag: (value) => this._dragEvent(value),
start: () => this._showBrightness(), start: () => this._showBrightness(),
stop: () => this._hideBrightness(), stop: () => this._hideBrightness(),
}); });
this.shadowRoot!.querySelector(".brightness")!.innerHTML = this.shadowRoot!.querySelector(".brightness")!.innerHTML =
(Math.round((brightness / 254) * 100) || 0) + "%"; (Math.round((brightness / 254) * 100) || 0) + "%";
} }
protected updated(): void { protected updated(): void {
const attrs = this.hass!.states[this._config!.entity].attributes; const attrs = this.hass!.states[this._config!.entity].attributes;
jQuery("#light", this.shadowRoot).roundSlider({ jQuery("#light", this.shadowRoot).roundSlider({
value: Math.round((attrs.brightness / 254) * 100) || 0, value: Math.round((attrs.brightness / 254) * 100) || 0,
}); });
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
${roundSliderStyle} ${roundSliderStyle}
<style> <style>
:host { :host {
display: block; display: block;
} }
ha-card { ha-card {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
--brightness-font-color: white; --brightness-font-color: white;
--brightness-font-text-shadow: --brightness-font-text-shadow:
-1px -1px 0 #000, -1px -1px 0 #000,
1px -1px 0 #000, 1px -1px 0 #000,
-1px 1px 0 #000, -1px 1px 0 #000,
1px 1px 0 #000; 1px 1px 0 #000;
--name-font-size: 1.2rem; --name-font-size: 1.2rem;
--brightness-font-size: 1.2rem; --brightness-font-size: 1.2rem;
--rail-border-color: transparent; --rail-border-color: transparent;
} }
#tooltip { #tooltip {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 100%; height: 100%;
text-align: center; text-align: center;
z-index: 15; z-index: 15;
} }
.icon-state { .icon-state {
display: block; display: block;
margin: auto; margin: auto;
width: 100%; width: 100%;
height: 100%; height: 100%;
transform: translate(0,25%); transform: translate(0,25%);
} }
#light { #light {
margin: 0 auto; margin: 0 auto;
padding-top: 16px; padding-top: 16px;
padding-bottom: 16px; padding-bottom: 16px;
} }
#light .rs-bar.rs-transition.rs-first, .rs-bar.rs-transition.rs-second{ #light .rs-bar.rs-transition.rs-first, .rs-bar.rs-transition.rs-second{
z-index: 20 !important; z-index: 20 !important;
} }
#light .rs-range-color { #light .rs-range-color {
background-color: var(--primary-color); background-color: var(--primary-color);
} }
#light .rs-path-color { #light .rs-path-color {
background-color: var(--disabled-text-color); background-color: var(--disabled-text-color);
} }
#light .rs-handle { #light .rs-handle {
background-color: var(--paper-card-background-color, white); background-color: var(--paper-card-background-color, white);
padding: 7px; padding: 7px;
border: 2px solid var(--disabled-text-color); border: 2px solid var(--disabled-text-color);
} }
#light .rs-handle.rs-focus { #light .rs-handle.rs-focus {
border-color:var(--primary-color); border-color:var(--primary-color);
} }
#light .rs-handle:after { #light .rs-handle:after {
border-color: var(--primary-color); border-color: var(--primary-color);
background-color: var(--primary-color); background-color: var(--primary-color);
} }
#light .rs-border { #light .rs-border {
border-color: var(--rail-border-color); border-color: var(--rail-border-color);
} }
#light .rs-inner.rs-bg-color.rs-border, #light .rs-inner.rs-bg-color.rs-border,
#light .rs-overlay.rs-transition.rs-bg-color { #light .rs-overlay.rs-transition.rs-bg-color {
background-color: var(--paper-card-background-color, white); background-color: var(--paper-card-background-color, white);
} }
ha-icon { ha-icon {
margin: auto; margin: auto;
width: 76px; width: 76px;
height: 76px; height: 76px;
color: var(--paper-item-icon-color, #44739e); color: var(--paper-item-icon-color, #44739e);
cursor: pointer; cursor: pointer;
} }
ha-icon[data-state=on] { ha-icon[data-state=on] {
color: var(--paper-item-icon-active-color, #FDD835); color: var(--paper-item-icon-active-color, #FDD835);
} }
ha-icon[data-state=unavailable] { ha-icon[data-state=unavailable] {
color: var(--state-icon-unavailable-color); color: var(--state-icon-unavailable-color);
} }
.name { .name {
padding-top: 40px; padding-top: 40px;
font-size: var(--name-font-size); font-size: var(--name-font-size);
} }
.brightness { .brightness {
font-size: var(--brightness-font-size); font-size: var(--brightness-font-size);
position: absolute; position: absolute;
margin: 0 auto; margin: 0 auto;
left: 50%; left: 50%;
top: 10%; top: 10%;
transform: translate(-50%); transform: translate(-50%);
opacity: 0; opacity: 0;
transition: opacity .5s ease-in-out; transition: opacity .5s ease-in-out;
-moz-transition: opacity .5s ease-in-out; -moz-transition: opacity .5s ease-in-out;
-webkit-transition: opacity .5s ease-in-out; -webkit-transition: opacity .5s ease-in-out;
cursor: pointer; cursor: pointer;
color: var(--brightness-font-color); color: var(--brightness-font-color);
text-shadow: var(--brightness-font-text-shadow) text-shadow: var(--brightness-font-text-shadow)
} }
.show_brightness { .show_brightness {
opacity: 1; opacity: 1;
} }
.not-found { .not-found {
flex: 1; flex: 1;
background-color: yellow; background-color: yellow;
padding: 8px; padding: 8px;
} }
</style> </style>
`; `;
} }
private _dragEvent(e: any): void { private _dragEvent(e: any): void {
this.shadowRoot!.querySelector(".brightness")!.innerHTML = e.value + "%"; this.shadowRoot!.querySelector(".brightness")!.innerHTML = e.value + "%";
} }
private _showBrightness(): void { private _showBrightness(): void {
clearTimeout(this._brightnessTimout); clearTimeout(this._brightnessTimout);
this.shadowRoot!.querySelector(".brightness")!.classList.add( this.shadowRoot!.querySelector(".brightness")!.classList.add(
"show_brightness" "show_brightness"
); );
} }
private _hideBrightness(): void { private _hideBrightness(): void {
this._brightnessTimout = window.setTimeout(() => { this._brightnessTimout = window.setTimeout(() => {
this.shadowRoot!.querySelector(".brightness")!.classList.remove( this.shadowRoot!.querySelector(".brightness")!.classList.remove(
"show_brightness" "show_brightness"
); );
}, 500); }, 500);
} }
private _setBrightness(e: any): void { private _setBrightness(e: any): void {
this.hass!.callService("light", "turn_on", { this.hass!.callService("light", "turn_on", {
entity_id: this._config!.entity, entity_id: this._config!.entity,
brightness_pct: e.value, brightness_pct: e.value,
}); });
} }
private _computeBrightness(stateObj: LightEntity): string { private _computeBrightness(stateObj: LightEntity): string {
if (!stateObj.attributes.brightness) { if (!stateObj.attributes.brightness) {
return ""; return "";
} }
const brightness = stateObj.attributes.brightness; const brightness = stateObj.attributes.brightness;
return `brightness(${(brightness + 245) / 5}%)`; return `brightness(${(brightness + 245) / 5}%)`;
} }
private _computeColor(stateObj: LightEntity): string { private _computeColor(stateObj: LightEntity): string {
if (!stateObj.attributes.hs_color) { if (!stateObj.attributes.hs_color) {
return ""; return "";
} }
const [hue, sat] = stateObj.attributes.hs_color; const [hue, sat] = stateObj.attributes.hs_color;
if (sat <= 10) { if (sat <= 10) {
return ""; return "";
} }
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`; return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
} }
private _handleClick(hold: boolean): void { private _handleClick(hold: boolean): void {
const entityId = this._config!.entity; const entityId = this._config!.entity;
if (hold) { if (hold) {
fireEvent(this, "hass-more-info", { fireEvent(this, "hass-more-info", {
entityId, entityId,
}); });
return; return;
} }
this.hass!.callService("light", "toggle", { this.hass!.callService("light", "toggle", {
entity_id: entityId, entity_id: entityId,
}); });
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-light-card": HuiLightCard; "hui-light-card": HuiLightCard;
} }
} }
customElements.define("hui-light-card", HuiLightCard); customElements.define("hui-light-card", HuiLightCard);

View File

@@ -1,305 +1,305 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import Leaflet from "leaflet"; import Leaflet from "leaflet";
import "../../map/ha-entity-marker"; import "../../map/ha-entity-marker";
import setupLeafletMap from "../../../common/dom/setup-leaflet-map"; import setupLeafletMap from "../../../common/dom/setup-leaflet-map";
import processConfigEntities from "../common/process-config-entities"; import processConfigEntities from "../common/process-config-entities";
import computeStateDomain from "../../../common/entity/compute_state_domain"; import computeStateDomain from "../../../common/entity/compute_state_domain";
import computeStateName from "../../../common/entity/compute_state_name"; import computeStateName from "../../../common/entity/compute_state_name";
import debounce from "../../../common/util/debounce"; import debounce from "../../../common/util/debounce";
Leaflet.Icon.Default.imagePath = "/static/images/leaflet"; Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
class HuiMapCard extends PolymerElement { class HuiMapCard extends PolymerElement {
static get template() { static get template() {
return html` return html`
<style> <style>
:host([is-panel]) ha-card { :host([is-panel]) ha-card {
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
/** /**
* In panel mode we want a full height map. Since parent #view * In panel mode we want a full height map. Since parent #view
* only sets min-height, we need absolute positioning here * only sets min-height, we need absolute positioning here
*/ */
height: 100%; height: 100%;
position: absolute; position: absolute;
} }
ha-card { ha-card {
overflow: hidden; overflow: hidden;
} }
#map { #map {
z-index: 0; z-index: 0;
border: none; border: none;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
paper-icon-button { paper-icon-button {
position: absolute; position: absolute;
top: 75px; top: 75px;
left: 7px; left: 7px;
} }
#root { #root {
position: relative; position: relative;
} }
:host([is-panel]) #root { :host([is-panel]) #root {
height: 100%; height: 100%;
} }
</style> </style>
<ha-card id="card" header="[[_config.title]]"> <ha-card id="card" header="[[_config.title]]">
<div id="root"> <div id="root">
<div id="map"></div> <div id="map"></div>
<paper-icon-button <paper-icon-button
on-click="_fitMap" on-click="_fitMap"
icon="hass:image-filter-center-focus" icon="hass:image-filter-center-focus"
title="Reset focus" title="Reset focus"
></paper-icon-button> ></paper-icon-button>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: { hass: {
type: Object, type: Object,
observer: "_drawEntities", observer: "_drawEntities",
}, },
_config: Object, _config: Object,
isPanel: { isPanel: {
type: Boolean, type: Boolean,
reflectToAttribute: true, reflectToAttribute: true,
}, },
}; };
} }
constructor() { constructor() {
super(); super();
this._debouncedResizeListener = debounce(this._resetMap.bind(this), 100); this._debouncedResizeListener = debounce(this._resetMap.bind(this), 100);
} }
ready() { ready() {
super.ready(); super.ready();
if (!this._config || this.isPanel) { if (!this._config || this.isPanel) {
return; return;
} }
this.$.root.style.paddingTop = this._config.aspect_ratio || "100%"; this.$.root.style.paddingTop = this._config.aspect_ratio || "100%";
} }
setConfig(config) { setConfig(config) {
if (!config) { if (!config) {
throw new Error("Error in card configuration."); throw new Error("Error in card configuration.");
} }
this._configEntities = processConfigEntities(config.entities); this._configEntities = processConfigEntities(config.entities);
this._config = config; this._config = config;
} }
getCardSize() { getCardSize() {
let ar = this._config.aspect_ratio || "100%"; let ar = this._config.aspect_ratio || "100%";
ar = ar.substr(0, ar.length - 1); ar = ar.substr(0, ar.length - 1);
return 1 + Math.floor(ar / 25) || 3; return 1 + Math.floor(ar / 25) || 3;
} }
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
// Observe changes to map size and invalidate to prevent broken rendering // Observe changes to map size and invalidate to prevent broken rendering
// Uses ResizeObserver in Chrome, otherwise window resize event // Uses ResizeObserver in Chrome, otherwise window resize event
if (typeof ResizeObserver === "function") { if (typeof ResizeObserver === "function") {
this._resizeObserver = new ResizeObserver(() => this._resizeObserver = new ResizeObserver(() =>
this._debouncedResizeListener() this._debouncedResizeListener()
); );
this._resizeObserver.observe(this.$.map); this._resizeObserver.observe(this.$.map);
} else { } else {
window.addEventListener("resize", this._debouncedResizeListener); window.addEventListener("resize", this._debouncedResizeListener);
} }
this._map = setupLeafletMap(this.$.map); this._map = setupLeafletMap(this.$.map);
this._drawEntities(this.hass); this._drawEntities(this.hass);
setTimeout(() => { setTimeout(() => {
this._resetMap(); this._resetMap();
this._fitMap(); this._fitMap();
}, 1); }, 1);
} }
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
if (this._map) { if (this._map) {
this._map.remove(); this._map.remove();
} }
if (this._resizeObserver) { if (this._resizeObserver) {
this._resizeObserver.unobserve(this.$.map); this._resizeObserver.unobserve(this.$.map);
} else { } else {
window.removeEventListener("resize", this._debouncedResizeListener); window.removeEventListener("resize", this._debouncedResizeListener);
} }
} }
_resetMap() { _resetMap() {
if (!this._map) { if (!this._map) {
return; return;
} }
this._map.invalidateSize(); this._map.invalidateSize();
} }
_fitMap() { _fitMap() {
const zoom = this._config.default_zoom; const zoom = this._config.default_zoom;
if (this._mapItems.length === 0) { if (this._mapItems.length === 0) {
this._map.setView( this._map.setView(
new Leaflet.LatLng( new Leaflet.LatLng(
this.hass.config.latitude, this.hass.config.latitude,
this.hass.config.longitude this.hass.config.longitude
), ),
zoom || 14 zoom || 14
); );
return; return;
} }
const bounds = new Leaflet.latLngBounds( const bounds = new Leaflet.latLngBounds(
this._mapItems.map((item) => item.getLatLng()) this._mapItems.map((item) => item.getLatLng())
); );
this._map.fitBounds(bounds.pad(0.5)); this._map.fitBounds(bounds.pad(0.5));
if (zoom && this._map.getZoom() > zoom) { if (zoom && this._map.getZoom() > zoom) {
this._map.setZoom(zoom); this._map.setZoom(zoom);
} }
} }
_drawEntities(hass) { _drawEntities(hass) {
const map = this._map; const map = this._map;
if (!map) { if (!map) {
return; return;
} }
if (this._mapItems) { if (this._mapItems) {
this._mapItems.forEach((marker) => marker.remove()); this._mapItems.forEach((marker) => marker.remove());
} }
const mapItems = (this._mapItems = []); const mapItems = (this._mapItems = []);
this._configEntities.forEach((entity) => { this._configEntities.forEach((entity) => {
const entityId = entity.entity; const entityId = entity.entity;
if (!(entityId in hass.states)) { if (!(entityId in hass.states)) {
return; return;
} }
const stateObj = hass.states[entityId]; const stateObj = hass.states[entityId];
const title = computeStateName(stateObj); const title = computeStateName(stateObj);
const { const {
latitude, latitude,
longitude, longitude,
passive, passive,
icon, icon,
radius, radius,
entity_picture: entityPicture, entity_picture: entityPicture,
gps_accuracy: gpsAccuracy, gps_accuracy: gpsAccuracy,
} = stateObj.attributes; } = stateObj.attributes;
if (!(latitude && longitude)) { if (!(latitude && longitude)) {
return; return;
} }
let markerIcon; let markerIcon;
let iconHTML; let iconHTML;
let el; let el;
if (computeStateDomain(stateObj) === "zone") { if (computeStateDomain(stateObj) === "zone") {
// DRAW ZONE // DRAW ZONE
if (passive) return; if (passive) return;
// create icon // create icon
if (icon) { if (icon) {
el = document.createElement("ha-icon"); el = document.createElement("ha-icon");
el.setAttribute("icon", icon); el.setAttribute("icon", icon);
iconHTML = el.outerHTML; iconHTML = el.outerHTML;
} else { } else {
iconHTML = title; iconHTML = title;
} }
markerIcon = Leaflet.divIcon({ markerIcon = Leaflet.divIcon({
html: iconHTML, html: iconHTML,
iconSize: [24, 24], iconSize: [24, 24],
className: "", className: "",
}); });
// create market with the icon // create market with the icon
mapItems.push( mapItems.push(
Leaflet.marker([latitude, longitude], { Leaflet.marker([latitude, longitude], {
icon: markerIcon, icon: markerIcon,
interactive: false, interactive: false,
title: title, title: title,
}).addTo(map) }).addTo(map)
); );
// create circle around it // create circle around it
mapItems.push( mapItems.push(
Leaflet.circle([latitude, longitude], { Leaflet.circle([latitude, longitude], {
interactive: false, interactive: false,
color: "#FF9800", color: "#FF9800",
radius: radius, radius: radius,
}).addTo(map) }).addTo(map)
); );
return; return;
} }
// DRAW ENTITY // DRAW ENTITY
// create icon // create icon
const entityName = title const entityName = title
.split(" ") .split(" ")
.map((part) => part[0]) .map((part) => part[0])
.join("") .join("")
.substr(0, 3); .substr(0, 3);
el = document.createElement("ha-entity-marker"); el = document.createElement("ha-entity-marker");
el.setAttribute("entity-id", entityId); el.setAttribute("entity-id", entityId);
el.setAttribute("entity-name", entityName); el.setAttribute("entity-name", entityName);
el.setAttribute("entity-picture", entityPicture || ""); el.setAttribute("entity-picture", entityPicture || "");
/* Leaflet clones this element before adding it to the map. This messes up /* Leaflet clones this element before adding it to the map. This messes up
our Polymer object and we can't pass data through. Thus we hack like this. */ our Polymer object and we can't pass data through. Thus we hack like this. */
markerIcon = Leaflet.divIcon({ markerIcon = Leaflet.divIcon({
html: el.outerHTML, html: el.outerHTML,
iconSize: [48, 48], iconSize: [48, 48],
className: "", className: "",
}); });
// create market with the icon // create market with the icon
mapItems.push( mapItems.push(
Leaflet.marker([latitude, longitude], { Leaflet.marker([latitude, longitude], {
icon: markerIcon, icon: markerIcon,
title: computeStateName(stateObj), title: computeStateName(stateObj),
}).addTo(map) }).addTo(map)
); );
// create circle around if entity has accuracy // create circle around if entity has accuracy
if (gpsAccuracy) { if (gpsAccuracy) {
mapItems.push( mapItems.push(
Leaflet.circle([latitude, longitude], { Leaflet.circle([latitude, longitude], {
interactive: false, interactive: false,
color: "#0288D1", color: "#0288D1",
radius: gpsAccuracy, radius: gpsAccuracy,
}).addTo(map) }).addTo(map)
); );
} }
}); });
} }
} }
customElements.define("hui-map-card", HuiMapCard); customElements.define("hui-map-card", HuiMapCard);

View File

@@ -1,93 +1,93 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { classMap } from "lit-html/directives/classMap"; import { classMap } from "lit-html/directives/classMap";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-markdown"; import "../../../components/ha-markdown";
import { LovelaceCard, LovelaceConfig } from "../types"; import { LovelaceCard, LovelaceConfig } from "../types";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
interface Config extends LovelaceConfig { interface Config extends LovelaceConfig {
content: string; content: string;
title?: string; title?: string;
} }
export class HuiMarkdownCard extends LitElement implements LovelaceCard { export class HuiMarkdownCard extends LitElement implements LovelaceCard {
private _config?: Config; private _config?: Config;
static get properties(): PropertyDeclarations { static get properties(): PropertyDeclarations {
return { return {
_config: {}, _config: {},
}; };
} }
public getCardSize(): number { public getCardSize(): number {
return this._config!.content.split("\n").length; return this._config!.content.split("\n").length;
} }
public setConfig(config: Config): void { public setConfig(config: Config): void {
if (!config.content) { if (!config.content) {
throw new Error("Invalid Configuration: Content Required"); throw new Error("Invalid Configuration: Content Required");
} }
this._config = config; this._config = config;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config) { if (!this._config) {
return html``; return html``;
} }
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<ha-card .header="${this._config.title}"> <ha-card .header="${this._config.title}">
<ha-markdown <ha-markdown
class="markdown ${classMap({ class="markdown ${classMap({
"no-header": !this._config.title, "no-header": !this._config.title,
})}" })}"
.content="${this._config.content}" .content="${this._config.content}"
></ha-markdown> ></ha-markdown>
</ha-card> </ha-card>
`; `;
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
<style> <style>
:host { :host {
@apply --paper-font-body1; @apply --paper-font-body1;
} }
ha-markdown { ha-markdown {
display: block; display: block;
padding: 0 16px 16px; padding: 0 16px 16px;
-ms-user-select: initial; -ms-user-select: initial;
-webkit-user-select: initial; -webkit-user-select: initial;
-moz-user-select: initial; -moz-user-select: initial;
} }
.markdown.no-header { .markdown.no-header {
padding-top: 16px; padding-top: 16px;
} }
ha-markdown > *:first-child { ha-markdown > *:first-child {
margin-top: 0; margin-top: 0;
} }
ha-markdown > *:last-child { ha-markdown > *:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
ha-markdown a { ha-markdown a {
color: var(--primary-color); color: var(--primary-color);
} }
ha-markdown img { ha-markdown img {
max-width: 100%; max-width: 100%;
} }
</style> </style>
`; `;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-markdown-card": HuiMarkdownCard; "hui-markdown-card": HuiMarkdownCard;
} }
} }
customElements.define("hui-markdown-card", HuiMarkdownCard); customElements.define("hui-markdown-card", HuiMarkdownCard);

View File

@@ -1,11 +1,11 @@
import "../../../cards/ha-media_player-card"; import "../../../cards/ha-media_player-card";
import LegacyWrapperCard from "./hui-legacy-wrapper-card"; import LegacyWrapperCard from "./hui-legacy-wrapper-card";
class HuiMediaControlCard extends LegacyWrapperCard { class HuiMediaControlCard extends LegacyWrapperCard {
constructor() { constructor() {
super("ha-media_player-card", "media_player"); super("ha-media_player-card", "media_player");
} }
} }
customElements.define("hui-media-control-card", HuiMediaControlCard); customElements.define("hui-media-control-card", HuiMediaControlCard);

View File

@@ -1,67 +1,67 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card"; import "../../../components/ha-card";
import NavigateMixin from "../../../mixins/navigate-mixin"; import NavigateMixin from "../../../mixins/navigate-mixin";
/* /*
* @appliesMixin NavigateMixin * @appliesMixin NavigateMixin
*/ */
class HuiPictureCard extends NavigateMixin(PolymerElement) { class HuiPictureCard extends NavigateMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
<style> <style>
ha-card { ha-card {
overflow: hidden; overflow: hidden;
} }
ha-card[clickable] { ha-card[clickable] {
cursor: pointer; cursor: pointer;
} }
img { img {
display: block; display: block;
width: 100%; width: 100%;
} }
</style> </style>
<ha-card on-click="_cardClicked" clickable$='[[_computeClickable(_config)]]'> <ha-card on-click="_cardClicked" clickable$='[[_computeClickable(_config)]]'>
<img src='[[_config.image]]' /> <img src='[[_config.image]]' />
</ha-card> </ha-card>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
}; };
} }
getCardSize() { getCardSize() {
return 3; return 3;
} }
setConfig(config) { setConfig(config) {
if (!config || !config.image) { if (!config || !config.image) {
throw new Error("Error in card configuration."); throw new Error("Error in card configuration.");
} }
this._config = config; this._config = config;
} }
_computeClickable(config) { _computeClickable(config) {
return config.navigation_path || config.service; return config.navigation_path || config.service;
} }
_cardClicked() { _cardClicked() {
if (this._config.navigation_path) { if (this._config.navigation_path) {
this.navigate(this._config.navigation_path); this.navigate(this._config.navigation_path);
} }
if (this._config.service) { if (this._config.service) {
const [domain, service] = this._config.service.split(".", 2); const [domain, service] = this._config.service.split(".", 2);
this.hass.callService(domain, service, this._config.service_data); this.hass.callService(domain, service, this._config.service_data);
} }
} }
} }
customElements.define("hui-picture-card", HuiPictureCard); customElements.define("hui-picture-card", HuiPictureCard);

View File

@@ -1,111 +1,111 @@
import { html, LitElement } from "@polymer/lit-element"; import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
import createHuiElement from "../common/create-hui-element"; import createHuiElement from "../common/create-hui-element";
import { LovelaceCard, LovelaceConfig } from "../types"; import { LovelaceCard, LovelaceConfig } from "../types";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { LovelaceElementConfig, LovelaceElement } from "../elements/types"; import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
interface Config extends LovelaceConfig { interface Config extends LovelaceConfig {
title?: string; title?: string;
image: string; image: string;
elements: LovelaceElementConfig[]; elements: LovelaceElementConfig[];
} }
class HuiPictureElementsCard extends LitElement implements LovelaceCard { class HuiPictureElementsCard extends LitElement implements LovelaceCard {
private _config?: Config; private _config?: Config;
private _hass?: HomeAssistant; private _hass?: HomeAssistant;
static get properties() { static get properties() {
return { return {
_config: {}, _config: {},
}; };
} }
set hass(hass: HomeAssistant) { set hass(hass: HomeAssistant) {
this._hass = hass; this._hass = hass;
for (const el of this.shadowRoot!.querySelectorAll("#root > *")) { for (const el of this.shadowRoot!.querySelectorAll("#root > *")) {
const element = el as LovelaceElement; const element = el as LovelaceElement;
element.hass = this._hass; element.hass = this._hass;
} }
} }
public getCardSize(): number { public getCardSize(): number {
return 4; return 4;
} }
public setConfig(config: Config): void { public setConfig(config: Config): void {
if (!config) { if (!config) {
throw new Error("Invalid Configuration"); throw new Error("Invalid Configuration");
} else if (!config.image) { } else if (!config.image) {
throw new Error("Invalid Configuration: image required"); throw new Error("Invalid Configuration: image required");
} else if (!Array.isArray(config.elements)) { } else if (!Array.isArray(config.elements)) {
throw new Error("Invalid Configuration: elements required"); throw new Error("Invalid Configuration: elements required");
} }
this._config = config; this._config = config;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config) { if (!this._config) {
return html``; return html``;
} }
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<ha-card .header="${this._config.title}"> <ha-card .header="${this._config.title}">
<div id="root"> <div id="root">
<img src="${this._config.image}"> <img src="${this._config.image}">
${this._config.elements.map((elementConfig: LovelaceElementConfig) => ${this._config.elements.map((elementConfig: LovelaceElementConfig) =>
this._createHuiElement(elementConfig) this._createHuiElement(elementConfig)
)} )}
</div> </div>
</ha-card> </ha-card>
`; `;
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
<style> <style>
ha-card { ha-card {
overflow: hidden; overflow: hidden;
} }
#root { #root {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
#root img { #root img {
display: block; display: block;
width: 100%; width: 100%;
} }
.element { .element {
position: absolute; position: absolute;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
</style> </style>
`; `;
} }
private _createHuiElement( private _createHuiElement(
elementConfig: LovelaceElementConfig elementConfig: LovelaceElementConfig
): LovelaceElement { ): LovelaceElement {
const element = createHuiElement(elementConfig) as LovelaceElement; const element = createHuiElement(elementConfig) as LovelaceElement;
element.hass = this._hass; element.hass = this._hass;
element.classList.add("element"); element.classList.add("element");
Object.keys(elementConfig.style).forEach((prop) => { Object.keys(elementConfig.style).forEach((prop) => {
element.style.setProperty(prop, elementConfig.style[prop]); element.style.setProperty(prop, elementConfig.style[prop]);
}); });
return element; return element;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-picture-elements-card": HuiPictureElementsCard; "hui-picture-elements-card": HuiPictureElementsCard;
} }
} }
customElements.define("hui-picture-elements-card", HuiPictureElementsCard); customElements.define("hui-picture-elements-card", HuiPictureElementsCard);

View File

@@ -1,201 +1,201 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../components/hui-image"; import "../components/hui-image";
import computeDomain from "../../../common/entity/compute_domain"; import computeDomain from "../../../common/entity/compute_domain";
import computeStateDisplay from "../../../common/entity/compute_state_display"; import computeStateDisplay from "../../../common/entity/compute_state_display";
import computeStateName from "../../../common/entity/compute_state_name"; import computeStateName from "../../../common/entity/compute_state_name";
import toggleEntity from "../common/entity/toggle-entity"; import toggleEntity from "../common/entity/toggle-entity";
import EventsMixin from "../../../mixins/events-mixin"; import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin";
import { longPressBind } from "../common/directives/long-press-directive"; import { longPressBind } from "../common/directives/long-press-directive";
const UNAVAILABLE = "Unavailable"; const UNAVAILABLE = "Unavailable";
/* /*
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
*/ */
class HuiPictureEntityCard extends EventsMixin(LocalizeMixin(PolymerElement)) { class HuiPictureEntityCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() { static get template() {
return html` return html`
<style> <style>
ha-card { ha-card {
min-height: 75px; min-height: 75px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
ha-card.canInteract { ha-card.canInteract {
cursor: pointer; cursor: pointer;
} }
.footer { .footer {
@apply --paper-font-common-nowrap; @apply --paper-font-common-nowrap;
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
padding: 16px; padding: 16px;
font-size: 16px; font-size: 16px;
line-height: 16px; line-height: 16px;
color: white; color: white;
} }
.both { .both {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.state { .state {
text-align: right; text-align: right;
} }
</style> </style>
<ha-card id='card'> <ha-card id='card'>
<hui-image <hui-image
hass="[[hass]]" hass="[[hass]]"
image="[[_config.image]]" image="[[_config.image]]"
state-image="[[_config.state_image]]" state-image="[[_config.state_image]]"
camera-image="[[_getCameraImage(_config)]]" camera-image="[[_getCameraImage(_config)]]"
entity="[[_config.entity]]" entity="[[_config.entity]]"
aspect-ratio="[[_config.aspect_ratio]]" aspect-ratio="[[_config.aspect_ratio]]"
></hui-image> ></hui-image>
<template is="dom-if" if="[[_showNameAndState(_config)]]"> <template is="dom-if" if="[[_showNameAndState(_config)]]">
<div class="footer both"> <div class="footer both">
<div>[[_name]]</div> <div>[[_name]]</div>
<div>[[_state]]</div> <div>[[_state]]</div>
</div> </div>
</template> </template>
<template is="dom-if" if="[[_showName(_config)]]"> <template is="dom-if" if="[[_showName(_config)]]">
<div class="footer"> <div class="footer">
[[_name]] [[_name]]
</div> </div>
</template> </template>
<template is="dom-if" if="[[_showState(_config)]]"> <template is="dom-if" if="[[_showState(_config)]]">
<div class="footer state"> <div class="footer state">
[[_state]] [[_state]]
</div> </div>
</template> </template>
</ha-card> </ha-card>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: { hass: {
type: Object, type: Object,
observer: "_hassChanged", observer: "_hassChanged",
}, },
_config: Object, _config: Object,
_name: String, _name: String,
_state: String, _state: String,
}; };
} }
getCardSize() { getCardSize() {
return 3; return 3;
} }
setConfig(config) { setConfig(config) {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Error in card configuration."); throw new Error("Error in card configuration.");
} }
this._entityDomain = computeDomain(config.entity); this._entityDomain = computeDomain(config.entity);
if ( if (
this._entityDomain !== "camera" && this._entityDomain !== "camera" &&
(!config.image && !config.state_image && !config.camera_image) (!config.image && !config.state_image && !config.camera_image)
) { ) {
throw new Error("No image source configured."); throw new Error("No image source configured.");
} }
this._config = config; this._config = config;
} }
ready() { ready() {
super.ready(); super.ready();
const card = this.shadowRoot.querySelector("#card"); const card = this.shadowRoot.querySelector("#card");
longPressBind(card); longPressBind(card);
card.addEventListener("ha-click", () => this._cardClicked(false)); card.addEventListener("ha-click", () => this._cardClicked(false));
card.addEventListener("ha-hold", () => this._cardClicked(true)); card.addEventListener("ha-hold", () => this._cardClicked(true));
} }
_hassChanged(hass) { _hassChanged(hass) {
const config = this._config; const config = this._config;
const entityId = config.entity; const entityId = config.entity;
const stateObj = hass.states[entityId]; const stateObj = hass.states[entityId];
// Nothing changed // Nothing changed
if ( if (
(!stateObj && this._oldState === UNAVAILABLE) || (!stateObj && this._oldState === UNAVAILABLE) ||
(stateObj && stateObj.state === this._oldState) (stateObj && stateObj.state === this._oldState)
) { ) {
return; return;
} }
let name; let name;
let state; let state;
let stateLabel; let stateLabel;
let available; let available;
if (stateObj) { if (stateObj) {
name = config.name || computeStateName(stateObj); name = config.name || computeStateName(stateObj);
state = stateObj.state; state = stateObj.state;
stateLabel = computeStateDisplay(this.localize, stateObj); stateLabel = computeStateDisplay(this.localize, stateObj);
available = true; available = true;
} else { } else {
name = config.name || entityId; name = config.name || entityId;
state = UNAVAILABLE; state = UNAVAILABLE;
stateLabel = this.localize("state.default.unavailable"); stateLabel = this.localize("state.default.unavailable");
available = false; available = false;
} }
this.setProperties({ this.setProperties({
_name: name, _name: name,
_state: stateLabel, _state: stateLabel,
_oldState: state, _oldState: state,
}); });
this.$.card.classList.toggle("canInteract", available); this.$.card.classList.toggle("canInteract", available);
} }
_showNameAndState(config) { _showNameAndState(config) {
return config.show_name !== false && config.show_state !== false; return config.show_name !== false && config.show_state !== false;
} }
_showName(config) { _showName(config) {
return config.show_name !== false && config.show_state === false; return config.show_name !== false && config.show_state === false;
} }
_showState(config) { _showState(config) {
return config.show_name === false && config.show_state !== false; return config.show_name === false && config.show_state !== false;
} }
_cardClicked(hold) { _cardClicked(hold) {
const config = this._config; const config = this._config;
const entityId = config.entity; const entityId = config.entity;
if (!(entityId in this.hass.states)) return; if (!(entityId in this.hass.states)) return;
const action = hold ? config.hold_action : config.tap_action || "more-info"; const action = hold ? config.hold_action : config.tap_action || "more-info";
switch (action) { switch (action) {
case "toggle": case "toggle":
toggleEntity(this.hass, entityId); toggleEntity(this.hass, entityId);
break; break;
case "more-info": case "more-info":
this.fire("hass-more-info", { entityId }); this.fire("hass-more-info", { entityId });
break; break;
default: default:
} }
} }
_getCameraImage(config) { _getCameraImage(config) {
return this._entityDomain === "camera" return this._entityDomain === "camera"
? config.entity ? config.entity
: config.camera_image; : config.camera_image;
} }
} }
customElements.define("hui-picture-entity-card", HuiPictureEntityCard); customElements.define("hui-picture-entity-card", HuiPictureEntityCard);

View File

@@ -1,195 +1,195 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import "../components/hui-image"; import "../components/hui-image";
import computeStateDisplay from "../../../common/entity/compute_state_display"; import computeStateDisplay from "../../../common/entity/compute_state_display";
import computeStateName from "../../../common/entity/compute_state_name"; import computeStateName from "../../../common/entity/compute_state_name";
import { DOMAINS_TOGGLE } from "../../../common/const"; import { DOMAINS_TOGGLE } from "../../../common/const";
import stateIcon from "../../../common/entity/state_icon"; import stateIcon from "../../../common/entity/state_icon";
import toggleEntity from "../common/entity/toggle-entity"; import toggleEntity from "../common/entity/toggle-entity";
import processConfigEntities from "../common/process-config-entities"; import processConfigEntities from "../common/process-config-entities";
import EventsMixin from "../../../mixins/events-mixin"; import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin";
import NavigateMixin from "../../../mixins/navigate-mixin"; import NavigateMixin from "../../../mixins/navigate-mixin";
import computeDomain from "../../../common/entity/compute_domain"; import computeDomain from "../../../common/entity/compute_domain";
const STATES_OFF = new Set(["closed", "locked", "not_home", "off"]); const STATES_OFF = new Set(["closed", "locked", "not_home", "off"]);
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
* @appliesMixin NavigateMixin * @appliesMixin NavigateMixin
*/ */
class HuiPictureGlanceCard extends NavigateMixin( class HuiPictureGlanceCard extends NavigateMixin(
LocalizeMixin(EventsMixin(PolymerElement)) LocalizeMixin(EventsMixin(PolymerElement))
) { ) {
static get template() { static get template() {
return html` return html`
<style> <style>
ha-card { ha-card {
position: relative; position: relative;
min-height: 48px; min-height: 48px;
overflow: hidden; overflow: hidden;
} }
hui-image.clickable { hui-image.clickable {
cursor: pointer; cursor: pointer;
} }
.box { .box {
@apply --paper-font-common-nowrap; @apply --paper-font-common-nowrap;
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
padding: 4px 8px; padding: 4px 8px;
font-size: 16px; font-size: 16px;
line-height: 40px; line-height: 40px;
color: white; color: white;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.box .title { .box .title {
font-weight: 500; font-weight: 500;
margin-left: 8px; margin-left: 8px;
} }
ha-icon { ha-icon {
cursor: pointer; cursor: pointer;
padding: 8px; padding: 8px;
color: #A9A9A9; color: #A9A9A9;
} }
ha-icon.state-on { ha-icon.state-on {
color: white; color: white;
} }
</style> </style>
<ha-card> <ha-card>
<hui-image <hui-image
class$='[[_computeImageClass(_config)]]' class$='[[_computeImageClass(_config)]]'
on-click='_handleImageClick' on-click='_handleImageClick'
hass="[[hass]]" hass="[[hass]]"
image="[[_config.image]]" image="[[_config.image]]"
state-image="[[_config.state_image]]" state-image="[[_config.state_image]]"
camera-image="[[_config.camera_image]]" camera-image="[[_config.camera_image]]"
entity="[[_config.entity]]" entity="[[_config.entity]]"
aspect-ratio="[[_config.aspect_ratio]]" aspect-ratio="[[_config.aspect_ratio]]"
></hui-image> ></hui-image>
<div class="box"> <div class="box">
<template is="dom-if" if="[[_config.title]]"> <template is="dom-if" if="[[_config.title]]">
<div class="title">[[_config.title]]</div> <div class="title">[[_config.title]]</div>
</template> </template>
<div> <div>
<template is="dom-repeat" items="[[_computeVisible(_entitiesDialog, hass.states)]]"> <template is="dom-repeat" items="[[_computeVisible(_entitiesDialog, hass.states)]]">
<ha-icon <ha-icon
on-click="_openDialog" on-click="_openDialog"
class$="[[_computeButtonClass(item.entity, hass.states)]]" class$="[[_computeButtonClass(item.entity, hass.states)]]"
icon="[[_computeIcon(item, hass.states)]]" icon="[[_computeIcon(item, hass.states)]]"
title="[[_computeTooltip(item.entity, hass.states)]]" title="[[_computeTooltip(item.entity, hass.states)]]"
></ha-icon> ></ha-icon>
</template> </template>
</div> </div>
<div> <div>
<template is="dom-repeat" items="[[_computeVisible(_entitiesToggle, hass.states)]]"> <template is="dom-repeat" items="[[_computeVisible(_entitiesToggle, hass.states)]]">
<ha-icon <ha-icon
on-click="_callService" on-click="_callService"
class$="[[_computeButtonClass(item.entity, hass.states)]]" class$="[[_computeButtonClass(item.entity, hass.states)]]"
icon="[[_computeIcon(item, hass.states)]]" icon="[[_computeIcon(item, hass.states)]]"
title="[[_computeTooltip(item.entity, hass.states)]]" title="[[_computeTooltip(item.entity, hass.states)]]"
></ha-icon> ></ha-icon>
</template> </template>
</div> </div>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
_entitiesDialog: Array, _entitiesDialog: Array,
_entitiesToggle: Array, _entitiesToggle: Array,
}; };
} }
getCardSize() { getCardSize() {
return 3; return 3;
} }
setConfig(config) { setConfig(config) {
if ( if (
!config || !config ||
!config.entities || !config.entities ||
!Array.isArray(config.entities) || !Array.isArray(config.entities) ||
!(config.image || config.camera_image || config.state_image) || !(config.image || config.camera_image || config.state_image) ||
(config.state_image && !config.entity) (config.state_image && !config.entity)
) { ) {
throw new Error("Invalid card configuration"); throw new Error("Invalid card configuration");
} }
const entities = processConfigEntities(config.entities); const entities = processConfigEntities(config.entities);
const dialog = []; const dialog = [];
const toggle = []; const toggle = [];
entities.forEach((item) => { entities.forEach((item) => {
if ( if (
config.force_dialog || config.force_dialog ||
!DOMAINS_TOGGLE.has(computeDomain(item.entity)) !DOMAINS_TOGGLE.has(computeDomain(item.entity))
) { ) {
dialog.push(item); dialog.push(item);
} else { } else {
toggle.push(item); toggle.push(item);
} }
}); });
this.setProperties({ this.setProperties({
_config: config, _config: config,
_entitiesDialog: dialog, _entitiesDialog: dialog,
_entitiesToggle: toggle, _entitiesToggle: toggle,
}); });
} }
_computeVisible(collection, states) { _computeVisible(collection, states) {
return collection.filter((el) => el.entity in states); return collection.filter((el) => el.entity in states);
} }
_computeIcon(item, states) { _computeIcon(item, states) {
return item.icon || stateIcon(states[item.entity]); return item.icon || stateIcon(states[item.entity]);
} }
_computeButtonClass(entityId, states) { _computeButtonClass(entityId, states) {
return STATES_OFF.has(states[entityId].state) ? "" : "state-on"; return STATES_OFF.has(states[entityId].state) ? "" : "state-on";
} }
_computeTooltip(entityId, states) { _computeTooltip(entityId, states) {
return `${computeStateName(states[entityId])}: ${computeStateDisplay( return `${computeStateName(states[entityId])}: ${computeStateDisplay(
this.localize, this.localize,
states[entityId] states[entityId]
)}`; )}`;
} }
_computeImageClass(config) { _computeImageClass(config) {
return config.navigation_path || config.camera_image ? "clickable" : ""; return config.navigation_path || config.camera_image ? "clickable" : "";
} }
_openDialog(ev) { _openDialog(ev) {
this.fire("hass-more-info", { entityId: ev.model.item.entity }); this.fire("hass-more-info", { entityId: ev.model.item.entity });
} }
_callService(ev) { _callService(ev) {
toggleEntity(this.hass, ev.model.item.entity); toggleEntity(this.hass, ev.model.item.entity);
} }
_handleImageClick() { _handleImageClick() {
if (this._config.navigation_path) { if (this._config.navigation_path) {
this.navigate(this._config.navigation_path); this.navigate(this._config.navigation_path);
return; return;
} }
if (this._config.camera_image) { if (this._config.camera_image) {
this.fire("hass-more-info", { entityId: this._config.camera_image }); this.fire("hass-more-info", { entityId: this._config.camera_image });
} }
} }
} }
customElements.define("hui-picture-glance-card", HuiPictureGlanceCard); customElements.define("hui-picture-glance-card", HuiPictureGlanceCard);

View File

@@ -1,11 +1,11 @@
import "../../../cards/ha-plant-card"; import "../../../cards/ha-plant-card";
import LegacyWrapperCard from "./hui-legacy-wrapper-card"; import LegacyWrapperCard from "./hui-legacy-wrapper-card";
class HuiPlantStatusCard extends LegacyWrapperCard { class HuiPlantStatusCard extends LegacyWrapperCard {
constructor() { constructor() {
super("ha-plant-card", "plant"); super("ha-plant-card", "plant");
} }
} }
customElements.define("hui-plant-status-card", HuiPlantStatusCard); customElements.define("hui-plant-status-card", HuiPlantStatusCard);

View File

@@ -1,292 +1,292 @@
import { LitElement, html, svg } from "@polymer/lit-element"; import { LitElement, html, svg } from "@polymer/lit-element";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import computeStateName from "../../../common/entity/compute_state_name"; import computeStateName from "../../../common/entity/compute_state_name";
import stateIcon from "../../../common/entity/state_icon"; import stateIcon from "../../../common/entity/state_icon";
import EventsMixin from "../../../mixins/events-mixin"; import EventsMixin from "../../../mixins/events-mixin";
class HuiSensorCard extends EventsMixin(LitElement) { class HuiSensorCard extends EventsMixin(LitElement) {
set hass(hass) { set hass(hass) {
this._hass = hass; this._hass = hass;
const entity = hass.states[this._config.entity]; const entity = hass.states[this._config.entity];
if (entity && this._entity !== entity) { if (entity && this._entity !== entity) {
this._entity = entity; this._entity = entity;
if ( if (
this._config.graph !== "none" && this._config.graph !== "none" &&
entity.attributes.unit_of_measurement entity.attributes.unit_of_measurement
) { ) {
this._getHistory(); this._getHistory();
} }
} }
} }
static get properties() { static get properties() {
return { return {
_hass: {}, _hass: {},
_config: {}, _config: {},
_entity: {}, _entity: {},
_line: String, _line: String,
}; };
} }
setConfig(config) { setConfig(config) {
if (!config.entity || config.entity.split(".")[0] !== "sensor") { if (!config.entity || config.entity.split(".")[0] !== "sensor") {
throw new Error("Specify an entity from within the sensor domain."); throw new Error("Specify an entity from within the sensor domain.");
} }
const cardConfig = { const cardConfig = {
icon: false, icon: false,
hours_to_show: 24, hours_to_show: 24,
accuracy: 10, accuracy: 10,
height: 100, height: 100,
line_width: 5, line_width: 5,
line_color: "var(--accent-color)", line_color: "var(--accent-color)",
...config, ...config,
}; };
cardConfig.hours_to_show = Number(cardConfig.hours_to_show); cardConfig.hours_to_show = Number(cardConfig.hours_to_show);
cardConfig.accuracy = Number(cardConfig.accuracy); cardConfig.accuracy = Number(cardConfig.accuracy);
cardConfig.height = Number(cardConfig.height); cardConfig.height = Number(cardConfig.height);
cardConfig.line_width = Number(cardConfig.line_width); cardConfig.line_width = Number(cardConfig.line_width);
this._config = cardConfig; this._config = cardConfig;
} }
shouldUpdate(changedProps) { shouldUpdate(changedProps) {
const change = changedProps.has("_entity") || changedProps.has("_line"); const change = changedProps.has("_entity") || changedProps.has("_line");
return change; return change;
} }
render({ _config, _entity, _line } = this) { render({ _config, _entity, _line } = this) {
return html` return html`
${this._style()} ${this._style()}
<ha-card @click=${this._handleClick}> <ha-card @click=${this._handleClick}>
<div class='flex'> <div class='flex'>
<div class='icon'> <div class='icon'>
<ha-icon .icon=${this._computeIcon(_entity)}></ha-icon> <ha-icon .icon=${this._computeIcon(_entity)}></ha-icon>
</div> </div>
<div class='header'> <div class='header'>
<span class='name'>${this._computeName(_entity)}</span> <span class='name'>${this._computeName(_entity)}</span>
</div> </div>
</div> </div>
<div class='flex info'> <div class='flex info'>
<span id='value'>${_entity.state}</span> <span id='value'>${_entity.state}</span>
<span id='measurement'>${this._computeUom(_entity)}</span> <span id='measurement'>${this._computeUom(_entity)}</span>
</div> </div>
<div class='graph'> <div class='graph'>
<div> <div>
${ ${
_line _line
? svg` ? svg`
<svg width='100%' height='100%' viewBox='0 0 500 ${_config.height}'> <svg width='100%' height='100%' viewBox='0 0 500 ${_config.height}'>
<path d=${_line} fill='none' stroke=${_config.line_color} <path d=${_line} fill='none' stroke=${_config.line_color}
stroke-width=${_config.line_width} stroke-width=${_config.line_width}
stroke-linecap='round' stroke-linejoin='round' /> stroke-linecap='round' stroke-linejoin='round' />
</svg>` </svg>`
: "" : ""
} }
</div> </div>
</div> </div>
</ha-card>`; </ha-card>`;
} }
_handleClick() { _handleClick() {
this.fire("hass-more-info", { entityId: this._config.entity }); this.fire("hass-more-info", { entityId: this._config.entity });
} }
_computeIcon(item) { _computeIcon(item) {
return this._config.icon || stateIcon(item); return this._config.icon || stateIcon(item);
} }
_computeName(item) { _computeName(item) {
return this._config.name || computeStateName(item); return this._config.name || computeStateName(item);
} }
_computeUom(item) { _computeUom(item) {
return this._config.unit || item.attributes.unit_of_measurement; return this._config.unit || item.attributes.unit_of_measurement;
} }
_getGraph(items, width, height) { _getGraph(items, width, height) {
const values = this._getValueArr(items); const values = this._getValueArr(items);
const coords = this._calcCoordinates(values, width, height); const coords = this._calcCoordinates(values, width, height);
return this._getPath(coords); return this._getPath(coords);
} }
_getValueArr(items) { _getValueArr(items) {
return items.map((item) => Number(item.state) || 0); return items.map((item) => Number(item.state) || 0);
} }
_calcCoordinates(values, width, height) { _calcCoordinates(values, width, height) {
const margin = this._config.line_width; const margin = this._config.line_width;
width -= margin * 2; width -= margin * 2;
height -= margin * 2; height -= margin * 2;
const min = Math.floor(Math.min.apply(null, values) * 0.95); const min = Math.floor(Math.min.apply(null, values) * 0.95);
const max = Math.ceil(Math.max.apply(null, values) * 1.05); const max = Math.ceil(Math.max.apply(null, values) * 1.05);
if (values.length === 1) values.push(values[0]); if (values.length === 1) values.push(values[0]);
const yRatio = (max - min) / height; const yRatio = (max - min) / height;
const xRatio = width / (values.length - 1); const xRatio = width / (values.length - 1);
return values.map((value, i) => { return values.map((value, i) => {
const y = height - (value - min) / yRatio || 0; const y = height - (value - min) / yRatio || 0;
const x = xRatio * i + margin; const x = xRatio * i + margin;
return [x, y]; return [x, y];
}); });
} }
_getPath(points) { _getPath(points) {
const SPACE = " "; const SPACE = " ";
let next; let next;
let Z; let Z;
const X = 0; const X = 0;
const Y = 1; const Y = 1;
let path = ""; let path = "";
let point = points[0]; let point = points[0];
path += "M" + point[X] + "," + point[Y]; path += "M" + point[X] + "," + point[Y];
const first = point; const first = point;
for (let i = 0; i < points.length; i++) { for (let i = 0; i < points.length; i++) {
next = points[i]; next = points[i];
Z = this._midPoint(point[X], point[Y], next[X], next[Y]); Z = this._midPoint(point[X], point[Y], next[X], next[Y]);
path += SPACE + Z[X] + "," + Z[Y]; path += SPACE + Z[X] + "," + Z[Y];
path += "Q" + Math.floor(next[X]) + "," + next[Y]; path += "Q" + Math.floor(next[X]) + "," + next[Y];
point = next; point = next;
} }
const second = points[1]; const second = points[1];
Z = this._midPoint(first[X], first[Y], second[X], second[Y]); Z = this._midPoint(first[X], first[Y], second[X], second[Y]);
path += SPACE + Math.floor(next[X]) + "." + points[points.length - 1]; path += SPACE + Math.floor(next[X]) + "." + points[points.length - 1];
return path; return path;
} }
_midPoint(Ax, Ay, Bx, By) { _midPoint(Ax, Ay, Bx, By) {
const Zx = (Ax - Bx) / 2 + Bx; const Zx = (Ax - Bx) / 2 + Bx;
const Zy = (Ay - By) / 2 + By; const Zy = (Ay - By) / 2 + By;
return [Zx, Zy]; return [Zx, Zy];
} }
async _getHistory() { async _getHistory() {
const endTime = new Date(); const endTime = new Date();
const startTime = new Date(); const startTime = new Date();
startTime.setHours(endTime.getHours() - this._config.hours_to_show); startTime.setHours(endTime.getHours() - this._config.hours_to_show);
const stateHistory = await this._fetchRecent( const stateHistory = await this._fetchRecent(
this._config.entity, this._config.entity,
startTime, startTime,
endTime endTime
); );
const history = stateHistory[0]; const history = stateHistory[0];
const valArray = [history[history.length - 1]]; const valArray = [history[history.length - 1]];
let pos = history.length - 1; let pos = history.length - 1;
const accuracy = this._config.accuracy <= pos ? this._config.accuracy : pos; const accuracy = this._config.accuracy <= pos ? this._config.accuracy : pos;
let increment = Math.ceil(history.length / accuracy); let increment = Math.ceil(history.length / accuracy);
increment = increment <= 0 ? 1 : increment; increment = increment <= 0 ? 1 : increment;
for (let i = accuracy; i >= 2; i--) { for (let i = accuracy; i >= 2; i--) {
pos -= increment; pos -= increment;
valArray.unshift(pos >= 0 ? history[pos] : history[0]); valArray.unshift(pos >= 0 ? history[pos] : history[0]);
} }
this._line = this._getGraph(valArray, 500, this._config.height); this._line = this._getGraph(valArray, 500, this._config.height);
} }
async _fetchRecent(entityId, startTime, endTime) { async _fetchRecent(entityId, startTime, endTime) {
let url = "history/period"; let url = "history/period";
if (startTime) url += "/" + startTime.toISOString(); if (startTime) url += "/" + startTime.toISOString();
url += "?filter_entity_id=" + entityId; url += "?filter_entity_id=" + entityId;
if (endTime) url += "&end_time=" + endTime.toISOString(); if (endTime) url += "&end_time=" + endTime.toISOString();
return await this._hass.callApi("GET", url); return await this._hass.callApi("GET", url);
} }
getCardSize() { getCardSize() {
return 3; return 3;
} }
_style() { _style() {
return html` return html`
<style> <style>
:host { :host {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
ha-card { ha-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
padding: 16px; padding: 16px;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
} }
.flex { .flex {
display: flex; display: flex;
} }
.header { .header {
align-items: center; align-items: center;
display: flex; display: flex;
min-width: 0; min-width: 0;
opacity: .8; opacity: .8;
position: relative; position: relative;
} }
.name { .name {
display: block; display: block;
display: -webkit-box; display: -webkit-box;
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 500; font-weight: 500;
max-height: 1.4rem; max-height: 1.4rem;
margin-top: 2px; margin-top: 2px;
opacity: .8; opacity: .8;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
word-wrap: break-word; word-wrap: break-word;
word-break: break-all; word-break: break-all;
} }
.icon { .icon {
color: var(--paper-item-icon-color, #44739e); color: var(--paper-item-icon-color, #44739e);
display: inline-block; display: inline-block;
flex: 0 0 40px; flex: 0 0 40px;
line-height: 40px; line-height: 40px;
position: relative; position: relative;
text-align: center; text-align: center;
width: 40px; width: 40px;
} }
.info { .info {
flex-wrap: wrap; flex-wrap: wrap;
margin: 16px 0 16px 8px; margin: 16px 0 16px 8px;
} }
#value { #value {
display: inline-block; display: inline-block;
font-size: 2rem; font-size: 2rem;
font-weight: 400; font-weight: 400;
line-height: 1em; line-height: 1em;
margin-right: 4px; margin-right: 4px;
} }
#measurement { #measurement {
align-self: flex-end; align-self: flex-end;
display: inline-block; display: inline-block;
font-size: 1.3rem; font-size: 1.3rem;
line-height: 1.2em; line-height: 1.2em;
margin-top: .1em; margin-top: .1em;
opacity: .6; opacity: .6;
vertical-align: bottom; vertical-align: bottom;
} }
.graph { .graph {
align-self: flex-end; align-self: flex-end;
margin: auto; margin: auto;
margin-bottom: 0px; margin-bottom: 0px;
position: relative; position: relative;
width: 100%; width: 100%;
} }
.graph > div { .graph > div {
align-self: flex-end; align-self: flex-end;
margin: auto 8px; margin: auto 8px;
} }
</style>`; </style>`;
} }
} }
customElements.define("hui-sensor-card", HuiSensorCard); customElements.define("hui-sensor-card", HuiSensorCard);

View File

@@ -1,66 +1,66 @@
import { html, LitElement } from "@polymer/lit-element"; import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
import createCardElement from "../common/create-card-element"; import createCardElement from "../common/create-card-element";
import { LovelaceCard, LovelaceConfig } from "../types"; import { LovelaceCard, LovelaceConfig } from "../types";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
interface Config extends LovelaceConfig { interface Config extends LovelaceConfig {
cards: LovelaceConfig[]; cards: LovelaceConfig[];
} }
export abstract class HuiStackCard extends LitElement implements LovelaceCard { export abstract class HuiStackCard extends LitElement implements LovelaceCard {
protected _cards?: LovelaceCard[]; protected _cards?: LovelaceCard[];
private _config?: Config; private _config?: Config;
private _hass?: HomeAssistant; private _hass?: HomeAssistant;
static get properties() { static get properties() {
return { return {
_config: {}, _config: {},
}; };
} }
set hass(hass: HomeAssistant) { set hass(hass: HomeAssistant) {
this._hass = hass; this._hass = hass;
if (!this._cards) { if (!this._cards) {
return; return;
} }
for (const element of this._cards) { for (const element of this._cards) {
element.hass = this._hass; element.hass = this._hass;
} }
} }
public abstract getCardSize(): number; public abstract getCardSize(): number;
public setConfig(config: Config): void { public setConfig(config: Config): void {
if (!config || !config.cards || !Array.isArray(config.cards)) { if (!config || !config.cards || !Array.isArray(config.cards)) {
throw new Error("Card config incorrect"); throw new Error("Card config incorrect");
} }
this._config = config; this._config = config;
this._cards = config.cards.map((card) => { this._cards = config.cards.map((card) => {
const element = createCardElement(card) as LovelaceCard; const element = createCardElement(card) as LovelaceCard;
if (this._hass) { if (this._hass) {
element.hass = this._hass; element.hass = this._hass;
} }
return element; return element;
}); });
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config) { if (!this._config) {
return html``; return html``;
} }
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<div id="root"> <div id="root">
${this._cards} ${this._cards}
</div> </div>
`; `;
} }
protected abstract renderStyle(): TemplateResult; protected abstract renderStyle(): TemplateResult;
} }

View File

@@ -1,390 +1,390 @@
import { import {
html, html,
LitElement, LitElement,
PropertyDeclarations, PropertyDeclarations,
PropertyValues, PropertyValues,
} from "@polymer/lit-element"; } from "@polymer/lit-element";
import { classMap } from "lit-html/directives/classMap"; import { classMap } from "lit-html/directives/classMap";
import { jQuery } from "../../../resources/jquery"; import { jQuery } from "../../../resources/jquery";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import { roundSliderStyle } from "../../../resources/jquery.roundslider"; import { roundSliderStyle } from "../../../resources/jquery.roundslider";
import { HomeAssistant, ClimateEntity } from "../../../types"; import { HomeAssistant, ClimateEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard, LovelaceConfig } from "../types"; import { LovelaceCard, LovelaceConfig } from "../types";
import computeStateName from "../../../common/entity/compute_state_name"; import computeStateName from "../../../common/entity/compute_state_name";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
const thermostatConfig = { const thermostatConfig = {
radius: 150, radius: 150,
step: 1, step: 1,
circleShape: "pie", circleShape: "pie",
startAngle: 315, startAngle: 315,
width: 5, width: 5,
lineCap: "round", lineCap: "round",
handleSize: "+10", handleSize: "+10",
showTooltip: false, showTooltip: false,
}; };
const modeIcons = { const modeIcons = {
auto: "hass:autorenew", auto: "hass:autorenew",
heat: "hass:fire", heat: "hass:fire",
cool: "hass:snowflake", cool: "hass:snowflake",
off: "hass:power", off: "hass:power",
}; };
interface Config extends LovelaceConfig { interface Config extends LovelaceConfig {
entity: string; entity: string;
} }
function formatTemp(temps: string[]): string { function formatTemp(temps: string[]): string {
return temps.filter(Boolean).join("-"); return temps.filter(Boolean).join("-");
} }
export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement) export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard { implements LovelaceCard {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: Config; private _config?: Config;
static get properties(): PropertyDeclarations { static get properties(): PropertyDeclarations {
return { return {
hass: {}, hass: {},
_config: {}, _config: {},
}; };
} }
public getCardSize(): number { public getCardSize(): number {
return 4; return 4;
} }
public setConfig(config: Config): void { public setConfig(config: Config): void {
if (!config.entity || config.entity.split(".")[0] !== "climate") { if (!config.entity || config.entity.split(".")[0] !== "climate") {
throw new Error("Specify an entity from within the climate domain."); throw new Error("Specify an entity from within the climate domain.");
} }
this._config = config; this._config = config;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.hass || !this._config) { if (!this.hass || !this._config) {
return html``; return html``;
} }
const stateObj = this.hass.states[this._config.entity] as ClimateEntity; const stateObj = this.hass.states[this._config.entity] as ClimateEntity;
const broadCard = this.clientWidth > 390; const broadCard = this.clientWidth > 390;
const mode = modeIcons[stateObj.attributes.operation_mode || ""] const mode = modeIcons[stateObj.attributes.operation_mode || ""]
? stateObj.attributes.operation_mode! ? stateObj.attributes.operation_mode!
: "unknown-mode"; : "unknown-mode";
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<ha-card <ha-card
class="${classMap({ class="${classMap({
[mode]: true, [mode]: true,
large: broadCard, large: broadCard,
small: !broadCard, small: !broadCard,
})}"> })}">
<div id="root"> <div id="root">
<div id="thermostat"></div> <div id="thermostat"></div>
<div id="tooltip"> <div id="tooltip">
<div class="title">${computeStateName(stateObj)}</div> <div class="title">${computeStateName(stateObj)}</div>
<div class="current-temperature"> <div class="current-temperature">
<span class="current-temperature-text">${ <span class="current-temperature-text">${
stateObj.attributes.current_temperature stateObj.attributes.current_temperature
} }
<span class="uom">${ <span class="uom">${
this.hass.config.unit_system.temperature this.hass.config.unit_system.temperature
}</span> }</span>
</span> </span>
</div> </div>
<div class="climate-info"> <div class="climate-info">
<div id="set-temperature"></div> <div id="set-temperature"></div>
<div class="current-mode">${this.localize( <div class="current-mode">${this.localize(
`state.climate.${stateObj.state}` `state.climate.${stateObj.state}`
)}</div> )}</div>
<div class="modes"> <div class="modes">
${(stateObj.attributes.operation_list || []).map((modeItem) => ${(stateObj.attributes.operation_list || []).map((modeItem) =>
this._renderIcon(modeItem, mode) this._renderIcon(modeItem, mode)
)} )}
</div> </div>
</div> </div>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
protected shouldUpdate(changedProps: PropertyValues): boolean { protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.get("hass")) { if (changedProps.get("hass")) {
return ( return (
(changedProps.get("hass") as any).states[this._config!.entity] !== (changedProps.get("hass") as any).states[this._config!.entity] !==
this.hass!.states[this._config!.entity] this.hass!.states[this._config!.entity]
); );
} }
if (changedProps.has("_config")) { if (changedProps.has("_config")) {
return true; return true;
} }
return true; return true;
} }
protected firstUpdated(): void { protected firstUpdated(): void {
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity; const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
const _sliderType = const _sliderType =
stateObj.attributes.target_temp_low && stateObj.attributes.target_temp_low &&
stateObj.attributes.target_temp_high stateObj.attributes.target_temp_high
? "range" ? "range"
: "min-range"; : "min-range";
jQuery("#thermostat", this.shadowRoot).roundSlider({ jQuery("#thermostat", this.shadowRoot).roundSlider({
...thermostatConfig, ...thermostatConfig,
radius: this.clientWidth / 3, radius: this.clientWidth / 3,
min: stateObj.attributes.min_temp, min: stateObj.attributes.min_temp,
max: stateObj.attributes.max_temp, max: stateObj.attributes.max_temp,
sliderType: _sliderType, sliderType: _sliderType,
change: (value) => this._setTemperature(value), change: (value) => this._setTemperature(value),
drag: (value) => this._dragEvent(value), drag: (value) => this._dragEvent(value),
}); });
} }
protected updated(): void { protected updated(): void {
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity; const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
let sliderValue; let sliderValue;
let uiValue; let uiValue;
if ( if (
stateObj.attributes.target_temp_low && stateObj.attributes.target_temp_low &&
stateObj.attributes.target_temp_high stateObj.attributes.target_temp_high
) { ) {
sliderValue = `${stateObj.attributes.target_temp_low}, ${ sliderValue = `${stateObj.attributes.target_temp_low}, ${
stateObj.attributes.target_temp_high stateObj.attributes.target_temp_high
}`; }`;
uiValue = formatTemp([ uiValue = formatTemp([
String(stateObj.attributes.target_temp_low), String(stateObj.attributes.target_temp_low),
String(stateObj.attributes.target_temp_high), String(stateObj.attributes.target_temp_high),
]); ]);
} else { } else {
sliderValue = uiValue = stateObj.attributes.temperature; sliderValue = uiValue = stateObj.attributes.temperature;
} }
jQuery("#thermostat", this.shadowRoot).roundSlider({ jQuery("#thermostat", this.shadowRoot).roundSlider({
value: sliderValue, value: sliderValue,
}); });
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = uiValue; this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = uiValue;
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
${roundSliderStyle} ${roundSliderStyle}
<style> <style>
:host { :host {
display: block; display: block;
} }
ha-card { ha-card {
overflow: hidden; overflow: hidden;
--rail-border-color: transparent; --rail-border-color: transparent;
--auto-color: green; --auto-color: green;
--cool-color: #2b9af9; --cool-color: #2b9af9;
--heat-color: #FF8100; --heat-color: #FF8100;
--off-color: #8a8a8a; --off-color: #8a8a8a;
--unknown-color: #bac; --unknown-color: #bac;
} }
#root { #root {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.auto { .auto {
--mode-color: var(--auto-color); --mode-color: var(--auto-color);
} }
.cool { .cool {
--mode-color: var(--cool-color); --mode-color: var(--cool-color);
} }
.heat { .heat {
--mode-color: var(--heat-color); --mode-color: var(--heat-color);
} }
.off { .off {
--mode-color: var(--off-color); --mode-color: var(--off-color);
} }
.unknown-mode { .unknown-mode {
--mode-color: var(--unknown-color); --mode-color: var(--unknown-color);
} }
.no-title { .no-title {
--title-margin-top: 33% !important; --title-margin-top: 33% !important;
} }
.large { .large {
--thermostat-padding-top: 25px; --thermostat-padding-top: 25px;
--thermostat-margin-bottom: 25px; --thermostat-margin-bottom: 25px;
--title-font-size: 28px; --title-font-size: 28px;
--title-margin-top: 20%; --title-margin-top: 20%;
--climate-info-margin-top: 17%; --climate-info-margin-top: 17%;
--modes-margin-top: 2%; --modes-margin-top: 2%;
--set-temperature-font-size: 25px; --set-temperature-font-size: 25px;
--current-temperature-font-size: 71px; --current-temperature-font-size: 71px;
--current-temperature-margin-top: 10%; --current-temperature-margin-top: 10%;
--current-temperature-text-padding-left: 15px; --current-temperature-text-padding-left: 15px;
--uom-font-size: 20px; --uom-font-size: 20px;
--uom-margin-left: -18px; --uom-margin-left: -18px;
--current-mode-font-size: 18px; --current-mode-font-size: 18px;
--set-temperature-padding-bottom: 5px; --set-temperature-padding-bottom: 5px;
} }
.small { .small {
--thermostat-padding-top: 15px; --thermostat-padding-top: 15px;
--thermostat-margin-bottom: 15px; --thermostat-margin-bottom: 15px;
--title-font-size: 18px; --title-font-size: 18px;
--title-margin-top: 20%; --title-margin-top: 20%;
--climate-info-margin-top: 7.5%; --climate-info-margin-top: 7.5%;
--modes-margin-top: 1%; --modes-margin-top: 1%;
--set-temperature-font-size: 16px; --set-temperature-font-size: 16px;
--current-temperature-font-size: 25px; --current-temperature-font-size: 25px;
--current-temperature-margin-top: 5%; --current-temperature-margin-top: 5%;
--current-temperature-text-padding-left: 7px; --current-temperature-text-padding-left: 7px;
--uom-font-size: 12px; --uom-font-size: 12px;
--uom-margin-left: -5px; --uom-margin-left: -5px;
--current-mode-font-size: 14px; --current-mode-font-size: 14px;
--set-temperature-padding-bottom: 0px; --set-temperature-padding-bottom: 0px;
} }
#thermostat { #thermostat {
margin: 0 auto var(--thermostat-margin-bottom); margin: 0 auto var(--thermostat-margin-bottom);
padding-top: var(--thermostat-padding-top); padding-top: var(--thermostat-padding-top);
} }
#thermostat .rs-range-color { #thermostat .rs-range-color {
background-color: var(--mode-color, var(--disabled-text-color)); background-color: var(--mode-color, var(--disabled-text-color));
} }
#thermostat .rs-path-color { #thermostat .rs-path-color {
background-color: var(--disabled-text-color); background-color: var(--disabled-text-color);
} }
#thermostat .rs-handle { #thermostat .rs-handle {
background-color: var(--paper-card-background-color, white); background-color: var(--paper-card-background-color, white);
padding: 7px; padding: 7px;
border: 2px solid var(--disabled-text-color); border: 2px solid var(--disabled-text-color);
} }
#thermostat .rs-handle.rs-focus { #thermostat .rs-handle.rs-focus {
border-color: var(--mode-color, var(--disabled-text-color)); border-color: var(--mode-color, var(--disabled-text-color));
} }
#thermostat .rs-handle:after { #thermostat .rs-handle:after {
border-color: var(--mode-color, var(--disabled-text-color)); border-color: var(--mode-color, var(--disabled-text-color));
background-color: var(--mode-color, var(--disabled-text-color)); background-color: var(--mode-color, var(--disabled-text-color));
} }
#thermostat .rs-border { #thermostat .rs-border {
border-color: var(--rail-border-color); border-color: var(--rail-border-color);
} }
#thermostat .rs-bar.rs-transition.rs-first, .rs-bar.rs-transition.rs-second{ #thermostat .rs-bar.rs-transition.rs-first, .rs-bar.rs-transition.rs-second{
z-index: 20 !important; z-index: 20 !important;
} }
#thermostat .rs-inner.rs-bg-color.rs-border, #thermostat .rs-inner.rs-bg-color.rs-border,
#thermostat .rs-overlay.rs-transition.rs-bg-color { #thermostat .rs-overlay.rs-transition.rs-bg-color {
background-color: var(--paper-card-background-color, white); background-color: var(--paper-card-background-color, white);
} }
#tooltip { #tooltip {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 100%; height: 100%;
text-align: center; text-align: center;
z-index: 15; z-index: 15;
color: var(--primary-text-color); color: var(--primary-text-color);
} }
#set-temperature { #set-temperature {
font-size: var(--set-temperature-font-size); font-size: var(--set-temperature-font-size);
padding-bottom: var(--set-temperature-padding-bottom); padding-bottom: var(--set-temperature-padding-bottom);
} }
.title { .title {
font-size: var(--title-font-size); font-size: var(--title-font-size);
margin-top: var(--title-margin-top); margin-top: var(--title-margin-top);
} }
.climate-info { .climate-info {
margin-top: var(--climate-info-margin-top); margin-top: var(--climate-info-margin-top);
} }
.current-mode { .current-mode {
font-size: var(--current-mode-font-size); font-size: var(--current-mode-font-size);
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.modes { .modes {
margin-top: var(--modes-margin-top); margin-top: var(--modes-margin-top);
} }
.modes ha-icon { .modes ha-icon {
color: var(--disabled-text-color); color: var(--disabled-text-color);
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
margin: 0 10px; margin: 0 10px;
} }
.modes ha-icon.selected-icon { .modes ha-icon.selected-icon {
color: var(--mode-color); color: var(--mode-color);
} }
.current-temperature { .current-temperature {
margin-top: var(--current-temperature-margin-top); margin-top: var(--current-temperature-margin-top);
font-size: var(--current-temperature-font-size); font-size: var(--current-temperature-font-size);
} }
.current-temperature-text { .current-temperature-text {
padding-left: var(--current-temperature-text-padding-left); padding-left: var(--current-temperature-text-padding-left);
} }
.uom { .uom {
font-size: var(--uom-font-size); font-size: var(--uom-font-size);
vertical-align: top; vertical-align: top;
margin-left: var(--uom-margin-left); margin-left: var(--uom-margin-left);
} }
</style> </style>
`; `;
} }
private _dragEvent(e): void { private _dragEvent(e): void {
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = formatTemp( this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = formatTemp(
String(e.value).split(",") String(e.value).split(",")
); );
} }
private _setTemperature(e): void { private _setTemperature(e): void {
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity; const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
if ( if (
stateObj.attributes.target_temp_low && stateObj.attributes.target_temp_low &&
stateObj.attributes.target_temp_high stateObj.attributes.target_temp_high
) { ) {
if (e.handle.index === 1) { if (e.handle.index === 1) {
this.hass!.callService("climate", "set_temperature", { this.hass!.callService("climate", "set_temperature", {
entity_id: this._config!.entity, entity_id: this._config!.entity,
target_temp_low: e.handle.value, target_temp_low: e.handle.value,
target_temp_high: stateObj.attributes.target_temp_high, target_temp_high: stateObj.attributes.target_temp_high,
}); });
} else { } else {
this.hass!.callService("climate", "set_temperature", { this.hass!.callService("climate", "set_temperature", {
entity_id: this._config!.entity, entity_id: this._config!.entity,
target_temp_low: stateObj.attributes.target_temp_low, target_temp_low: stateObj.attributes.target_temp_low,
target_temp_high: e.handle.value, target_temp_high: e.handle.value,
}); });
} }
} else { } else {
this.hass!.callService("climate", "set_temperature", { this.hass!.callService("climate", "set_temperature", {
entity_id: this._config!.entity, entity_id: this._config!.entity,
temperature: e.value, temperature: e.value,
}); });
} }
} }
private _renderIcon(mode: string, currentMode: string): TemplateResult { private _renderIcon(mode: string, currentMode: string): TemplateResult {
if (!modeIcons[mode]) { if (!modeIcons[mode]) {
return html``; return html``;
} }
return html`<ha-icon return html`<ha-icon
class="${classMap({ "selected-icon": currentMode === mode })}" class="${classMap({ "selected-icon": currentMode === mode })}"
.mode="${mode}" .mode="${mode}"
.icon="${modeIcons[mode]}" .icon="${modeIcons[mode]}"
@click="${this._handleModeClick}" @click="${this._handleModeClick}"
></ha-icon>`; ></ha-icon>`;
} }
private _handleModeClick(e: MouseEvent): void { private _handleModeClick(e: MouseEvent): void {
this.hass!.callService("climate", "set_operation_mode", { this.hass!.callService("climate", "set_operation_mode", {
entity_id: this._config!.entity, entity_id: this._config!.entity,
operation_mode: (e.currentTarget as any).mode, operation_mode: (e.currentTarget as any).mode,
}); });
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-thermostat-card": HuiThermostatCard; "hui-thermostat-card": HuiThermostatCard;
} }
} }
customElements.define("hui-thermostat-card", HuiThermostatCard); customElements.define("hui-thermostat-card", HuiThermostatCard);

View File

@@ -1,50 +1,50 @@
import { html } from "@polymer/lit-element"; import { html } from "@polymer/lit-element";
import computeCardSize from "../common/compute-card-size"; import computeCardSize from "../common/compute-card-size";
import { HuiStackCard } from "./hui-stack-card"; import { HuiStackCard } from "./hui-stack-card";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
class HuiVerticalStackCard extends HuiStackCard { class HuiVerticalStackCard extends HuiStackCard {
public getCardSize() { public getCardSize() {
let totalSize = 0; let totalSize = 0;
if (!this._cards) { if (!this._cards) {
return totalSize; return totalSize;
} }
for (const element of this._cards) { for (const element of this._cards) {
totalSize += computeCardSize(element); totalSize += computeCardSize(element);
} }
return totalSize; return totalSize;
} }
protected renderStyle(): TemplateResult { protected renderStyle(): TemplateResult {
return html` return html`
<style> <style>
#root { #root {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#root > * { #root > * {
margin: 4px 0 4px 0; margin: 4px 0 4px 0;
} }
#root > *:first-child { #root > *:first-child {
margin-top: 0; margin-top: 0;
} }
#root > *:last-child { #root > *:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
</style> </style>
`; `;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-vertical-stack-card": HuiVerticalStackCard; "hui-vertical-stack-card": HuiVerticalStackCard;
} }
} }
customElements.define("hui-vertical-stack-card", HuiVerticalStackCard); customElements.define("hui-vertical-stack-card", HuiVerticalStackCard);

View File

@@ -1,15 +1,15 @@
import "../../../cards/ha-camera-card"; import "../../../cards/ha-camera-card";
import LegacyWrapperCard from "./hui-legacy-wrapper-card"; import LegacyWrapperCard from "./hui-legacy-wrapper-card";
class HuiWeatherForecastCard extends LegacyWrapperCard { class HuiWeatherForecastCard extends LegacyWrapperCard {
constructor() { constructor() {
super("ha-weather-card", "weather"); super("ha-weather-card", "weather");
} }
getCardSize() { getCardSize() {
return 4; return 4;
} }
} }
customElements.define("hui-weather-forecast-card", HuiWeatherForecastCard); customElements.define("hui-weather-forecast-card", HuiWeatherForecastCard);

View File

@@ -1,7 +1,7 @@
import computeDomain from "../../../common/entity/compute_domain"; import computeDomain from "../../../common/entity/compute_domain";
export default function computeNotifications(states) { export default function computeNotifications(states) {
return Object.keys(states) return Object.keys(states)
.filter((entityId) => computeDomain(entityId) === "configurator") .filter((entityId) => computeDomain(entityId) === "configurator")
.map((entityId) => states[entityId]); .map((entityId) => states[entityId]);
} }

View File

@@ -1,38 +1,38 @@
import computeStateName from "../../../common/entity/compute_state_name"; import computeStateName from "../../../common/entity/compute_state_name";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { LovelaceElementConfig } from "../elements/types"; import { LovelaceElementConfig } from "../elements/types";
export const computeTooltip = ( export const computeTooltip = (
hass: HomeAssistant, hass: HomeAssistant,
config: LovelaceElementConfig config: LovelaceElementConfig
): string => { ): string => {
if (config.title) { if (config.title) {
return config.title; return config.title;
} }
let stateName = ""; let stateName = "";
let tooltip: string; let tooltip: string;
if (config.entity) { if (config.entity) {
stateName = stateName =
config.entity in hass.states config.entity in hass.states
? computeStateName(hass.states[config.entity]) ? computeStateName(hass.states[config.entity])
: config.entity; : config.entity;
} }
switch (config.tap_action) { switch (config.tap_action) {
case "navigate": case "navigate":
tooltip = `Navigate to ${config.navigation_path}`; tooltip = `Navigate to ${config.navigation_path}`;
break; break;
case "toggle": case "toggle":
tooltip = `Toggle ${stateName}`; tooltip = `Toggle ${stateName}`;
break; break;
case "call-service": case "call-service":
tooltip = `Call service ${config.service}`; tooltip = `Call service ${config.service}`;
break; break;
default: default:
tooltip = `Show more-info: ${stateName}`; tooltip = `Show more-info: ${stateName}`;
} }
return tooltip; return tooltip;
}; };

View File

@@ -1,112 +1,112 @@
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import "../cards/hui-alarm-panel-card"; import "../cards/hui-alarm-panel-card";
import "../cards/hui-conditional-card.ts"; import "../cards/hui-conditional-card.ts";
import "../cards/hui-entities-card.ts"; import "../cards/hui-entities-card.ts";
import "../cards/hui-entity-button-card.ts"; import "../cards/hui-entity-button-card.ts";
import "../cards/hui-entity-filter-card"; import "../cards/hui-entity-filter-card";
import "../cards/hui-error-card.ts"; import "../cards/hui-error-card.ts";
import "../cards/hui-glance-card.ts"; import "../cards/hui-glance-card.ts";
import "../cards/hui-history-graph-card"; import "../cards/hui-history-graph-card";
import "../cards/hui-horizontal-stack-card.ts"; import "../cards/hui-horizontal-stack-card.ts";
import "../cards/hui-iframe-card.ts"; import "../cards/hui-iframe-card.ts";
import "../cards/hui-light-card"; import "../cards/hui-light-card";
import "../cards/hui-map-card"; import "../cards/hui-map-card";
import "../cards/hui-markdown-card.ts"; import "../cards/hui-markdown-card.ts";
import "../cards/hui-media-control-card"; import "../cards/hui-media-control-card";
import "../cards/hui-picture-card"; import "../cards/hui-picture-card";
import "../cards/hui-picture-elements-card"; import "../cards/hui-picture-elements-card";
import "../cards/hui-picture-entity-card"; import "../cards/hui-picture-entity-card";
import "../cards/hui-picture-glance-card"; import "../cards/hui-picture-glance-card";
import "../cards/hui-plant-status-card"; import "../cards/hui-plant-status-card";
import "../cards/hui-sensor-card"; import "../cards/hui-sensor-card";
import "../cards/hui-vertical-stack-card.ts"; import "../cards/hui-vertical-stack-card.ts";
import "../cards/hui-thermostat-card.ts"; import "../cards/hui-thermostat-card.ts";
import "../cards/hui-weather-forecast-card"; import "../cards/hui-weather-forecast-card";
import "../cards/hui-gauge-card"; import "../cards/hui-gauge-card";
import createErrorCardConfig from "./create-error-card-config"; import createErrorCardConfig from "./create-error-card-config";
const CARD_TYPES = new Set([ const CARD_TYPES = new Set([
"alarm-panel", "alarm-panel",
"conditional", "conditional",
"entities", "entities",
"entity-button", "entity-button",
"entity-filter", "entity-filter",
"error", "error",
"gauge", "gauge",
"glance", "glance",
"history-graph", "history-graph",
"horizontal-stack", "horizontal-stack",
"iframe", "iframe",
"light", "light",
"map", "map",
"markdown", "markdown",
"media-control", "media-control",
"picture", "picture",
"picture-elements", "picture-elements",
"picture-entity", "picture-entity",
"picture-glance", "picture-glance",
"plant-status", "plant-status",
"sensor", "sensor",
"thermostat", "thermostat",
"vertical-stack", "vertical-stack",
"weather-forecast", "weather-forecast",
]); ]);
const CUSTOM_TYPE_PREFIX = "custom:"; const CUSTOM_TYPE_PREFIX = "custom:";
const TIMEOUT = 2000; const TIMEOUT = 2000;
function _createElement(tag, config) { function _createElement(tag, config) {
const element = document.createElement(tag); const element = document.createElement(tag);
try { try {
element.setConfig(config); element.setConfig(config);
} catch (err) { } catch (err) {
// eslint-disable-next-line // eslint-disable-next-line
console.error(tag, err); console.error(tag, err);
// eslint-disable-next-line // eslint-disable-next-line
return _createErrorElement(err.message, config); return _createErrorElement(err.message, config);
} }
return element; return element;
} }
function _createErrorElement(error, config) { function _createErrorElement(error, config) {
return _createElement("hui-error-card", createErrorCardConfig(error, config)); return _createElement("hui-error-card", createErrorCardConfig(error, config));
} }
export default function createCardElement(config) { export default function createCardElement(config) {
if (!config || typeof config !== "object" || !config.type) { if (!config || typeof config !== "object" || !config.type) {
return _createErrorElement("No card type configured.", config); return _createErrorElement("No card type configured.", config);
} }
if (config.type.startsWith(CUSTOM_TYPE_PREFIX)) { if (config.type.startsWith(CUSTOM_TYPE_PREFIX)) {
const tag = config.type.substr(CUSTOM_TYPE_PREFIX.length); const tag = config.type.substr(CUSTOM_TYPE_PREFIX.length);
if (customElements.get(tag)) { if (customElements.get(tag)) {
return _createElement(tag, config); return _createElement(tag, config);
} }
const element = _createErrorElement( const element = _createErrorElement(
`Custom element doesn't exist: ${tag}.`, `Custom element doesn't exist: ${tag}.`,
config config
); );
element.style.display = "None"; element.style.display = "None";
const timer = window.setTimeout(() => { const timer = window.setTimeout(() => {
element.style.display = ""; element.style.display = "";
}, TIMEOUT); }, TIMEOUT);
customElements.whenDefined(tag).then(() => { customElements.whenDefined(tag).then(() => {
clearTimeout(timer); clearTimeout(timer);
fireEvent(element, "rebuild-view"); fireEvent(element, "rebuild-view");
}); });
return element; return element;
} }
if (!CARD_TYPES.has(config.type)) { if (!CARD_TYPES.has(config.type)) {
return _createErrorElement( return _createErrorElement(
`Unknown card type encountered: ${config.type}.`, `Unknown card type encountered: ${config.type}.`,
config config
); );
} }
return _createElement(`hui-${config.type}-card`, config); return _createElement(`hui-${config.type}-card`, config);
} }

View File

@@ -1,79 +1,79 @@
import "../elements/hui-icon-element"; import "../elements/hui-icon-element";
import "../elements/hui-image-element"; import "../elements/hui-image-element";
import "../elements/hui-service-button-element"; import "../elements/hui-service-button-element";
import "../elements/hui-state-badge-element"; import "../elements/hui-state-badge-element";
import "../elements/hui-state-icon-element"; import "../elements/hui-state-icon-element";
import "../elements/hui-state-label-element"; import "../elements/hui-state-label-element";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import createErrorCardConfig from "./create-error-card-config"; import createErrorCardConfig from "./create-error-card-config";
const CUSTOM_TYPE_PREFIX = "custom:"; const CUSTOM_TYPE_PREFIX = "custom:";
const ELEMENT_TYPES = new Set([ const ELEMENT_TYPES = new Set([
"icon", "icon",
"image", "image",
"service-button", "service-button",
"state-badge", "state-badge",
"state-icon", "state-icon",
"state-label", "state-label",
]); ]);
const TIMEOUT = 2000; const TIMEOUT = 2000;
function _createElement(tag, config) { function _createElement(tag, config) {
const element = document.createElement(tag); const element = document.createElement(tag);
try { try {
element.setConfig(config); element.setConfig(config);
} catch (err) { } catch (err) {
// eslint-disable-next-line // eslint-disable-next-line
console.error(tag, err); console.error(tag, err);
// eslint-disable-next-line // eslint-disable-next-line
return _createErrorElement(err.message, config); return _createErrorElement(err.message, config);
} }
return element; return element;
} }
function _createErrorElement(error, config) { function _createErrorElement(error, config) {
return _createElement("hui-error-card", createErrorCardConfig(error, config)); return _createElement("hui-error-card", createErrorCardConfig(error, config));
} }
function _hideErrorElement(element) { function _hideErrorElement(element) {
element.style.display = "None"; element.style.display = "None";
return window.setTimeout(() => { return window.setTimeout(() => {
element.style.display = ""; element.style.display = "";
}, TIMEOUT); }, TIMEOUT);
} }
export default function createHuiElement(config) { export default function createHuiElement(config) {
if (!config || typeof config !== "object" || !config.type) { if (!config || typeof config !== "object" || !config.type) {
return _createErrorElement("No element type configured.", config); return _createErrorElement("No element type configured.", config);
} }
if (config.type.startsWith(CUSTOM_TYPE_PREFIX)) { if (config.type.startsWith(CUSTOM_TYPE_PREFIX)) {
const tag = config.type.substr(CUSTOM_TYPE_PREFIX.length); const tag = config.type.substr(CUSTOM_TYPE_PREFIX.length);
if (customElements.get(tag)) { if (customElements.get(tag)) {
return _createElement(tag, config); return _createElement(tag, config);
} }
const element = _createErrorElement( const element = _createErrorElement(
`Custom element doesn't exist: ${tag}.`, `Custom element doesn't exist: ${tag}.`,
config config
); );
const timer = _hideErrorElement(element); const timer = _hideErrorElement(element);
customElements.whenDefined(tag).then(() => { customElements.whenDefined(tag).then(() => {
clearTimeout(timer); clearTimeout(timer);
fireEvent(element, "rebuild-view"); fireEvent(element, "rebuild-view");
}); });
return element; return element;
} }
if (!ELEMENT_TYPES.has(config.type)) { if (!ELEMENT_TYPES.has(config.type)) {
return _createErrorElement( return _createErrorElement(
`Unknown element type encountered: ${config.type}.`, `Unknown element type encountered: ${config.type}.`,
config config
); );
} }
return _createElement(`hui-${config.type}-element`, config); return _createElement(`hui-${config.type}-element`, config);
} }

View File

@@ -1,117 +1,117 @@
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import "../entity-rows/hui-climate-entity-row"; import "../entity-rows/hui-climate-entity-row";
import "../entity-rows/hui-cover-entity-row"; import "../entity-rows/hui-cover-entity-row";
import "../entity-rows/hui-group-entity-row"; import "../entity-rows/hui-group-entity-row";
import "../entity-rows/hui-input-number-entity-row"; import "../entity-rows/hui-input-number-entity-row";
import "../entity-rows/hui-input-select-entity-row"; import "../entity-rows/hui-input-select-entity-row";
import "../entity-rows/hui-input-text-entity-row"; import "../entity-rows/hui-input-text-entity-row";
import "../entity-rows/hui-lock-entity-row"; import "../entity-rows/hui-lock-entity-row";
import "../entity-rows/hui-media-player-entity-row"; import "../entity-rows/hui-media-player-entity-row";
import "../entity-rows/hui-scene-entity-row"; import "../entity-rows/hui-scene-entity-row";
import "../entity-rows/hui-script-entity-row"; import "../entity-rows/hui-script-entity-row";
import "../entity-rows/hui-text-entity-row"; import "../entity-rows/hui-text-entity-row";
import "../entity-rows/hui-timer-entity-row"; import "../entity-rows/hui-timer-entity-row";
import "../entity-rows/hui-toggle-entity-row"; import "../entity-rows/hui-toggle-entity-row";
import "../special-rows/hui-call-service-row"; import "../special-rows/hui-call-service-row";
import "../special-rows/hui-divider-row"; import "../special-rows/hui-divider-row";
import "../special-rows/hui-section-row"; import "../special-rows/hui-section-row";
import "../special-rows/hui-weblink-row"; import "../special-rows/hui-weblink-row";
import createErrorCardConfig from "./create-error-card-config"; import createErrorCardConfig from "./create-error-card-config";
const CUSTOM_TYPE_PREFIX = "custom:"; const CUSTOM_TYPE_PREFIX = "custom:";
const SPECIAL_TYPES = new Set([ const SPECIAL_TYPES = new Set([
"call-service", "call-service",
"divider", "divider",
"section", "section",
"weblink", "weblink",
]); ]);
const DOMAIN_TO_ELEMENT_TYPE = { const DOMAIN_TO_ELEMENT_TYPE = {
automation: "toggle", automation: "toggle",
climate: "climate", climate: "climate",
cover: "cover", cover: "cover",
fan: "toggle", fan: "toggle",
group: "group", group: "group",
input_boolean: "toggle", input_boolean: "toggle",
input_number: "input-number", input_number: "input-number",
input_select: "input-select", input_select: "input-select",
input_text: "input-text", input_text: "input-text",
light: "toggle", light: "toggle",
media_player: "media-player", media_player: "media-player",
lock: "lock", lock: "lock",
scene: "scene", scene: "scene",
script: "script", script: "script",
timer: "timer", timer: "timer",
switch: "toggle", switch: "toggle",
vacuum: "toggle", vacuum: "toggle",
}; };
const TIMEOUT = 2000; const TIMEOUT = 2000;
function _createElement(tag, config) { function _createElement(tag, config) {
const element = document.createElement(tag); const element = document.createElement(tag);
try { try {
if ("setConfig" in element) element.setConfig(config); if ("setConfig" in element) element.setConfig(config);
} catch (err) { } catch (err) {
// eslint-disable-next-line // eslint-disable-next-line
console.error(tag, err); console.error(tag, err);
// eslint-disable-next-line // eslint-disable-next-line
return _createErrorElement(err.message, config); return _createErrorElement(err.message, config);
} }
return element; return element;
} }
function _createErrorElement(error, config) { function _createErrorElement(error, config) {
return _createElement("hui-error-card", createErrorCardConfig(error, config)); return _createElement("hui-error-card", createErrorCardConfig(error, config));
} }
function _hideErrorElement(element) { function _hideErrorElement(element) {
element.style.display = "None"; element.style.display = "None";
return window.setTimeout(() => { return window.setTimeout(() => {
element.style.display = ""; element.style.display = "";
}, TIMEOUT); }, TIMEOUT);
} }
export default function createRowElement(config) { export default function createRowElement(config) {
let tag; let tag;
if ( if (
!config || !config ||
typeof config !== "object" || typeof config !== "object" ||
(!config.entity && !config.type) (!config.entity && !config.type)
) { ) {
return _createErrorElement("Invalid config given.", config); return _createErrorElement("Invalid config given.", config);
} }
const type = config.type || "default"; const type = config.type || "default";
if (SPECIAL_TYPES.has(type)) { if (SPECIAL_TYPES.has(type)) {
return _createElement(`hui-${type}-row`, config); return _createElement(`hui-${type}-row`, config);
} }
if (type.startsWith(CUSTOM_TYPE_PREFIX)) { if (type.startsWith(CUSTOM_TYPE_PREFIX)) {
tag = type.substr(CUSTOM_TYPE_PREFIX.length); tag = type.substr(CUSTOM_TYPE_PREFIX.length);
if (customElements.get(tag)) { if (customElements.get(tag)) {
return _createElement(tag, config); return _createElement(tag, config);
} }
const element = _createErrorElement( const element = _createErrorElement(
`Custom element doesn't exist: ${tag}.`, `Custom element doesn't exist: ${tag}.`,
config config
); );
const timer = _hideErrorElement(element); const timer = _hideErrorElement(element);
customElements.whenDefined(tag).then(() => { customElements.whenDefined(tag).then(() => {
clearTimeout(timer); clearTimeout(timer);
fireEvent(element, "rebuild-view"); fireEvent(element, "rebuild-view");
}); });
return element; return element;
} }
const domain = config.entity.split(".", 1)[0]; const domain = config.entity.split(".", 1)[0];
tag = `hui-${DOMAIN_TO_ELEMENT_TYPE[domain] || "text"}-entity-row`; tag = `hui-${DOMAIN_TO_ELEMENT_TYPE[domain] || "text"}-entity-row`;
return _createElement(tag, config); return _createElement(tag, config);
} }

View File

@@ -1,7 +1,7 @@
import { STATES_OFF } from "../../../../common/const"; import { STATES_OFF } from "../../../../common/const";
import turnOnOffEntity from "./turn-on-off-entity"; import turnOnOffEntity from "./turn-on-off-entity";
export default function toggleEntity(hass, entityId) { export default function toggleEntity(hass, entityId) {
const turnOn = STATES_OFF.includes(hass.states[entityId].state); const turnOn = STATES_OFF.includes(hass.states[entityId].state);
turnOnOffEntity(hass, entityId, turnOn); turnOnOffEntity(hass, entityId, turnOn);
} }

View File

@@ -1,34 +1,34 @@
import { STATES_OFF } from "../../../../common/const"; import { STATES_OFF } from "../../../../common/const";
import computeDomain from "../../../../common/entity/compute_domain"; import computeDomain from "../../../../common/entity/compute_domain";
export default function turnOnOffEntities(hass, entityIds, turnOn = true) { export default function turnOnOffEntities(hass, entityIds, turnOn = true) {
const domainsToCall = {}; const domainsToCall = {};
entityIds.forEach((entityId) => { entityIds.forEach((entityId) => {
if (STATES_OFF.includes(hass.states[entityId].state) === turnOn) { if (STATES_OFF.includes(hass.states[entityId].state) === turnOn) {
const stateDomain = computeDomain(entityId); const stateDomain = computeDomain(entityId);
const serviceDomain = ["cover", "lock"].includes(stateDomain) const serviceDomain = ["cover", "lock"].includes(stateDomain)
? stateDomain ? stateDomain
: "homeassistant"; : "homeassistant";
if (!(serviceDomain in domainsToCall)) domainsToCall[serviceDomain] = []; if (!(serviceDomain in domainsToCall)) domainsToCall[serviceDomain] = [];
domainsToCall[serviceDomain].push(entityId); domainsToCall[serviceDomain].push(entityId);
} }
}); });
Object.keys(domainsToCall).forEach((domain) => { Object.keys(domainsToCall).forEach((domain) => {
let service; let service;
switch (domain) { switch (domain) {
case "lock": case "lock":
service = turnOn ? "unlock" : "lock"; service = turnOn ? "unlock" : "lock";
break; break;
case "cover": case "cover":
service = turnOn ? "open_cover" : "close_cover"; service = turnOn ? "open_cover" : "close_cover";
break; break;
default: default:
service = turnOn ? "turn_on" : "turn_off"; service = turnOn ? "turn_on" : "turn_off";
} }
const entities = domainsToCall[domain]; const entities = domainsToCall[domain];
hass.callService(domain, service, { entity_id: entities }); hass.callService(domain, service, { entity_id: entities });
}); });
} }

View File

@@ -1,20 +1,20 @@
import computeDomain from "../../../../common/entity/compute_domain"; import computeDomain from "../../../../common/entity/compute_domain";
export default function turnOnOffEntity(hass, entityId, turnOn = true) { export default function turnOnOffEntity(hass, entityId, turnOn = true) {
const stateDomain = computeDomain(entityId); const stateDomain = computeDomain(entityId);
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain; const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;
let service; let service;
switch (stateDomain) { switch (stateDomain) {
case "lock": case "lock":
service = turnOn ? "unlock" : "lock"; service = turnOn ? "unlock" : "lock";
break; break;
case "cover": case "cover":
service = turnOn ? "open_cover" : "close_cover"; service = turnOn ? "open_cover" : "close_cover";
break; break;
default: default:
service = turnOn ? "turn_on" : "turn_off"; service = turnOn ? "turn_on" : "turn_off";
} }
hass.callService(serviceDomain, service, { entity_id: entityId }); hass.callService(serviceDomain, service, { entity_id: entityId });
} }

View File

@@ -1,44 +1,44 @@
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { LovelaceElementConfig } from "../elements/types"; import { LovelaceElementConfig } from "../elements/types";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import toggleEntity from "../../../../src/panels/lovelace/common/entity/toggle-entity"; import toggleEntity from "../../../../src/panels/lovelace/common/entity/toggle-entity";
export const handleClick = ( export const handleClick = (
node: HTMLElement, node: HTMLElement,
hass: HomeAssistant, hass: HomeAssistant,
config: LovelaceElementConfig, config: LovelaceElementConfig,
hold: boolean hold: boolean
): void => { ): void => {
let action = config.tap_action || "more-info"; let action = config.tap_action || "more-info";
if (hold && config.hold_action) { if (hold && config.hold_action) {
action = config.hold_action; action = config.hold_action;
} }
if (action === "none") { if (action === "none") {
return; return;
} }
switch (action) { switch (action) {
case "more-info": case "more-info":
fireEvent(node, "hass-more-info", { entityId: config.entity }); fireEvent(node, "hass-more-info", { entityId: config.entity });
break; break;
case "navigate": case "navigate":
navigate(node, config.navigation_path ? config.navigation_path : ""); navigate(node, config.navigation_path ? config.navigation_path : "");
break; break;
case "toggle": case "toggle":
toggleEntity(hass, config.entity); toggleEntity(hass, config.entity);
break; break;
case "call-service": { case "call-service": {
if (config.service) { if (config.service) {
const [domain, service] = config.service.split(".", 2); const [domain, service] = config.service.split(".", 2);
const serviceData = { const serviceData = {
entity_id: config.entity, entity_id: config.entity,
...config.service_data, ...config.service_data,
}; };
hass.callService(domain, service, serviceData); hass.callService(domain, service, serviceData);
} }
} }
} }
}; };

View File

@@ -1,38 +1,38 @@
// Parse array of entity objects from config // Parse array of entity objects from config
import isValidEntityId from "../../../common/entity/valid_entity_id"; import isValidEntityId from "../../../common/entity/valid_entity_id";
export default function processConfigEntities(entities) { export default function processConfigEntities(entities) {
if (!entities || !Array.isArray(entities)) { if (!entities || !Array.isArray(entities)) {
throw new Error("Entities need to be an array"); throw new Error("Entities need to be an array");
} }
return entities.map((entityConf, index) => { return entities.map((entityConf, index) => {
if ( if (
typeof entityConf === "object" && typeof entityConf === "object" &&
!Array.isArray(entityConf) && !Array.isArray(entityConf) &&
entityConf.type entityConf.type
) { ) {
return entityConf; return entityConf;
} }
if (typeof entityConf === "string") { if (typeof entityConf === "string") {
entityConf = { entity: entityConf }; entityConf = { entity: entityConf };
} else if (typeof entityConf === "object" && !Array.isArray(entityConf)) { } else if (typeof entityConf === "object" && !Array.isArray(entityConf)) {
if (!entityConf.entity) { if (!entityConf.entity) {
throw new Error( throw new Error(
`Entity object at position ${index} is missing entity field.` `Entity object at position ${index} is missing entity field.`
); );
} }
} else { } else {
throw new Error(`Invalid entity specified at position ${index}.`); throw new Error(`Invalid entity specified at position ${index}.`);
} }
if (!isValidEntityId(entityConf.entity)) { if (!isValidEntityId(entityConf.entity)) {
throw new Error( throw new Error(
`Invalid entity ID at position ${index}: ${entityConf.entity}` `Invalid entity ID at position ${index}: ${entityConf.entity}`
); );
} }
return entityConf; return entityConf;
}); });
} }

View File

@@ -1,68 +1,68 @@
import "@polymer/paper-button/paper-button"; import "@polymer/paper-button/paper-button";
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
let registeredDialog = false; let registeredDialog = false;
export class HuiCardOptions extends LitElement { export class HuiCardOptions extends LitElement {
public cardId?: string; public cardId?: string;
protected hass?: HomeAssistant; protected hass?: HomeAssistant;
static get properties(): PropertyDeclarations { static get properties(): PropertyDeclarations {
return { return {
hass: {}, hass: {},
}; };
} }
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (!registeredDialog) { if (!registeredDialog) {
registeredDialog = true; registeredDialog = true;
fireEvent(this, "register-dialog", { fireEvent(this, "register-dialog", {
dialogShowEvent: "show-edit-card", dialogShowEvent: "show-edit-card",
dialogTag: "hui-dialog-edit-card", dialogTag: "hui-dialog-edit-card",
dialogImport: () => import("../editor/hui-dialog-edit-card"), dialogImport: () => import("../editor/hui-dialog-edit-card"),
}); });
} }
} }
protected render() { protected render() {
return html` return html`
<style> <style>
div { div {
border-top: 1px solid #e8e8e8; border-top: 1px solid #e8e8e8;
padding: 5px 16px; padding: 5px 16px;
background: var(--paper-card-background-color, white); background: var(--paper-card-background-color, white);
box-shadow: rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 1px 5px 0px, rgba(0, 0, 0, 0.2) 0px 3px 1px -2px; box-shadow: rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 1px 5px 0px, rgba(0, 0, 0, 0.2) 0px 3px 1px -2px;
text-align: right; text-align: right;
} }
paper-button { paper-button {
color: var(--primary-color); color: var(--primary-color);
font-weight: 500; font-weight: 500;
} }
</style> </style>
<slot></slot> <slot></slot>
<div> <div>
<paper-button <paper-button
@click="${this._editCard}" @click="${this._editCard}"
>EDIT</paper-button> >EDIT</paper-button>
</div> </div>
`; `;
} }
private _editCard() { private _editCard() {
fireEvent(this, "show-edit-card", { fireEvent(this, "show-edit-card", {
hass: this.hass, hass: this.hass,
cardId: this.cardId, cardId: this.cardId,
reloadLovelace: () => fireEvent(this, "config-refresh"), reloadLovelace: () => fireEvent(this, "config-refresh"),
}); });
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-card-options": HuiCardOptions; "hui-card-options": HuiCardOptions;
} }
} }
customElements.define("hui-card-options", HuiCardOptions); customElements.define("hui-card-options", HuiCardOptions);

View File

@@ -1,57 +1,57 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-toggle-button/paper-toggle-button"; import "@polymer/paper-toggle-button/paper-toggle-button";
import { DOMAINS_TOGGLE } from "../../../common/const"; import { DOMAINS_TOGGLE } from "../../../common/const";
import turnOnOffEntities from "../common/entity/turn-on-off-entities"; import turnOnOffEntities from "../common/entity/turn-on-off-entities";
class HuiEntitiesToggle extends PolymerElement { class HuiEntitiesToggle extends PolymerElement {
static get template() { static get template() {
return html` return html`
<style> <style>
:host { :host {
width: 38px; width: 38px;
display: block; display: block;
} }
paper-toggle-button { paper-toggle-button {
cursor: pointer; cursor: pointer;
--paper-toggle-button-label-spacing: 0; --paper-toggle-button-label-spacing: 0;
padding: 13px 5px; padding: 13px 5px;
margin: -4px -5px; margin: -4px -5px;
} }
</style> </style>
<template is="dom-if" if="[[_toggleEntities.length]]"> <template is="dom-if" if="[[_toggleEntities.length]]">
<paper-toggle-button checked="[[_computeIsChecked(hass, _toggleEntities)]]" on-change="_callService"></paper-toggle-button> <paper-toggle-button checked="[[_computeIsChecked(hass, _toggleEntities)]]" on-change="_callService"></paper-toggle-button>
</template> </template>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
entities: Array, entities: Array,
_toggleEntities: { _toggleEntities: {
type: Array, type: Array,
computed: "_computeToggleEntities(hass, entities)", computed: "_computeToggleEntities(hass, entities)",
}, },
}; };
} }
_computeToggleEntities(hass, entityIds) { _computeToggleEntities(hass, entityIds) {
return entityIds.filter( return entityIds.filter(
(entityId) => (entityId) =>
entityId in hass.states && DOMAINS_TOGGLE.has(entityId.split(".", 1)[0]) entityId in hass.states && DOMAINS_TOGGLE.has(entityId.split(".", 1)[0])
); );
} }
_computeIsChecked(hass, entityIds) { _computeIsChecked(hass, entityIds) {
return entityIds.some((entityId) => hass.states[entityId].state === "on"); return entityIds.some((entityId) => hass.states[entityId].state === "on");
} }
_callService(ev) { _callService(ev) {
const turnOn = ev.target.checked; const turnOn = ev.target.checked;
turnOnOffEntities(this.hass, this._toggleEntities, turnOn); turnOnOffEntities(this.hass, this._toggleEntities, turnOn);
} }
} }
customElements.define("hui-entities-toggle", HuiEntitiesToggle); customElements.define("hui-entities-toggle", HuiEntitiesToggle);

View File

@@ -1,137 +1,137 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/entity/state-badge"; import "../../../components/entity/state-badge";
import "../../../components/ha-relative-time"; import "../../../components/ha-relative-time";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import computeStateName from "../../../common/entity/compute_state_name"; import computeStateName from "../../../common/entity/compute_state_name";
class HuiGenericEntityRow extends PolymerElement { class HuiGenericEntityRow extends PolymerElement {
static get template() { static get template() {
return html` return html`
${this.styleTemplate} ${this.styleTemplate}
<template is="dom-if" if="[[_stateObj]]"> <template is="dom-if" if="[[_stateObj]]">
${this.stateBadgeTemplate} ${this.stateBadgeTemplate}
<div class="flex"> <div class="flex">
${this.infoTemplate} ${this.infoTemplate}
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
<template is="dom-if" if="[[!_stateObj]]"> <template is="dom-if" if="[[!_stateObj]]">
<div class="not-found"> <div class="not-found">
Entity not available: [[config.entity]] Entity not available: [[config.entity]]
</div> </div>
</template> </template>
`; `;
} }
static get styleTemplate() { static get styleTemplate() {
return html` return html`
<style> <style>
:host { :host {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.flex { .flex {
flex: 1; flex: 1;
margin-left: 16px; margin-left: 16px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
min-width: 0; min-width: 0;
} }
.info { .info {
flex: 1 0 60px; flex: 1 0 60px;
} }
.info, .info,
.info > * { .info > * {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.flex ::slotted(*) { .flex ::slotted(*) {
margin-left: 8px; margin-left: 8px;
min-width: 0; min-width: 0;
} }
.flex ::slotted([slot=secondary]) { .flex ::slotted([slot=secondary]) {
margin-left: 0; margin-left: 0;
} }
.secondary, .secondary,
ha-relative-time { ha-relative-time {
display: block; display: block;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.not-found { .not-found {
flex: 1; flex: 1;
background-color: yellow; background-color: yellow;
padding: 8px; padding: 8px;
} }
state-badge { state-badge {
flex: 0 0 40px; flex: 0 0 40px;
} }
</style> </style>
`; `;
} }
static get stateBadgeTemplate() { static get stateBadgeTemplate() {
return html` return html`
<state-badge <state-badge
state-obj="[[_stateObj]]" state-obj="[[_stateObj]]"
override-icon="[[config.icon]]" override-icon="[[config.icon]]"
></state-badge> ></state-badge>
`; `;
} }
static get infoTemplate() { static get infoTemplate() {
return html` return html`
<div class="info"> <div class="info">
[[_computeName(config.name, _stateObj)]] [[_computeName(config.name, _stateObj)]]
<div class="secondary"> <div class="secondary">
<template is="dom-if" if="[[showSecondary]]"> <template is="dom-if" if="[[showSecondary]]">
<template is="dom-if" if="[[_equals(config.secondary_info, 'entity-id')]]"> <template is="dom-if" if="[[_equals(config.secondary_info, 'entity-id')]]">
[[_stateObj.entity_id]] [[_stateObj.entity_id]]
</template> </template>
<template is="dom-if" if="[[_equals(config.secondary_info, 'last-changed')]]"> <template is="dom-if" if="[[_equals(config.secondary_info, 'last-changed')]]">
<ha-relative-time <ha-relative-time
hass="[[hass]]" hass="[[hass]]"
datetime="[[_stateObj.last_changed]]" datetime="[[_stateObj.last_changed]]"
></ha-relative-time> ></ha-relative-time>
</template> </template>
</template> </template>
<template is="dom-if" if="[[!showSecondary]"> <template is="dom-if" if="[[!showSecondary]">
<slot name="secondary"></slot> <slot name="secondary"></slot>
</template> </template>
</div> </div>
</div> </div>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
config: Object, config: Object,
_stateObj: { _stateObj: {
type: Object, type: Object,
computed: "_computeStateObj(hass.states, config.entity)", computed: "_computeStateObj(hass.states, config.entity)",
}, },
showSecondary: { showSecondary: {
type: Boolean, type: Boolean,
value: true, value: true,
}, },
}; };
} }
_equals(a, b) { _equals(a, b) {
return a === b; return a === b;
} }
_computeStateObj(states, entityId) { _computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null; return states && entityId in states ? states[entityId] : null;
} }
_computeName(name, stateObj) { _computeName(name, stateObj) {
return name || computeStateName(stateObj); return name || computeStateName(stateObj);
} }
} }
customElements.define("hui-generic-entity-row", HuiGenericEntityRow); customElements.define("hui-generic-entity-row", HuiGenericEntityRow);

View File

@@ -1,198 +1,198 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-toggle-button/paper-toggle-button"; import "@polymer/paper-toggle-button/paper-toggle-button";
import { STATES_OFF } from "../../../common/const"; import { STATES_OFF } from "../../../common/const";
import LocalizeMixin from "../../../mixins/localize-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio"; import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
const UPDATE_INTERVAL = 10000; const UPDATE_INTERVAL = 10000;
const DEFAULT_FILTER = "grayscale(100%)"; const DEFAULT_FILTER = "grayscale(100%)";
/* /*
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
*/ */
class HuiImage extends LocalizeMixin(PolymerElement) { class HuiImage extends LocalizeMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
${this.styleTemplate} ${this.styleTemplate}
<div id="wrapper"> <div id="wrapper">
<img <img
id="image" id="image"
src="[[_imageSrc]]" src="[[_imageSrc]]"
on-error="_onImageError" on-error="_onImageError"
on-load="_onImageLoad" /> on-load="_onImageLoad" />
<div id="brokenImage"></div> <div id="brokenImage"></div>
</div> </div>
`; `;
} }
static get styleTemplate() { static get styleTemplate() {
return html` return html`
<style> <style>
img { img {
display: block; display: block;
height: auto; height: auto;
transition: filter .2s linear; transition: filter .2s linear;
width: 100%; width: 100%;
} }
.error { .error {
text-align: center; text-align: center;
} }
.hidden { .hidden {
display: none; display: none;
} }
.ratio { .ratio {
position: relative; position: relative;
width: 100%; width: 100%;
height: 0 height: 0
} }
.ratio img, .ratio div { .ratio img, .ratio div {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
#brokenImage { #brokenImage {
background: grey url('/static/images/image-broken.svg') center/36px no-repeat; background: grey url('/static/images/image-broken.svg') center/36px no-repeat;
} }
</style> </style>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: { hass: {
type: Object, type: Object,
observer: "_hassChanged", observer: "_hassChanged",
}, },
entity: String, entity: String,
image: String, image: String,
stateImage: Object, stateImage: Object,
cameraImage: String, cameraImage: String,
aspectRatio: String, aspectRatio: String,
filter: String, filter: String,
stateFilter: Object, stateFilter: Object,
_imageSrc: String, _imageSrc: String,
}; };
} }
static get observers() { static get observers() {
return ["_configChanged(image, stateImage, cameraImage, aspectRatio)"]; return ["_configChanged(image, stateImage, cameraImage, aspectRatio)"];
} }
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (this.cameraImage) { if (this.cameraImage) {
this.timer = setInterval( this.timer = setInterval(
() => this._updateCameraImageSrc(), () => this._updateCameraImageSrc(),
UPDATE_INTERVAL UPDATE_INTERVAL
); );
} }
} }
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
clearInterval(this.timer); clearInterval(this.timer);
} }
_configChanged(image, stateImage, cameraImage, aspectRatio) { _configChanged(image, stateImage, cameraImage, aspectRatio) {
const ratio = parseAspectRatio(aspectRatio); const ratio = parseAspectRatio(aspectRatio);
if (ratio && ratio.w > 0 && ratio.h > 0) { if (ratio && ratio.w > 0 && ratio.h > 0) {
this.$.wrapper.style.paddingBottom = `${( this.$.wrapper.style.paddingBottom = `${(
(100 * ratio.h) / (100 * ratio.h) /
ratio.w ratio.w
).toFixed(2)}%`; ).toFixed(2)}%`;
this.$.wrapper.classList.add("ratio"); this.$.wrapper.classList.add("ratio");
} }
if (cameraImage) { if (cameraImage) {
this._updateCameraImageSrc(); this._updateCameraImageSrc();
} else if (image && !stateImage) { } else if (image && !stateImage) {
this._imageSrc = image; this._imageSrc = image;
} }
} }
_onImageError() { _onImageError() {
this._imageSrc = null; this._imageSrc = null;
this.$.image.classList.add("hidden"); this.$.image.classList.add("hidden");
if (!this.$.wrapper.classList.contains("ratio")) { if (!this.$.wrapper.classList.contains("ratio")) {
this.$.brokenImage.style.setProperty( this.$.brokenImage.style.setProperty(
"height", "height",
`${this._lastImageHeight || "100"}px` `${this._lastImageHeight || "100"}px`
); );
} }
this.$.brokenImage.classList.remove("hidden"); this.$.brokenImage.classList.remove("hidden");
} }
_onImageLoad() { _onImageLoad() {
this.$.image.classList.remove("hidden"); this.$.image.classList.remove("hidden");
this.$.brokenImage.classList.add("hidden"); this.$.brokenImage.classList.add("hidden");
if (!this.$.wrapper.classList.contains("ratio")) { if (!this.$.wrapper.classList.contains("ratio")) {
this._lastImageHeight = this.$.image.offsetHeight; this._lastImageHeight = this.$.image.offsetHeight;
} }
} }
_hassChanged(hass) { _hassChanged(hass) {
if (this.cameraImage || !this.entity) { if (this.cameraImage || !this.entity) {
return; return;
} }
const stateObj = hass.states[this.entity]; const stateObj = hass.states[this.entity];
const newState = !stateObj ? "unavailable" : stateObj.state; const newState = !stateObj ? "unavailable" : stateObj.state;
if (newState === this._currentState) return; if (newState === this._currentState) return;
this._currentState = newState; this._currentState = newState;
this._updateStateImage(); this._updateStateImage();
this._updateStateFilter(stateObj); this._updateStateFilter(stateObj);
} }
_updateStateImage() { _updateStateImage() {
if (!this.stateImage) { if (!this.stateImage) {
this._imageFallback = true; this._imageFallback = true;
return; return;
} }
const stateImg = this.stateImage[this._currentState]; const stateImg = this.stateImage[this._currentState];
this._imageSrc = stateImg || this.image; this._imageSrc = stateImg || this.image;
this._imageFallback = !stateImg; this._imageFallback = !stateImg;
} }
_updateStateFilter(stateObj) { _updateStateFilter(stateObj) {
let filter; let filter;
if (!this.stateFilter) { if (!this.stateFilter) {
filter = this.filter; filter = this.filter;
} else { } else {
filter = this.stateFilter[this._currentState] || this.filter; filter = this.stateFilter[this._currentState] || this.filter;
} }
const isOff = !stateObj || STATES_OFF.includes(stateObj.state); const isOff = !stateObj || STATES_OFF.includes(stateObj.state);
this.$.image.style.filter = this.$.image.style.filter =
filter || (isOff && this._imageFallback && DEFAULT_FILTER) || ""; filter || (isOff && this._imageFallback && DEFAULT_FILTER) || "";
} }
async _updateCameraImageSrc() { async _updateCameraImageSrc() {
try { try {
const { content_type: contentType, content } = await this.hass.callWS({ const { content_type: contentType, content } = await this.hass.callWS({
type: "camera_thumbnail", type: "camera_thumbnail",
entity_id: this.cameraImage, entity_id: this.cameraImage,
}); });
this._imageSrc = `data:${contentType};base64, ${content}`; this._imageSrc = `data:${contentType};base64, ${content}`;
this._onImageLoad(); this._onImageLoad();
} catch (err) { } catch (err) {
this._onImageError(); this._onImageError();
} }
} }
} }
customElements.define("hui-image", HuiImage); customElements.define("hui-image", HuiImage);

View File

@@ -1,62 +1,62 @@
import "@polymer/paper-button/paper-button"; import "@polymer/paper-button/paper-button";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./hui-notification-item-template"; import "./hui-notification-item-template";
import EventsMixin from "../../../../mixins/events-mixin"; import EventsMixin from "../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../mixins/localize-mixin"; import LocalizeMixin from "../../../../mixins/localize-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
*/ */
export class HuiConfiguratorNotificationItem extends EventsMixin( export class HuiConfiguratorNotificationItem extends EventsMixin(
LocalizeMixin(PolymerElement) LocalizeMixin(PolymerElement)
) { ) {
static get template() { static get template() {
return html` return html`
<hui-notification-item-template> <hui-notification-item-template>
<span slot="header">[[localize('domain.configurator')]]</span> <span slot="header">[[localize('domain.configurator')]]</span>
<div>[[_getMessage(notification)]]</div> <div>[[_getMessage(notification)]]</div>
<paper-button <paper-button
slot="actions" slot="actions"
class="primary" class="primary"
on-click="_handleClick" on-click="_handleClick"
>[[_localizeState(notification.state)]]</paper-button> >[[_localizeState(notification.state)]]</paper-button>
</hui-notification-item-template> </hui-notification-item-template>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
notification: Object, notification: Object,
}; };
} }
_handleClick() { _handleClick() {
this.fire("hass-more-info", { entityId: this.notification.entity_id }); this.fire("hass-more-info", { entityId: this.notification.entity_id });
} }
_localizeState(state) { _localizeState(state) {
return this.localize(`state.configurator.${state}`); return this.localize(`state.configurator.${state}`);
} }
_getMessage(notification) { _getMessage(notification) {
const friendlyName = notification.attributes.friendly_name; const friendlyName = notification.attributes.friendly_name;
return this.localize( return this.localize(
"ui.notification_drawer.click_to_configure", "ui.notification_drawer.click_to_configure",
"entity", "entity",
friendlyName friendlyName
); );
} }
} }
customElements.define( customElements.define(
"hui-configurator-notification-item", "hui-configurator-notification-item",
HuiConfiguratorNotificationItem HuiConfiguratorNotificationItem
); );

View File

@@ -1,175 +1,175 @@
import "@polymer/paper-button/paper-button"; import "@polymer/paper-button/paper-button";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./hui-notification-item"; import "./hui-notification-item";
import EventsMixin from "../../../../mixins/events-mixin"; import EventsMixin from "../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../mixins/localize-mixin"; import LocalizeMixin from "../../../../mixins/localize-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
*/ */
export class HuiNotificationDrawer extends EventsMixin( export class HuiNotificationDrawer extends EventsMixin(
LocalizeMixin(PolymerElement) LocalizeMixin(PolymerElement)
) { ) {
static get template() { static get template() {
return html` return html`
<style include="paper-material-styles"> <style include="paper-material-styles">
:host { :host {
bottom: 0; bottom: 0;
left: 0; left: 0;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
} }
:host([hidden]) { :host([hidden]) {
display: none; display: none;
} }
.container { .container {
align-items: stretch; align-items: stretch;
background: var(--sidebar-background-color, var(--primary-background-color)); background: var(--sidebar-background-color, var(--primary-background-color));
bottom: 0; bottom: 0;
box-shadow: var(--paper-material-elevation-1_-_box-shadow); box-shadow: var(--paper-material-elevation-1_-_box-shadow);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: hidden; overflow-y: hidden;
position: fixed; position: fixed;
top: 0; top: 0;
transition: right .2s ease-in; transition: right .2s ease-in;
width: 500px; width: 500px;
z-index: 10; z-index: 10;
} }
:host(:not(narrow)) .container { :host(:not(narrow)) .container {
right: -500px; right: -500px;
} }
:host([narrow]) .container { :host([narrow]) .container {
right: -100%; right: -100%;
width: 100%; width: 100%;
} }
:host(.open) .container, :host(.open) .container,
:host(.open[narrow]) .container { :host(.open[narrow]) .container {
right: 0; right: 0;
} }
app-toolbar { app-toolbar {
color: var(--primary-text-color); color: var(--primary-text-color);
border-bottom: 1px solid var(--divider-color); border-bottom: 1px solid var(--divider-color);
background-color: var(--primary-background-color); background-color: var(--primary-background-color);
min-height: 64px; min-height: 64px;
width: calc(100% - 32px); width: calc(100% - 32px);
z-index: 11; z-index: 11;
} }
.overlay { .overlay {
display: none; display: none;
} }
:host(.open) .overlay { :host(.open) .overlay {
bottom: 0; bottom: 0;
display: block; display: block;
left: 0; left: 0;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
z-index: 5; z-index: 5;
} }
.notifications { .notifications {
overflow-y: auto; overflow-y: auto;
padding-top: 16px; padding-top: 16px;
} }
.notification { .notification {
padding: 0 16px 16px; padding: 0 16px 16px;
} }
.empty { .empty {
padding: 16px; padding: 16px;
text-align: center; text-align: center;
} }
</style> </style>
<div class="overlay" on-click="_closeDrawer"></div> <div class="overlay" on-click="_closeDrawer"></div>
<div class="container"> <div class="container">
<app-toolbar> <app-toolbar>
<div main-title>[[localize('ui.notification_drawer.title')]]</div> <div main-title>[[localize('ui.notification_drawer.title')]]</div>
<paper-icon-button icon="hass:chevron-right" on-click="_closeDrawer"></paper-icon-button> <paper-icon-button icon="hass:chevron-right" on-click="_closeDrawer"></paper-icon-button>
</app-toolbar> </app-toolbar>
<div class="notifications"> <div class="notifications">
<template is="dom-if" if="[[!_empty(notifications)]]"> <template is="dom-if" if="[[!_empty(notifications)]]">
<dom-repeat items="[[notifications]]"> <dom-repeat items="[[notifications]]">
<template> <template>
<div class="notification"> <div class="notification">
<hui-notification-item hass="[[hass]]" notification="[[item]]"></hui-notification-item> <hui-notification-item hass="[[hass]]" notification="[[item]]"></hui-notification-item>
</div> </div>
</template> </template>
</dom-repeat> </dom-repeat>
</template> </template>
<template is="dom-if" if="[[_empty(notifications)]]"> <template is="dom-if" if="[[_empty(notifications)]]">
<div class="empty">[[localize('ui.notification_drawer.empty')]]<div> <div class="empty">[[localize('ui.notification_drawer.empty')]]<div>
</template> </template>
</div> </div>
</div> </div>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
narrow: { narrow: {
type: Boolean, type: Boolean,
reflectToAttribute: true, reflectToAttribute: true,
}, },
open: { open: {
type: Boolean, type: Boolean,
notify: true, notify: true,
observer: "_openChanged", observer: "_openChanged",
}, },
hidden: { hidden: {
type: Boolean, type: Boolean,
value: true, value: true,
reflectToAttribute: true, reflectToAttribute: true,
}, },
notifications: { notifications: {
type: Array, type: Array,
value: [], value: [],
}, },
}; };
} }
_closeDrawer(ev) { _closeDrawer(ev) {
ev.stopPropagation(); ev.stopPropagation();
this.open = false; this.open = false;
} }
_empty(notifications) { _empty(notifications) {
return notifications.length === 0; return notifications.length === 0;
} }
_openChanged(open) { _openChanged(open) {
clearTimeout(this._openTimer); clearTimeout(this._openTimer);
if (open) { if (open) {
// Render closed then animate open // Render closed then animate open
this.hidden = false; this.hidden = false;
this._openTimer = setTimeout(() => { this._openTimer = setTimeout(() => {
this.classList.add("open"); this.classList.add("open");
}, 50); }, 50);
} else { } else {
// Animate closed then hide // Animate closed then hide
this.classList.remove("open"); this.classList.remove("open");
this._openTimer = setTimeout(() => { this._openTimer = setTimeout(() => {
this.hidden = true; this.hidden = true;
}, 250); }, 250);
} }
} }
} }
customElements.define("hui-notification-drawer", HuiNotificationDrawer); customElements.define("hui-notification-drawer", HuiNotificationDrawer);

View File

@@ -1,49 +1,49 @@
import "@polymer/paper-button/paper-button"; import "@polymer/paper-button/paper-button";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
export class HuiNotificationItemTemplate extends PolymerElement { export class HuiNotificationItemTemplate extends PolymerElement {
static get template() { static get template() {
return html` return html`
<style> <style>
.contents { .contents {
padding: 16px; padding: 16px;
} }
ha-card .header { ha-card .header {
@apply --paper-font-headline; @apply --paper-font-headline;
color: var(--primary-text-color); color: var(--primary-text-color);
padding: 16px 16px 0; padding: 16px 16px 0;
} }
.actions { .actions {
border-top: 1px solid #e8e8e8; border-top: 1px solid #e8e8e8;
padding: 5px 16px; padding: 5px 16px;
} }
::slotted(.primary) { ::slotted(.primary) {
color: var(--primary-color); color: var(--primary-color);
} }
</style> </style>
<ha-card> <ha-card>
<div class="header"> <div class="header">
<slot name="header"></slot> <slot name="header"></slot>
</div> </div>
<div class="contents"> <div class="contents">
<slot></slot> <slot></slot>
</div> </div>
<div class="actions"> <div class="actions">
<slot name="actions"></slot> <slot name="actions"></slot>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
} }
customElements.define( customElements.define(
"hui-notification-item-template", "hui-notification-item-template",
HuiNotificationItemTemplate HuiNotificationItemTemplate
); );

View File

@@ -1,35 +1,35 @@
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import computeDomain from "../../../../common/entity/compute_domain"; import computeDomain from "../../../../common/entity/compute_domain";
import "./hui-configurator-notification-item"; import "./hui-configurator-notification-item";
import "./hui-persistent-notification-item"; import "./hui-persistent-notification-item";
export class HuiNotificationItem extends PolymerElement { export class HuiNotificationItem extends PolymerElement {
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
notification: { notification: {
type: Object, type: Object,
observer: "_stateChanged", observer: "_stateChanged",
}, },
}; };
} }
_stateChanged(notification) { _stateChanged(notification) {
if (this.lastChild) { if (this.lastChild) {
this.removeChild(this.lastChild); this.removeChild(this.lastChild);
} }
if (!notification) return; if (!notification) return;
const domain = notification.entity_id const domain = notification.entity_id
? computeDomain(notification.entity_id) ? computeDomain(notification.entity_id)
: "persistent_notification"; : "persistent_notification";
const tag = `hui-${domain}-notification-item`; const tag = `hui-${domain}-notification-item`;
const el = document.createElement(tag); const el = document.createElement(tag);
el.hass = this.hass; el.hass = this.hass;
el.notification = notification; el.notification = notification;
this.appendChild(el); this.appendChild(el);
} }
} }
customElements.define("hui-notification-item", HuiNotificationItem); customElements.define("hui-notification-item", HuiNotificationItem);

View File

@@ -1,62 +1,62 @@
import "@polymer/paper-button/paper-button"; import "@polymer/paper-button/paper-button";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../../../../mixins/events-mixin"; import EventsMixin from "../../../../mixins/events-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
*/ */
export class HuiNotificationsButton extends EventsMixin(PolymerElement) { export class HuiNotificationsButton extends EventsMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
<style> <style>
:host { :host {
position: relative; position: relative;
} }
.indicator { .indicator {
position: absolute; position: absolute;
top: 10px; top: 10px;
right: 10px; right: 10px;
width: 10px; width: 10px;
height: 10px; height: 10px;
border-radius: 50%; border-radius: 50%;
background: var(--accent-color); background: var(--accent-color);
pointer-events: none; pointer-events: none;
} }
.indicator[hidden] { .indicator[hidden] {
display: none; display: none;
} }
</style> </style>
<paper-icon-button icon="hass:bell" on-click="_clicked"></paper-icon-button> <paper-icon-button icon="hass:bell" on-click="_clicked"></paper-icon-button>
<span class="indicator" hidden$="[[!_hasNotifications(notifications)]]"></span> <span class="indicator" hidden$="[[!_hasNotifications(notifications)]]"></span>
`; `;
} }
static get properties() { static get properties() {
return { return {
notificationsOpen: { notificationsOpen: {
type: Boolean, type: Boolean,
notify: true, notify: true,
}, },
notifications: { notifications: {
type: Array, type: Array,
value: [], value: [],
}, },
}; };
} }
_clicked() { _clicked() {
this.notificationsOpen = true; this.notificationsOpen = true;
} }
_hasNotifications(notifications) { _hasNotifications(notifications) {
return notifications.length > 0; return notifications.length > 0;
} }
} }
customElements.define("hui-notifications-button", HuiNotificationsButton); customElements.define("hui-notifications-button", HuiNotificationsButton);

View File

@@ -1,89 +1,89 @@
import "@polymer/paper-button/paper-button"; import "@polymer/paper-button/paper-button";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../../components/ha-relative-time"; import "../../../../components/ha-relative-time";
import "../../../../components/ha-markdown"; import "../../../../components/ha-markdown";
import "./hui-notification-item-template"; import "./hui-notification-item-template";
import LocalizeMixin from "../../../../mixins/localize-mixin"; import LocalizeMixin from "../../../../mixins/localize-mixin";
/* /*
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
*/ */
export class HuiPersistentNotificationItem extends LocalizeMixin( export class HuiPersistentNotificationItem extends LocalizeMixin(
PolymerElement PolymerElement
) { ) {
static get template() { static get template() {
return html` return html`
<style> <style>
.time { .time {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-top: 6px; margin-top: 6px;
} }
ha-relative-time { ha-relative-time {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
</style> </style>
<hui-notification-item-template> <hui-notification-item-template>
<span slot="header">[[_computeTitle(notification)]]</span> <span slot="header">[[_computeTitle(notification)]]</span>
<ha-markdown content="[[notification.message]]"></ha-markdown> <ha-markdown content="[[notification.message]]"></ha-markdown>
<div class="time"> <div class="time">
<span> <span>
<ha-relative-time <ha-relative-time
hass="[[hass]]" hass="[[hass]]"
datetime="[[notification.created_at]]" datetime="[[notification.created_at]]"
></ha-relative-time> ></ha-relative-time>
<paper-tooltip>[[_computeTooltip(hass, notification)]]</paper-tooltip> <paper-tooltip>[[_computeTooltip(hass, notification)]]</paper-tooltip>
</span> </span>
</div> </div>
<paper-button <paper-button
slot="actions" slot="actions"
class="primary" class="primary"
on-click="_handleDismiss" on-click="_handleDismiss"
>[[localize('ui.card.persistent_notification.dismiss')]]</paper-button> >[[localize('ui.card.persistent_notification.dismiss')]]</paper-button>
</hui-notification-item-template> </hui-notification-item-template>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
notification: Object, notification: Object,
}; };
} }
_handleDismiss() { _handleDismiss() {
this.hass.callService("persistent_notification", "dismiss", { this.hass.callService("persistent_notification", "dismiss", {
notification_id: this.notification.notification_id, notification_id: this.notification.notification_id,
}); });
} }
_computeTitle(notification) { _computeTitle(notification) {
return notification.title || notification.notification_id; return notification.title || notification.notification_id;
} }
_computeTooltip(hass, notification) { _computeTooltip(hass, notification) {
if (!hass || !notification) return null; if (!hass || !notification) return null;
const d = new Date(notification.created_at); const d = new Date(notification.created_at);
return d.toLocaleDateString(hass.language, { return d.toLocaleDateString(hass.language, {
year: "numeric", year: "numeric",
month: "short", month: "short",
day: "numeric", day: "numeric",
minute: "numeric", minute: "numeric",
hour: "numeric", hour: "numeric",
}); });
} }
} }
customElements.define( customElements.define(
"hui-persistent_notification-notification-item", "hui-persistent_notification-notification-item",
HuiPersistentNotificationItem HuiPersistentNotificationItem
); );

View File

@@ -1,122 +1,122 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import "@polymer/paper-button/paper-button"; import "@polymer/paper-button/paper-button";
import "@polymer/paper-input/paper-textarea"; import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-dialog/paper-dialog"; import "@polymer/paper-dialog/paper-dialog";
// This is not a duplicate import, one is for types, one is for element. // This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line // tslint:disable-next-line
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog"; import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { getCardConfig, updateCardConfig } from "../common/data"; import { getCardConfig, updateCardConfig } from "../common/data";
import "./hui-yaml-editor"; import "./hui-yaml-editor";
import "./hui-yaml-card-preview"; import "./hui-yaml-card-preview";
// This is not a duplicate import, one is for types, one is for element. // This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line // tslint:disable-next-line
import { HuiYAMLCardPreview } from "./hui-yaml-card-preview"; import { HuiYAMLCardPreview } from "./hui-yaml-card-preview";
export class HuiDialogEditCard extends LitElement { export class HuiDialogEditCard extends LitElement {
protected hass?: HomeAssistant; protected hass?: HomeAssistant;
private _cardId?: string; private _cardId?: string;
private _cardConfig?: string; private _cardConfig?: string;
private _reloadLovelace?: () => void; private _reloadLovelace?: () => void;
static get properties(): PropertyDeclarations { static get properties(): PropertyDeclarations {
return { return {
hass: {}, hass: {},
cardId: { cardId: {
type: Number, type: Number,
}, },
_cardConfig: {}, _cardConfig: {},
_dialogClosedCallback: {}, _dialogClosedCallback: {},
}; };
} }
public async showDialog({ hass, cardId, reloadLovelace }) { public async showDialog({ hass, cardId, reloadLovelace }) {
this.hass = hass; this.hass = hass;
this._cardId = cardId; this._cardId = cardId;
this._reloadLovelace = reloadLovelace; this._reloadLovelace = reloadLovelace;
this._cardConfig = ""; this._cardConfig = "";
this._loadConfig(); this._loadConfig();
// Wait till dialog is rendered. // Wait till dialog is rendered.
await this.updateComplete; await this.updateComplete;
this._dialog.open(); this._dialog.open();
} }
private get _dialog(): PaperDialogElement { private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!; return this.shadowRoot!.querySelector("paper-dialog")!;
} }
private get _previewEl(): HuiYAMLCardPreview { private get _previewEl(): HuiYAMLCardPreview {
return this.shadowRoot!.querySelector("hui-yaml-card-preview")!; return this.shadowRoot!.querySelector("hui-yaml-card-preview")!;
} }
protected render() { protected render() {
return html` return html`
<style> <style>
paper-dialog { paper-dialog {
width: 650px; width: 650px;
} }
</style> </style>
<paper-dialog with-backdrop> <paper-dialog with-backdrop>
<h2>Card Configuration</h2> <h2>Card Configuration</h2>
<paper-dialog-scrollable> <paper-dialog-scrollable>
<hui-yaml-editor <hui-yaml-editor
.yaml="${this._cardConfig}" .yaml="${this._cardConfig}"
@yaml-changed="${this._handleYamlChanged}" @yaml-changed="${this._handleYamlChanged}"
></hui-yaml-editor> ></hui-yaml-editor>
<hui-yaml-card-preview <hui-yaml-card-preview
.hass="${this.hass}" .hass="${this.hass}"
.yaml="${this._cardConfig}" .yaml="${this._cardConfig}"
></hui-yaml-card-preview> ></hui-yaml-card-preview>
</paper-dialog-scrollable> </paper-dialog-scrollable>
<div class="paper-dialog-buttons"> <div class="paper-dialog-buttons">
<paper-button @click="${this._closeDialog}">Cancel</paper-button> <paper-button @click="${this._closeDialog}">Cancel</paper-button>
<paper-button @click="${this._updateConfig}">Save</paper-button> <paper-button @click="${this._updateConfig}">Save</paper-button>
</div> </div>
</paper-dialog> </paper-dialog>
`; `;
} }
private _handleYamlChanged(ev) { private _handleYamlChanged(ev) {
this._previewEl.yaml = ev.detail.yaml; this._previewEl.yaml = ev.detail.yaml;
} }
private _closeDialog() { private _closeDialog() {
this._dialog.close(); this._dialog.close();
} }
private async _loadConfig() { private async _loadConfig() {
this._cardConfig = await getCardConfig(this.hass!, this._cardId!); this._cardConfig = await getCardConfig(this.hass!, this._cardId!);
await this.updateComplete; await this.updateComplete;
// This will center the dialog with the updated config // This will center the dialog with the updated config
fireEvent(this._dialog, "iron-resize"); fireEvent(this._dialog, "iron-resize");
} }
private async _updateConfig() { private async _updateConfig() {
const newCardConfig = this.shadowRoot!.querySelector("hui-yaml-editor")! const newCardConfig = this.shadowRoot!.querySelector("hui-yaml-editor")!
.yaml; .yaml;
if (this._cardConfig === newCardConfig) { if (this._cardConfig === newCardConfig) {
this._dialog.close(); this._dialog.close();
return; return;
} }
try { try {
await updateCardConfig(this.hass!, this._cardId!, newCardConfig); await updateCardConfig(this.hass!, this._cardId!, newCardConfig);
this._dialog.close(); this._dialog.close();
this._reloadLovelace!(); this._reloadLovelace!();
} catch (err) { } catch (err) {
alert(`Saving failed: ${err.reason}`); alert(`Saving failed: ${err.reason}`);
} }
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-dialog-edit-card": HuiDialogEditCard; "hui-dialog-edit-card": HuiDialogEditCard;
} }
} }
customElements.define("hui-dialog-edit-card", HuiDialogEditCard); customElements.define("hui-dialog-edit-card", HuiDialogEditCard);

View File

@@ -1,52 +1,52 @@
import yaml from "js-yaml"; import yaml from "js-yaml";
import "@polymer/paper-input/paper-textarea"; import "@polymer/paper-input/paper-textarea";
import createCardElement from "../common/create-card-element"; import createCardElement from "../common/create-card-element";
import createErrorCardConfig from "../common/create-error-card-config"; import createErrorCardConfig from "../common/create-error-card-config";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types"; import { LovelaceCard } from "../types";
export class HuiYAMLCardPreview extends HTMLElement { export class HuiYAMLCardPreview extends HTMLElement {
private _hass?: HomeAssistant; private _hass?: HomeAssistant;
set hass(value: HomeAssistant) { set hass(value: HomeAssistant) {
this._hass = value; this._hass = value;
if (this.lastChild) { if (this.lastChild) {
(this.lastChild as LovelaceCard).hass = value; (this.lastChild as LovelaceCard).hass = value;
} }
} }
set yaml(value: string) { set yaml(value: string) {
if (this.lastChild) { if (this.lastChild) {
this.removeChild(this.lastChild); this.removeChild(this.lastChild);
} }
if (value === "") { if (value === "") {
return; return;
} }
let conf; let conf;
try { try {
conf = yaml.safeLoad(value); conf = yaml.safeLoad(value);
} catch (err) { } catch (err) {
conf = createErrorCardConfig(`Invalid YAML: ${err.message}`, undefined); conf = createErrorCardConfig(`Invalid YAML: ${err.message}`, undefined);
} }
const element = createCardElement(conf); const element = createCardElement(conf);
if (this._hass) { if (this._hass) {
element.hass = this._hass; element.hass = this._hass;
} }
this.appendChild(element); this.appendChild(element);
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-yaml-card-preview": HuiYAMLCardPreview; "hui-yaml-card-preview": HuiYAMLCardPreview;
} }
} }
customElements.define("hui-yaml-card-preview", HuiYAMLCardPreview); customElements.define("hui-yaml-card-preview", HuiYAMLCardPreview);

View File

@@ -1,41 +1,41 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import "@polymer/paper-input/paper-textarea"; import "@polymer/paper-input/paper-textarea";
export class HuiYAMLEditor extends LitElement { export class HuiYAMLEditor extends LitElement {
public yaml?: string; public yaml?: string;
static get properties(): PropertyDeclarations { static get properties(): PropertyDeclarations {
return { return {
yaml: {}, yaml: {},
}; };
} }
protected render() { protected render() {
return html` return html`
<style> <style>
paper-textarea { paper-textarea {
--paper-input-container-shared-input-style_-_font-family: monospace; --paper-input-container-shared-input-style_-_font-family: monospace;
} }
</style> </style>
<paper-textarea <paper-textarea
value="${this.yaml}" value="${this.yaml}"
@value-changed="${this._valueChanged}" @value-changed="${this._valueChanged}"
></paper-textarea> ></paper-textarea>
`; `;
} }
private _valueChanged(ev) { private _valueChanged(ev) {
this.yaml = ev.target.value; this.yaml = ev.target.value;
fireEvent(this, "yaml-changed", { yaml: ev.target.value }); fireEvent(this, "yaml-changed", { yaml: ev.target.value });
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-yaml-editor": HuiYAMLEditor; "hui-yaml-editor": HuiYAMLEditor;
} }
} }
customElements.define("hui-yaml-editor", HuiYAMLEditor); customElements.define("hui-yaml-editor", HuiYAMLEditor);

View File

@@ -1,68 +1,68 @@
import { html, LitElement } from "@polymer/lit-element"; import { html, LitElement } from "@polymer/lit-element";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import { computeTooltip } from "../common/compute-tooltip"; import { computeTooltip } from "../common/compute-tooltip";
import { handleClick } from "../common/handle-click"; import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive"; import { longPress } from "../common/directives/long-press-directive";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceElement, LovelaceElementConfig } from "./types"; import { LovelaceElement, LovelaceElementConfig } from "./types";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
interface Config extends LovelaceElementConfig { interface Config extends LovelaceElementConfig {
icon: string; icon: string;
} }
export class HuiIconElement extends hassLocalizeLitMixin(LitElement) export class HuiIconElement extends hassLocalizeLitMixin(LitElement)
implements LovelaceElement { implements LovelaceElement {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: Config; private _config?: Config;
static get properties() { static get properties() {
return { hass: {}, _config: {} }; return { hass: {}, _config: {} };
} }
public setConfig(config: Config): void { public setConfig(config: Config): void {
if (!config.icon) { if (!config.icon) {
throw Error("Invalid Configuration: 'icon' required"); throw Error("Invalid Configuration: 'icon' required");
} }
this._config = config; this._config = config;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config) { if (!this._config) {
return html``; return html``;
} }
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<ha-icon <ha-icon
.icon="${this._config.icon}" .icon="${this._config.icon}"
.title="${computeTooltip(this.hass!, this._config)}" .title="${computeTooltip(this.hass!, this._config)}"
@ha-click="${() => handleClick(this, this.hass!, this._config!, false)}" @ha-click="${() => handleClick(this, this.hass!, this._config!, false)}"
@ha-hold="${() => handleClick(this, this.hass!, this._config!, true)}" @ha-hold="${() => handleClick(this, this.hass!, this._config!, true)}"
.longPress="${longPress()}" .longPress="${longPress()}"
></ha-icon> ></ha-icon>
`; `;
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
<style> <style>
:host { :host {
cursor: pointer; cursor: pointer;
} }
</style> </style>
`; `;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-icon-element": HuiIconElement; "hui-icon-element": HuiIconElement;
} }
} }
customElements.define("hui-icon-element", HuiIconElement); customElements.define("hui-icon-element", HuiIconElement);

View File

@@ -1,94 +1,94 @@
import { html, LitElement } from "@polymer/lit-element"; import { html, LitElement } from "@polymer/lit-element";
import "../components/hui-image"; import "../components/hui-image";
import { computeTooltip } from "../common/compute-tooltip"; import { computeTooltip } from "../common/compute-tooltip";
import { handleClick } from "../common/handle-click"; import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive"; import { longPress } from "../common/directives/long-press-directive";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceElement, LovelaceElementConfig } from "./types"; import { LovelaceElement, LovelaceElementConfig } from "./types";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
interface Config extends LovelaceElementConfig { interface Config extends LovelaceElementConfig {
image?: string; image?: string;
state_image?: string; state_image?: string;
camera_image?: string; camera_image?: string;
filter?: string; filter?: string;
state_filter?: string; state_filter?: string;
aspect_ratio?: string; aspect_ratio?: string;
} }
export class HuiImageElement extends hassLocalizeLitMixin(LitElement) export class HuiImageElement extends hassLocalizeLitMixin(LitElement)
implements LovelaceElement { implements LovelaceElement {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: Config; private _config?: Config;
static get properties() { static get properties() {
return { hass: {}, _config: {} }; return { hass: {}, _config: {} };
} }
public setConfig(config: Config): void { public setConfig(config: Config): void {
if (!config) { if (!config) {
throw Error("Error in element configuration"); throw Error("Error in element configuration");
} }
this.classList.toggle("clickable", config.tap_action !== "none"); this.classList.toggle("clickable", config.tap_action !== "none");
this._config = config; this._config = config;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config) { if (!this._config) {
return html``; return html``;
} }
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<hui-image <hui-image
.hass=${this.hass} .hass=${this.hass}
.entity="${this._config.entity}" .entity="${this._config.entity}"
.image="${this._config.image}" .image="${this._config.image}"
.stateImage=${this._config.state_image} .stateImage=${this._config.state_image}
.cameraImage=${this._config.camera_image} .cameraImage=${this._config.camera_image}
.filter=${this._config.filter} .filter=${this._config.filter}
.stateFilter=${this._config.state_filter} .stateFilter=${this._config.state_filter}
.title="${computeTooltip(this.hass!, this._config)}" .title="${computeTooltip(this.hass!, this._config)}"
.aspectRatio="${this._config.aspect_ratio}" .aspectRatio="${this._config.aspect_ratio}"
@ha-click="${this._handleClick}" @ha-click="${this._handleClick}"
@ha-hold="${this._handleHold}" @ha-hold="${this._handleHold}"
.longPress="${longPress()}" .longPress="${longPress()}"
></hui-image> ></hui-image>
`; `;
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
<style> <style>
:host(.clickable) { :host(.clickable) {
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
-webkit-touch-callout: none !important; -webkit-touch-callout: none !important;
} }
hui-image { hui-image {
-webkit-user-select: none !important; -webkit-user-select: none !important;
} }
</style> </style>
`; `;
} }
private _handleClick() { private _handleClick() {
handleClick(this, this.hass!, this._config!, false); handleClick(this, this.hass!, this._config!, false);
} }
private _handleHold() { private _handleHold() {
handleClick(this, this.hass!, this._config!, true); handleClick(this, this.hass!, this._config!, true);
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-image-element": HuiImageElement; "hui-image-element": HuiImageElement;
} }
} }
customElements.define("hui-image-element", HuiImageElement); customElements.define("hui-image-element", HuiImageElement);

View File

@@ -1,74 +1,74 @@
import { html, LitElement } from "@polymer/lit-element"; import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
import "../../../components/buttons/ha-call-service-button"; import "../../../components/buttons/ha-call-service-button";
import { LovelaceElement, LovelaceElementConfig } from "./types"; import { LovelaceElement, LovelaceElementConfig } from "./types";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
export class HuiServiceButtonElement extends LitElement export class HuiServiceButtonElement extends LitElement
implements LovelaceElement { implements LovelaceElement {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: LovelaceElementConfig; private _config?: LovelaceElementConfig;
private _domain?: string; private _domain?: string;
private _service?: string; private _service?: string;
static get properties() { static get properties() {
return { _config: {} }; return { _config: {} };
} }
public setConfig(config: LovelaceElementConfig): void { public setConfig(config: LovelaceElementConfig): void {
if (!config || !config.service) { if (!config || !config.service) {
throw Error("Invalid Configuration: 'service' required"); throw Error("Invalid Configuration: 'service' required");
} }
[this._domain, this._service] = config.service.split(".", 2); [this._domain, this._service] = config.service.split(".", 2);
if (!this._domain) { if (!this._domain) {
throw Error("Invalid Configuration: 'service' does not have a domain"); throw Error("Invalid Configuration: 'service' does not have a domain");
} }
if (!this._service) { if (!this._service) {
throw Error( throw Error(
"Invalid Configuration: 'service' does not have a service name" "Invalid Configuration: 'service' does not have a service name"
); );
} }
this._config = config; this._config = config;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config) { if (!this._config) {
return html``; return html``;
} }
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<ha-call-service-button <ha-call-service-button
.hass="${this.hass}" .hass="${this.hass}"
.domain="${this._domain}" .domain="${this._domain}"
.service="${this._service}" .service="${this._service}"
.service-data="${this._config.service_data}" .service-data="${this._config.service_data}"
>${this._config.title}</ha-call-service-button> >${this._config.title}</ha-call-service-button>
`; `;
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
<style> <style>
ha-call-service-button { ha-call-service-button {
color: var(--primary-color); color: var(--primary-color);
white-space: nowrap; white-space: nowrap;
} }
</style> </style>
`; `;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-service-button-element": HuiServiceButtonElement; "hui-service-button-element": HuiServiceButtonElement;
} }
} }
customElements.define("hui-service-button-element", HuiServiceButtonElement); customElements.define("hui-service-button-element", HuiServiceButtonElement);

View File

@@ -1,53 +1,53 @@
import { html, LitElement } from "@polymer/lit-element"; import { html, LitElement } from "@polymer/lit-element";
import "../../../components/entity/ha-state-label-badge"; import "../../../components/entity/ha-state-label-badge";
import computeStateName from "../../../common/entity/compute_state_name"; import computeStateName from "../../../common/entity/compute_state_name";
import { LovelaceElement, LovelaceElementConfig } from "./types"; import { LovelaceElement, LovelaceElementConfig } from "./types";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
export class HuiStateBadgeElement extends LitElement export class HuiStateBadgeElement extends LitElement
implements LovelaceElement { implements LovelaceElement {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: LovelaceElementConfig; private _config?: LovelaceElementConfig;
static get properties() { static get properties() {
return { hass: {}, _config: {} }; return { hass: {}, _config: {} };
} }
public setConfig(config: LovelaceElementConfig): void { public setConfig(config: LovelaceElementConfig): void {
if (!config.entity) { if (!config.entity) {
throw Error("Invalid Configuration: 'entity' required"); throw Error("Invalid Configuration: 'entity' required");
} }
this._config = config; this._config = config;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.hass.states[this._config.entity!] !this.hass.states[this._config.entity!]
) { ) {
return html``; return html``;
} }
const state = this.hass.states[this._config.entity!]; const state = this.hass.states[this._config.entity!];
return html` return html`
<ha-state-label-badge <ha-state-label-badge
.hass=${this.hass} .hass=${this.hass}
.state=${state} .state=${state}
.title="${computeStateName(state)}" .title="${computeStateName(state)}"
></ha-state-label-badge> ></ha-state-label-badge>
`; `;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-state-badge-element": HuiStateBadgeElement; "hui-state-badge-element": HuiStateBadgeElement;
} }
} }
customElements.define("hui-state-badge-element", HuiStateBadgeElement); customElements.define("hui-state-badge-element", HuiStateBadgeElement);

View File

@@ -1,77 +1,77 @@
import { html, LitElement } from "@polymer/lit-element"; import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
import "../../../components/entity/state-badge"; import "../../../components/entity/state-badge";
import { computeTooltip } from "../common/compute-tooltip"; import { computeTooltip } from "../common/compute-tooltip";
import { handleClick } from "../common/handle-click"; import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive"; import { longPress } from "../common/directives/long-press-directive";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceElement, LovelaceElementConfig } from "./types"; import { LovelaceElement, LovelaceElementConfig } from "./types";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
export class HuiStateIconElement extends hassLocalizeLitMixin(LitElement) export class HuiStateIconElement extends hassLocalizeLitMixin(LitElement)
implements LovelaceElement { implements LovelaceElement {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: LovelaceElementConfig; private _config?: LovelaceElementConfig;
static get properties() { static get properties() {
return { hass: {}, _config: {} }; return { hass: {}, _config: {} };
} }
public setConfig(config: LovelaceElementConfig): void { public setConfig(config: LovelaceElementConfig): void {
if (!config.entity) { if (!config.entity) {
throw Error("Invalid Configuration: 'entity' required"); throw Error("Invalid Configuration: 'entity' required");
} }
this._config = config; this._config = config;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.hass.states[this._config.entity!] !this.hass.states[this._config.entity!]
) { ) {
return html``; return html``;
} }
const state = this.hass!.states[this._config.entity!]; const state = this.hass!.states[this._config.entity!];
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<state-badge <state-badge
.stateObj=${state} .stateObj=${state}
.title="${computeTooltip(this.hass!, this._config)}" .title="${computeTooltip(this.hass!, this._config)}"
@ha-click="${this._handleClick}" @ha-click="${this._handleClick}"
@ha-hold="${this._handleHold}" @ha-hold="${this._handleHold}"
.longPress="${longPress()}" .longPress="${longPress()}"
></state-badge> ></state-badge>
`; `;
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
<style> <style>
:host { :host {
cursor: pointer; cursor: pointer;
} }
</style> </style>
`; `;
} }
private _handleClick() { private _handleClick() {
handleClick(this, this.hass!, this._config!, false); handleClick(this, this.hass!, this._config!, false);
} }
private _handleHold() { private _handleHold() {
handleClick(this, this.hass!, this._config!, true); handleClick(this, this.hass!, this._config!, true);
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-state-icon-element": HuiStateIconElement; "hui-state-icon-element": HuiStateIconElement;
} }
} }
customElements.define("hui-state-icon-element", HuiStateIconElement); customElements.define("hui-state-icon-element", HuiStateIconElement);

View File

@@ -1,78 +1,78 @@
import { html, LitElement } from "@polymer/lit-element"; import { html, LitElement } from "@polymer/lit-element";
import "../../../components/entity/ha-state-label-badge"; import "../../../components/entity/ha-state-label-badge";
import computeStateDisplay from "../../../common/entity/compute_state_display"; import computeStateDisplay from "../../../common/entity/compute_state_display";
import { computeTooltip } from "../common/compute-tooltip"; import { computeTooltip } from "../common/compute-tooltip";
import { handleClick } from "../common/handle-click"; import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive"; import { longPress } from "../common/directives/long-press-directive";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceElement, LovelaceElementConfig } from "./types"; import { LovelaceElement, LovelaceElementConfig } from "./types";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
interface Config extends LovelaceElementConfig { interface Config extends LovelaceElementConfig {
prefix?: string; prefix?: string;
suffix?: string; suffix?: string;
} }
class HuiStateLabelElement extends hassLocalizeLitMixin(LitElement) class HuiStateLabelElement extends hassLocalizeLitMixin(LitElement)
implements LovelaceElement { implements LovelaceElement {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: Config; private _config?: Config;
static get properties() { static get properties() {
return { hass: {}, _config: {} }; return { hass: {}, _config: {} };
} }
public setConfig(config: Config): void { public setConfig(config: Config): void {
if (!config.entity) { if (!config.entity) {
throw Error("Invalid Configuration: 'entity' required"); throw Error("Invalid Configuration: 'entity' required");
} }
this._config = config; this._config = config;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config) { if (!this._config) {
return html``; return html``;
} }
const state = this.hass!.states[this._config.entity!]; const state = this.hass!.states[this._config.entity!];
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<div <div
.title="${computeTooltip(this.hass!, this._config)}" .title="${computeTooltip(this.hass!, this._config)}"
@ha-click="${() => handleClick(this, this.hass!, this._config!, false)}" @ha-click="${() => handleClick(this, this.hass!, this._config!, false)}"
@ha-hold="${() => handleClick(this, this.hass!, this._config!, true)}" @ha-hold="${() => handleClick(this, this.hass!, this._config!, true)}"
.longPress="${longPress()}" .longPress="${longPress()}"
> >
${this._config.prefix}${ ${this._config.prefix}${
state ? computeStateDisplay(this.localize, state) : "-" state ? computeStateDisplay(this.localize, state) : "-"
}${this._config.suffix} }${this._config.suffix}
</div> </div>
`; `;
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
<style> <style>
:host { :host {
cursor: pointer; cursor: pointer;
} }
div { div {
padding: 8px; padding: 8px;
white-space: nowrap; white-space: nowrap;
} }
</style> </style>
`; `;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-state-label-element": HuiStateLabelElement; "hui-state-label-element": HuiStateLabelElement;
} }
} }
customElements.define("hui-state-label-element", HuiStateLabelElement); customElements.define("hui-state-label-element", HuiStateLabelElement);

View File

@@ -1,65 +1,65 @@
import { html, LitElement } from "@polymer/lit-element"; import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
import "../../../components/ha-climate-state"; import "../../../components/ha-climate-state";
import "../components/hui-generic-entity-row"; import "../components/hui-generic-entity-row";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { EntityRow, EntityConfig } from "./types"; import { EntityRow, EntityConfig } from "./types";
class HuiClimateEntityRow extends LitElement implements EntityRow { class HuiClimateEntityRow extends LitElement implements EntityRow {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: EntityConfig; private _config?: EntityConfig;
static get properties() { static get properties() {
return { return {
hass: {}, hass: {},
_config: {}, _config: {},
}; };
} }
public setConfig(config: EntityConfig): void { public setConfig(config: EntityConfig): void {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Invalid Configuration: 'entity' required"); throw new Error("Invalid Configuration: 'entity' required");
} }
this._config = config; this._config = config;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.hass || !this._config) { if (!this.hass || !this._config) {
return html``; return html``;
} }
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<hui-generic-entity-row <hui-generic-entity-row
.hass=${this.hass} .hass=${this.hass}
.config=${this._config} .config=${this._config}
> >
<ha-climate-state <ha-climate-state
.hass=${this.hass} .hass=${this.hass}
.stateObj=${this.hass.states[this._config.entity]} .stateObj=${this.hass.states[this._config.entity]}
></ha-climate-state> ></ha-climate-state>
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
<style> <style>
ha-climate-state { ha-climate-state {
text-align: right; text-align: right;
} }
</style> </style>
`; `;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-climate-entity-row": HuiClimateEntityRow; "hui-climate-entity-row": HuiClimateEntityRow;
} }
} }
customElements.define("hui-climate-entity-row", HuiClimateEntityRow); customElements.define("hui-climate-entity-row", HuiClimateEntityRow);

View File

@@ -1,74 +1,74 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/hui-generic-entity-row"; import "../components/hui-generic-entity-row";
import "../../../components/ha-cover-controls"; import "../../../components/ha-cover-controls";
import "../../../components/ha-cover-tilt-controls"; import "../../../components/ha-cover-tilt-controls";
import CoverEntity from "../../../util/cover-model"; import CoverEntity from "../../../util/cover-model";
class HuiCoverEntityRow extends PolymerElement { class HuiCoverEntityRow extends PolymerElement {
static get template() { static get template() {
return html` return html`
${this.styleTemplate} ${this.styleTemplate}
<hui-generic-entity-row <hui-generic-entity-row
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
> >
${this.coverControlTemplate} ${this.coverControlTemplate}
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get styleTemplate() { static get styleTemplate() {
return html` return html`
<style> <style>
ha-cover-controls, ha-cover-controls,
ha-cover-tilt-controls { ha-cover-tilt-controls {
margin-right: -.57em; margin-right: -.57em;
} }
</style> </style>
`; `;
} }
static get coverControlTemplate() { static get coverControlTemplate() {
return html` return html`
<template is="dom-if" if="[[!_entityObj.isTiltOnly]]"> <template is="dom-if" if="[[!_entityObj.isTiltOnly]]">
<ha-cover-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-controls> <ha-cover-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-controls>
</template> </template>
<template is="dom-if" if="[[_entityObj.isTiltOnly]]"> <template is="dom-if" if="[[_entityObj.isTiltOnly]]">
<ha-cover-tilt-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-tilt-controls> <ha-cover-tilt-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-tilt-controls>
</template> </template>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
_stateObj: { _stateObj: {
type: Object, type: Object,
computed: "_computeStateObj(hass.states, _config.entity)", computed: "_computeStateObj(hass.states, _config.entity)",
}, },
_entityObj: { _entityObj: {
type: Object, type: Object,
computed: "_computeEntityObj(hass, _stateObj)", computed: "_computeEntityObj(hass, _stateObj)",
}, },
}; };
} }
_computeStateObj(states, entityId) { _computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null; return states && entityId in states ? states[entityId] : null;
} }
_computeEntityObj(hass, stateObj) { _computeEntityObj(hass, stateObj) {
return stateObj ? new CoverEntity(hass, stateObj) : null; return stateObj ? new CoverEntity(hass, stateObj) : null;
} }
setConfig(config) { setConfig(config) {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Entity not configured."); throw new Error("Entity not configured.");
} }
this._config = config; this._config = config;
} }
} }
customElements.define("hui-cover-entity-row", HuiCoverEntityRow); customElements.define("hui-cover-entity-row", HuiCoverEntityRow);

View File

@@ -1,78 +1,78 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/hui-generic-entity-row"; import "../components/hui-generic-entity-row";
import "../../../components/entity/ha-entity-toggle"; import "../../../components/entity/ha-entity-toggle";
import computeStateDisplay from "../../../common/entity/compute_state_display"; import computeStateDisplay from "../../../common/entity/compute_state_display";
import { DOMAINS_TOGGLE } from "../../../common/const"; import { DOMAINS_TOGGLE } from "../../../common/const";
import LocalizeMixin from "../../../mixins/localize-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin";
/* /*
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
*/ */
class HuiGroupEntityRow extends LocalizeMixin(PolymerElement) { class HuiGroupEntityRow extends LocalizeMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
<hui-generic-entity-row <hui-generic-entity-row
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
> >
${this.groupControlTemplate} ${this.groupControlTemplate}
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get groupControlTemplate() { static get groupControlTemplate() {
return html` return html`
<template is="dom-if" if="[[_canToggle]]"> <template is="dom-if" if="[[_canToggle]]">
<ha-entity-toggle <ha-entity-toggle
hass="[[hass]]" hass="[[hass]]"
state-obj="[[_stateObj]]" state-obj="[[_stateObj]]"
></ha-entity-toggle> ></ha-entity-toggle>
</template> </template>
<template is="dom-if" if="[[!_canToggle]]"> <template is="dom-if" if="[[!_canToggle]]">
<div> <div>
[[_computeState(_stateObj)]] [[_computeState(_stateObj)]]
</div> </div>
</template> </template>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
_stateObj: { _stateObj: {
type: Object, type: Object,
computed: "_computeStateObj(hass.states, _config.entity)", computed: "_computeStateObj(hass.states, _config.entity)",
}, },
_canToggle: { _canToggle: {
type: Boolean, type: Boolean,
computed: "_computeCanToggle(_stateObj.attributes.entity_id)", computed: "_computeCanToggle(_stateObj.attributes.entity_id)",
}, },
}; };
} }
setConfig(config) { setConfig(config) {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Entity not configured."); throw new Error("Entity not configured.");
} }
this._config = config; this._config = config;
} }
_computeStateObj(states, entityId) { _computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null; return states && entityId in states ? states[entityId] : null;
} }
_computeCanToggle(entityIds) { _computeCanToggle(entityIds) {
return entityIds.some((entityId) => return entityIds.some((entityId) =>
DOMAINS_TOGGLE.has(entityId.split(".", 1)[0]) DOMAINS_TOGGLE.has(entityId.split(".", 1)[0])
); );
} }
_computeState(stateObj) { _computeState(stateObj) {
return computeStateDisplay(this.localize, stateObj); return computeStateDisplay(this.localize, stateObj);
} }
} }
customElements.define("hui-group-entity-row", HuiGroupEntityRow); customElements.define("hui-group-entity-row", HuiGroupEntityRow);

View File

@@ -1,170 +1,170 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior"; import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior";
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class"; import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
import "../components/hui-generic-entity-row"; import "../components/hui-generic-entity-row";
import "../../../components/ha-slider"; import "../../../components/ha-slider";
class HuiInputNumberEntityRow extends mixinBehaviors( class HuiInputNumberEntityRow extends mixinBehaviors(
[IronResizableBehavior], [IronResizableBehavior],
PolymerElement PolymerElement
) { ) {
static get template() { static get template() {
return html` return html`
${this.styleTemplate} ${this.styleTemplate}
<hui-generic-entity-row <hui-generic-entity-row
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
id="input_number_card" id="input_number_card"
> >
${this.inputNumberControlTemplate} ${this.inputNumberControlTemplate}
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get styleTemplate() { static get styleTemplate() {
return html` return html`
<style> <style>
.flex { .flex {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.state { .state {
min-width: 45px; min-width: 45px;
text-align: center; text-align: center;
} }
paper-input { paper-input {
text-align: right; text-align: right;
} }
</style> </style>
`; `;
} }
static get inputNumberControlTemplate() { static get inputNumberControlTemplate() {
return html` return html`
<div> <div>
<template is="dom-if" if="[[_equals(_stateObj.attributes.mode, 'slider')]]"> <template is="dom-if" if="[[_equals(_stateObj.attributes.mode, 'slider')]]">
<div class="flex"> <div class="flex">
<ha-slider <ha-slider
min="[[_min]]" min="[[_min]]"
max="[[_max]]" max="[[_max]]"
value="{{_value}}" value="{{_value}}"
step="[[_step]]" step="[[_step]]"
pin pin
on-change="_selectedValueChanged" on-change="_selectedValueChanged"
ignore-bar-touch ignore-bar-touch
></ha-slider> ></ha-slider>
<span class="state">[[_value]] [[_stateObj.attributes.unit_of_measurement]]</span> <span class="state">[[_value]] [[_stateObj.attributes.unit_of_measurement]]</span>
</div> </div>
</template> </template>
<template is="dom-if" if="[[_equals(_stateObj.attributes.mode, 'box')]]"> <template is="dom-if" if="[[_equals(_stateObj.attributes.mode, 'box')]]">
<paper-input <paper-input
no-label-float no-label-float
auto-validate auto-validate
pattern="[0-9]+([\\.][0-9]+)?" pattern="[0-9]+([\\.][0-9]+)?"
step="[[_step]]" step="[[_step]]"
min="[[_min]]" min="[[_min]]"
max="[[_max]]" max="[[_max]]"
value="{{_value}}" value="{{_value}}"
type="number" type="number"
on-change="_selectedValueChanged" on-change="_selectedValueChanged"
></paper-input> ></paper-input>
</template> </template>
</div> </div>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
_stateObj: { _stateObj: {
type: Object, type: Object,
computed: "_computeStateObj(hass.states, _config.entity)", computed: "_computeStateObj(hass.states, _config.entity)",
observer: "_stateObjChanged", observer: "_stateObjChanged",
}, },
_min: { _min: {
type: Number, type: Number,
value: 0, value: 0,
}, },
_max: { _max: {
type: Number, type: Number,
value: 100, value: 100,
}, },
_step: Number, _step: Number,
_value: Number, _value: Number,
}; };
} }
ready() { ready() {
super.ready(); super.ready();
if (typeof ResizeObserver === "function") { if (typeof ResizeObserver === "function") {
const ro = new ResizeObserver((entries) => { const ro = new ResizeObserver((entries) => {
entries.forEach(() => { entries.forEach(() => {
this._hiddenState(); this._hiddenState();
}); });
}); });
ro.observe(this.$.input_number_card); ro.observe(this.$.input_number_card);
} else { } else {
this.addEventListener("iron-resize", this._hiddenState); this.addEventListener("iron-resize", this._hiddenState);
} }
} }
_equals(a, b) { _equals(a, b) {
return a === b; return a === b;
} }
_computeStateObj(states, entityId) { _computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null; return states && entityId in states ? states[entityId] : null;
} }
setConfig(config) { setConfig(config) {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Entity not configured."); throw new Error("Entity not configured.");
} }
this._config = config; this._config = config;
} }
_hiddenState() { _hiddenState() {
if ( if (
!this.$ || !this.$ ||
!this._stateObj || !this._stateObj ||
this._stateObj.attributes.mode !== "slider" this._stateObj.attributes.mode !== "slider"
) )
return; return;
const width = this.$.input_number_card.offsetWidth; const width = this.$.input_number_card.offsetWidth;
const stateEl = this.shadowRoot.querySelector(".state"); const stateEl = this.shadowRoot.querySelector(".state");
if (!stateEl) return; if (!stateEl) return;
stateEl.hidden = width <= 350; stateEl.hidden = width <= 350;
} }
_stateObjChanged(stateObj, oldStateObj) { _stateObjChanged(stateObj, oldStateObj) {
if (!stateObj) return; if (!stateObj) return;
this.setProperties({ this.setProperties({
_min: Number(stateObj.attributes.min), _min: Number(stateObj.attributes.min),
_max: Number(stateObj.attributes.max), _max: Number(stateObj.attributes.max),
_step: Number(stateObj.attributes.step), _step: Number(stateObj.attributes.step),
_value: Number(stateObj.state), _value: Number(stateObj.state),
}); });
if ( if (
oldStateObj && oldStateObj &&
stateObj.attributes.mode === "slider" && stateObj.attributes.mode === "slider" &&
oldStateObj.attributes.mode !== "slider" oldStateObj.attributes.mode !== "slider"
) { ) {
this._hiddenState(); this._hiddenState();
} }
} }
_selectedValueChanged() { _selectedValueChanged() {
if (this._value === Number(this._stateObj.state)) return; if (this._value === Number(this._stateObj.state)) return;
this.hass.callService("input_number", "set_value", { this.hass.callService("input_number", "set_value", {
value: this._value, value: this._value,
entity_id: this._stateObj.entity_id, entity_id: this._stateObj.entity_id,
}); });
} }
} }
customElements.define("hui-input-number-entity-row", HuiInputNumberEntityRow); customElements.define("hui-input-number-entity-row", HuiInputNumberEntityRow);

View File

@@ -1,107 +1,107 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import "../../../components/entity/state-badge"; import "../../../components/entity/state-badge";
import computeStateName from "../../../common/entity/compute_state_name"; import computeStateName from "../../../common/entity/compute_state_name";
import EventsMixin from "../../../mixins/events-mixin"; import EventsMixin from "../../../mixins/events-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
*/ */
class HuiInputSelectEntityRow extends EventsMixin(PolymerElement) { class HuiInputSelectEntityRow extends EventsMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
${this.styleTemplate} ${this.styleTemplate}
<template is="dom-if" if="[[_stateObj]]"> <template is="dom-if" if="[[_stateObj]]">
<state-badge state-obj="[[_stateObj]]"></state-badge> <state-badge state-obj="[[_stateObj]]"></state-badge>
<paper-dropdown-menu on-click="_stopPropagation" selected-item-label="{{_selected}}" label="[[_computeName(_config.name, _stateObj)]]"> <paper-dropdown-menu on-click="_stopPropagation" selected-item-label="{{_selected}}" label="[[_computeName(_config.name, _stateObj)]]">
<paper-listbox slot="dropdown-content" selected="[[_computeSelected(_stateObj)]]"> <paper-listbox slot="dropdown-content" selected="[[_computeSelected(_stateObj)]]">
<template is="dom-repeat" items="[[_stateObj.attributes.options]]"> <template is="dom-repeat" items="[[_stateObj.attributes.options]]">
<paper-item>[[item]]</paper-item> <paper-item>[[item]]</paper-item>
</template> </template>
</paper-listbox> </paper-listbox>
</paper-dropdown-menu> </paper-dropdown-menu>
</template> </template>
<template is="dom-if" if="[[!_stateObj]]"> <template is="dom-if" if="[[!_stateObj]]">
<div class="not-found"> <div class="not-found">
Entity not available: [[_config.entity]] Entity not available: [[_config.entity]]
</div> </div>
</template> </template>
`; `;
} }
static get styleTemplate() { static get styleTemplate() {
return html` return html`
<style> <style>
:host { :host {
display: flex; display: flex;
align-items: center; align-items: center;
} }
paper-dropdown-menu { paper-dropdown-menu {
margin-left: 16px; margin-left: 16px;
flex: 1; flex: 1;
} }
.not-found { .not-found {
flex: 1; flex: 1;
background-color: yellow; background-color: yellow;
padding: 8px; padding: 8px;
} }
</style> </style>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
_stateObj: { _stateObj: {
type: Object, type: Object,
computed: "_computeStateObj(hass.states, _config.entity)", computed: "_computeStateObj(hass.states, _config.entity)",
}, },
_selected: { _selected: {
type: String, type: String,
observer: "_selectedChanged", observer: "_selectedChanged",
}, },
}; };
} }
setConfig(config) { setConfig(config) {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Entity not configured."); throw new Error("Entity not configured.");
} }
this._config = config; this._config = config;
} }
_computeStateObj(states, entityId) { _computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null; return states && entityId in states ? states[entityId] : null;
} }
_computeName(name, stateObj) { _computeName(name, stateObj) {
return name || computeStateName(stateObj); return name || computeStateName(stateObj);
} }
_computeSelected(stateObj) { _computeSelected(stateObj) {
return stateObj.attributes.options.indexOf(stateObj.state); return stateObj.attributes.options.indexOf(stateObj.state);
} }
_selectedChanged(option) { _selectedChanged(option) {
// Selected Option will transition to '' before transitioning to new value // Selected Option will transition to '' before transitioning to new value
if (option === "" || option === this._stateObj.state) { if (option === "" || option === this._stateObj.state) {
return; return;
} }
this.hass.callService("input_select", "select_option", { this.hass.callService("input_select", "select_option", {
option: option, option: option,
entity_id: this._stateObj.entity_id, entity_id: this._stateObj.entity_id,
}); });
} }
_stopPropagation(ev) { _stopPropagation(ev) {
ev.stopPropagation(); ev.stopPropagation();
} }
} }
customElements.define("hui-input-select-entity-row", HuiInputSelectEntityRow); customElements.define("hui-input-select-entity-row", HuiInputSelectEntityRow);

View File

@@ -1,73 +1,73 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "../components/hui-generic-entity-row"; import "../components/hui-generic-entity-row";
class HuiInputTextEntityRow extends PolymerElement { class HuiInputTextEntityRow extends PolymerElement {
static get template() { static get template() {
return html` return html`
<hui-generic-entity-row <hui-generic-entity-row
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
> >
${this.inputTextControlTemplate} ${this.inputTextControlTemplate}
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get inputTextControlTemplate() { static get inputTextControlTemplate() {
return html` return html`
<paper-input <paper-input
no-label-float no-label-float
minlength="[[_stateObj.attributes.min]]" minlength="[[_stateObj.attributes.min]]"
maxlength="[[_stateObj.attributes.max]]" maxlength="[[_stateObj.attributes.max]]"
value="{{_value}}" value="{{_value}}"
auto-validate="[[_stateObj.attributes.pattern]]" auto-validate="[[_stateObj.attributes.pattern]]"
pattern="[[_stateObj.attributes.pattern]]" pattern="[[_stateObj.attributes.pattern]]"
type="[[_stateObj.attributes.mode]]" type="[[_stateObj.attributes.mode]]"
on-change="_selectedValueChanged" on-change="_selectedValueChanged"
placeholder="(empty value)" placeholder="(empty value)"
></paper-input> ></paper-input>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
_stateObj: { _stateObj: {
type: Object, type: Object,
computed: "_computeStateObj(hass.states, _config.entity)", computed: "_computeStateObj(hass.states, _config.entity)",
observer: "_stateObjChanged", observer: "_stateObjChanged",
}, },
_value: String, _value: String,
}; };
} }
_computeStateObj(states, entityId) { _computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null; return states && entityId in states ? states[entityId] : null;
} }
setConfig(config) { setConfig(config) {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Entity not configured."); throw new Error("Entity not configured.");
} }
this._config = config; this._config = config;
} }
_stateObjChanged(stateObj) { _stateObjChanged(stateObj) {
this._value = stateObj && stateObj.state; this._value = stateObj && stateObj.state;
} }
_selectedValueChanged() { _selectedValueChanged() {
if (this._value === this._stateObj.state) { if (this._value === this._stateObj.state) {
return; return;
} }
this.hass.callService("input_text", "set_value", { this.hass.callService("input_text", "set_value", {
value: this._value, value: this._value,
entity_id: this._stateObj.entity_id, entity_id: this._stateObj.entity_id,
}); });
} }
} }
customElements.define("hui-input-text-entity-row", HuiInputTextEntityRow); customElements.define("hui-input-text-entity-row", HuiInputTextEntityRow);

View File

@@ -1,83 +1,83 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-button/paper-button"; import "@polymer/paper-button/paper-button";
import "../components/hui-generic-entity-row"; import "../components/hui-generic-entity-row";
import LocalizeMixin from "../../../mixins/localize-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin";
/* /*
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
*/ */
class HuiLockEntityRow extends LocalizeMixin(PolymerElement) { class HuiLockEntityRow extends LocalizeMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
${this.styleTemplate} ${this.styleTemplate}
<hui-generic-entity-row <hui-generic-entity-row
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
> >
${this.lockControlTemplate} ${this.lockControlTemplate}
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get styleTemplate() { static get styleTemplate() {
return html` return html`
<style> <style>
paper-button { paper-button {
color: var(--primary-color); color: var(--primary-color);
font-weight: 500; font-weight: 500;
margin-right: -.57em; margin-right: -.57em;
} }
</style> </style>
`; `;
} }
static get lockControlTemplate() { static get lockControlTemplate() {
return html` return html`
<paper-button on-click="_callService"> <paper-button on-click="_callService">
[[_computeButtonTitle(_stateObj.state)]] [[_computeButtonTitle(_stateObj.state)]]
</paper-button> </paper-button>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
_stateObj: { _stateObj: {
type: Object, type: Object,
computed: "_computeStateObj(hass.states, _config.entity)", computed: "_computeStateObj(hass.states, _config.entity)",
}, },
}; };
} }
_computeStateObj(states, entityId) { _computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null; return states && entityId in states ? states[entityId] : null;
} }
setConfig(config) { setConfig(config) {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Entity not configured."); throw new Error("Entity not configured.");
} }
this._config = config; this._config = config;
} }
_computeButtonTitle(state) { _computeButtonTitle(state) {
return state === "locked" return state === "locked"
? this.localize("ui.card.lock.unlock") ? this.localize("ui.card.lock.unlock")
: this.localize("ui.card.lock.lock"); : this.localize("ui.card.lock.lock");
} }
_callService(ev) { _callService(ev) {
ev.stopPropagation(); ev.stopPropagation();
const stateObj = this._stateObj; const stateObj = this._stateObj;
this.hass.callService( this.hass.callService(
"lock", "lock",
stateObj.state === "locked" ? "unlock" : "lock", stateObj.state === "locked" ? "unlock" : "lock",
{ entity_id: stateObj.entity_id } { entity_id: stateObj.entity_id }
); );
} }
} }
customElements.define("hui-lock-entity-row", HuiLockEntityRow); customElements.define("hui-lock-entity-row", HuiLockEntityRow);

View File

@@ -1,162 +1,162 @@
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/hui-generic-entity-row"; import "../components/hui-generic-entity-row";
import LocalizeMixin from "../../../mixins/localize-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin";
const SUPPORT_PAUSE = 1; const SUPPORT_PAUSE = 1;
const SUPPORT_NEXT_TRACK = 32; const SUPPORT_NEXT_TRACK = 32;
const SUPPORTS_PLAY = 16384; const SUPPORTS_PLAY = 16384;
const OFF_STATES = ["off", "idle"]; const OFF_STATES = ["off", "idle"];
/* /*
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
*/ */
class HuiMediaPlayerEntityRow extends LocalizeMixin(PolymerElement) { class HuiMediaPlayerEntityRow extends LocalizeMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
${this.styleTemplate} ${this.styleTemplate}
<hui-generic-entity-row <hui-generic-entity-row
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
show-secondary="false" show-secondary="false"
> >
${this.mediaPlayerControlTemplate} ${this.mediaPlayerControlTemplate}
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get styleTemplate() { static get styleTemplate() {
return html` return html`
<style> <style>
.controls { .controls {
white-space: nowrap; white-space: nowrap;
} }
</style> </style>
`; `;
} }
static get mediaPlayerControlTemplate() { static get mediaPlayerControlTemplate() {
return html` return html`
<template is="dom-if" if="[[!_isOff(_stateObj.state)]]"> <template is="dom-if" if="[[!_isOff(_stateObj.state)]]">
<div class="controls"> <div class="controls">
<template is="dom-if" if="[[_computeControlIcon(_stateObj)]]"> <template is="dom-if" if="[[_computeControlIcon(_stateObj)]]">
<paper-icon-button <paper-icon-button
icon="[[_computeControlIcon(_stateObj)]]" icon="[[_computeControlIcon(_stateObj)]]"
on-click="_playPause" on-click="_playPause"
></paper-icon-button> ></paper-icon-button>
</template> </template>
<template is="dom-if" if="[[_supportsNext(_stateObj)]]"> <template is="dom-if" if="[[_supportsNext(_stateObj)]]">
<paper-icon-button <paper-icon-button
icon="hass:skip-next" icon="hass:skip-next"
on-click="_nextTrack" on-click="_nextTrack"
></paper-icon-button> ></paper-icon-button>
</template> </template>
</div> </div>
</template> </template>
<template is="dom-if" if="[[_isOff(_stateObj.state)]]"> <template is="dom-if" if="[[_isOff(_stateObj.state)]]">
<div>[[_computeState(_stateObj.state)]]</div> <div>[[_computeState(_stateObj.state)]]</div>
</template> </template>
<div slot="secondary"> <div slot="secondary">
[[_computeMediaTitle(_stateObj)]] [[_computeMediaTitle(_stateObj)]]
</div> </div>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
_stateObj: { _stateObj: {
type: Object, type: Object,
computed: "_computeStateObj(hass.states, _config.entity)", computed: "_computeStateObj(hass.states, _config.entity)",
}, },
}; };
} }
_computeStateObj(states, entityId) { _computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null; return states && entityId in states ? states[entityId] : null;
} }
setConfig(config) { setConfig(config) {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Entity not configured."); throw new Error("Entity not configured.");
} }
this._config = config; this._config = config;
} }
_computeControlIcon(stateObj) { _computeControlIcon(stateObj) {
if (!stateObj) return null; if (!stateObj) return null;
if (stateObj.state !== "playing") { if (stateObj.state !== "playing") {
return stateObj.attributes.supported_features & SUPPORTS_PLAY return stateObj.attributes.supported_features & SUPPORTS_PLAY
? "hass:play" ? "hass:play"
: ""; : "";
} }
return stateObj.attributes.supported_features & SUPPORT_PAUSE return stateObj.attributes.supported_features & SUPPORT_PAUSE
? "hass:pause" ? "hass:pause"
: "hass:stop"; : "hass:stop";
} }
_computeMediaTitle(stateObj) { _computeMediaTitle(stateObj) {
if (!stateObj || this._isOff(stateObj.state)) return null; if (!stateObj || this._isOff(stateObj.state)) return null;
switch (stateObj.attributes.media_content_type) { switch (stateObj.attributes.media_content_type) {
case "music": case "music":
return `${stateObj.attributes.media_artist}: ${ return `${stateObj.attributes.media_artist}: ${
stateObj.attributes.media_title stateObj.attributes.media_title
}`; }`;
case "tvshow": case "tvshow":
return `${stateObj.attributes.media_series_title}: ${ return `${stateObj.attributes.media_series_title}: ${
stateObj.attributes.media_title stateObj.attributes.media_title
}`; }`;
default: default:
return ( return (
stateObj.attributes.media_title || stateObj.attributes.media_title ||
stateObj.attributes.app_name || stateObj.attributes.app_name ||
stateObj.state stateObj.state
); );
} }
} }
_computeState(state) { _computeState(state) {
return ( return (
this.localize(`state.media_player.${state}`) || this.localize(`state.media_player.${state}`) ||
this.localize(`state.default.${state}`) || this.localize(`state.default.${state}`) ||
state state
); );
} }
_callService(service) { _callService(service) {
this.hass.callService("media_player", service, { this.hass.callService("media_player", service, {
entity_id: this._config.entity, entity_id: this._config.entity,
}); });
} }
_playPause(event) { _playPause(event) {
event.stopPropagation(); event.stopPropagation();
this._callService("media_play_pause"); this._callService("media_play_pause");
} }
_nextTrack(event) { _nextTrack(event) {
event.stopPropagation(); event.stopPropagation();
if (this._stateObj.attributes.supported_features & SUPPORT_NEXT_TRACK) { if (this._stateObj.attributes.supported_features & SUPPORT_NEXT_TRACK) {
this._callService("media_next_track"); this._callService("media_next_track");
} }
} }
_isOff(state) { _isOff(state) {
return OFF_STATES.includes(state); return OFF_STATES.includes(state);
} }
_supportsNext(stateObj) { _supportsNext(stateObj) {
return ( return (
stateObj && stateObj.attributes.supported_features & SUPPORT_NEXT_TRACK stateObj && stateObj.attributes.supported_features & SUPPORT_NEXT_TRACK
); );
} }
} }
customElements.define("hui-media-player-entity-row", HuiMediaPlayerEntityRow); customElements.define("hui-media-player-entity-row", HuiMediaPlayerEntityRow);

View File

@@ -1,70 +1,70 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-button/paper-button"; import "@polymer/paper-button/paper-button";
import "../components/hui-generic-entity-row"; import "../components/hui-generic-entity-row";
import LocalizeMixin from "../../../mixins/localize-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin";
/* /*
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
*/ */
class HuiSceneEntityRow extends LocalizeMixin(PolymerElement) { class HuiSceneEntityRow extends LocalizeMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
${this.styleTemplate} ${this.styleTemplate}
<hui-generic-entity-row <hui-generic-entity-row
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
> >
${this.sceneControlTemplate} ${this.sceneControlTemplate}
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get styleTemplate() { static get styleTemplate() {
return html` return html`
<style> <style>
paper-button { paper-button {
color: var(--primary-color); color: var(--primary-color);
font-weight: 500; font-weight: 500;
margin-right: -.57em; margin-right: -.57em;
} }
</style> </style>
`; `;
} }
static get sceneControlTemplate() { static get sceneControlTemplate() {
return html` return html`
<paper-button on-click="_callService"> <paper-button on-click="_callService">
[[localize('ui.card.scene.activate')]] [[localize('ui.card.scene.activate')]]
</paper-button> </paper-button>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
}; };
} }
_computeStateObj(states, entityId) { _computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null; return states && entityId in states ? states[entityId] : null;
} }
setConfig(config) { setConfig(config) {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Entity not configured."); throw new Error("Entity not configured.");
} }
this._config = config; this._config = config;
} }
_callService(ev) { _callService(ev) {
ev.stopPropagation(); ev.stopPropagation();
this.hass.callService("scene", "turn_on", { this.hass.callService("scene", "turn_on", {
entity_id: this._config.entity, entity_id: this._config.entity,
}); });
} }
} }
customElements.define("hui-scene-entity-row", HuiSceneEntityRow); customElements.define("hui-scene-entity-row", HuiSceneEntityRow);

View File

@@ -1,78 +1,78 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-button/paper-button"; import "@polymer/paper-button/paper-button";
import "../components/hui-generic-entity-row"; import "../components/hui-generic-entity-row";
import "../../../components/entity/ha-entity-toggle"; import "../../../components/entity/ha-entity-toggle";
import LocalizeMixin from "../../../mixins/localize-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin";
/* /*
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
*/ */
class HuiScriptEntityRow extends LocalizeMixin(PolymerElement) { class HuiScriptEntityRow extends LocalizeMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
${this.styleTemplate} ${this.styleTemplate}
<hui-generic-entity-row <hui-generic-entity-row
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
> >
${this.scriptControlTemplate} ${this.scriptControlTemplate}
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get styleTemplate() { static get styleTemplate() {
return html` return html`
<style> <style>
paper-button { paper-button {
color: var(--primary-color); color: var(--primary-color);
font-weight: 500; font-weight: 500;
margin-right: -.57em; margin-right: -.57em;
} }
</style> </style>
`; `;
} }
static get scriptControlTemplate() { static get scriptControlTemplate() {
return html` return html`
<template is="dom-if" if="[[_stateObj.attributes.can_cancel]]"> <template is="dom-if" if="[[_stateObj.attributes.can_cancel]]">
<ha-entity-toggle state-obj="[[_stateObj]]" hass="[[hass]]"></ha-entity-toggle> <ha-entity-toggle state-obj="[[_stateObj]]" hass="[[hass]]"></ha-entity-toggle>
</template> </template>
<template is="dom-if" if="[[!_stateObj.attributes.can_cancel]]"> <template is="dom-if" if="[[!_stateObj.attributes.can_cancel]]">
<paper-button on-click="_callService">[[localize('ui.card.script.execute')]]</paper-button> <paper-button on-click="_callService">[[localize('ui.card.script.execute')]]</paper-button>
</template> </template>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
_stateObj: { _stateObj: {
type: Object, type: Object,
computed: "_computeStateObj(hass.states, _config.entity)", computed: "_computeStateObj(hass.states, _config.entity)",
}, },
}; };
} }
_computeStateObj(states, entityId) { _computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null; return states && entityId in states ? states[entityId] : null;
} }
setConfig(config) { setConfig(config) {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Entity not configured."); throw new Error("Entity not configured.");
} }
this._config = config; this._config = config;
} }
_callService(ev) { _callService(ev) {
ev.stopPropagation(); ev.stopPropagation();
this.hass.callService("script", "turn_on", { this.hass.callService("script", "turn_on", {
entity_id: this._config.entity, entity_id: this._config.entity,
}); });
} }
} }
customElements.define("hui-script-entity-row", HuiScriptEntityRow); customElements.define("hui-script-entity-row", HuiScriptEntityRow);

View File

@@ -1,70 +1,70 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/hui-generic-entity-row"; import "../components/hui-generic-entity-row";
import computeStateDisplay from "../../../common/entity/compute_state_display"; import computeStateDisplay from "../../../common/entity/compute_state_display";
import LocalizeMixin from "../../../mixins/localize-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin";
/* /*
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
*/ */
class HuiTextEntityRow extends LocalizeMixin(PolymerElement) { class HuiTextEntityRow extends LocalizeMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
${this.styleTemplate} ${this.styleTemplate}
<hui-generic-entity-row <hui-generic-entity-row
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
> >
${this.textControlTemplate} ${this.textControlTemplate}
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get styleTemplate() { static get styleTemplate() {
return html` return html`
<style> <style>
div { div {
text-align: right; text-align: right;
} }
</style> </style>
`; `;
} }
static get textControlTemplate() { static get textControlTemplate() {
return html` return html`
<div> <div>
[[_computeState(_stateObj)]] [[_computeState(_stateObj)]]
</div> </div>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
_stateObj: { _stateObj: {
type: Object, type: Object,
computed: "_computeStateObj(hass.states, _config.entity)", computed: "_computeStateObj(hass.states, _config.entity)",
}, },
}; };
} }
_computeStateObj(states, entityId) { _computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null; return states && entityId in states ? states[entityId] : null;
} }
setConfig(config) { setConfig(config) {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Entity not configured."); throw new Error("Entity not configured.");
} }
this._config = config; this._config = config;
} }
_computeState(stateObj) { _computeState(stateObj) {
return stateObj && computeStateDisplay(this.localize, stateObj); return stateObj && computeStateDisplay(this.localize, stateObj);
} }
} }
customElements.define("hui-text-entity-row", HuiTextEntityRow); customElements.define("hui-text-entity-row", HuiTextEntityRow);

View File

@@ -1,103 +1,103 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/hui-generic-entity-row"; import "../components/hui-generic-entity-row";
import timerTimeRemaining from "../../../common/entity/timer_time_remaining"; import timerTimeRemaining from "../../../common/entity/timer_time_remaining";
import secondsToDuration from "../../../common/datetime/seconds_to_duration"; import secondsToDuration from "../../../common/datetime/seconds_to_duration";
class HuiTimerEntityRow extends PolymerElement { class HuiTimerEntityRow extends PolymerElement {
static get template() { static get template() {
return html` return html`
<hui-generic-entity-row <hui-generic-entity-row
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
> >
${this.timerControlTemplate} ${this.timerControlTemplate}
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get timerControlTemplate() { static get timerControlTemplate() {
return html` return html`
<div> <div>
[[_computeDisplay(_stateObj, _timeRemaining)]] [[_computeDisplay(_stateObj, _timeRemaining)]]
</div> </div>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
_stateObj: { _stateObj: {
type: Object, type: Object,
computed: "_computeStateObj(hass.states, _config.entity)", computed: "_computeStateObj(hass.states, _config.entity)",
observer: "_stateObjChanged", observer: "_stateObjChanged",
}, },
_timeRemaining: Number, _timeRemaining: Number,
}; };
} }
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
this._clearInterval(); this._clearInterval();
} }
_stateObjChanged(stateObj) { _stateObjChanged(stateObj) {
if (stateObj) { if (stateObj) {
this._startInterval(stateObj); this._startInterval(stateObj);
} else { } else {
this._clearInterval(); this._clearInterval();
} }
} }
_clearInterval() { _clearInterval() {
if (this._updateRemaining) { if (this._updateRemaining) {
clearInterval(this._updateRemaining); clearInterval(this._updateRemaining);
this._updateRemaining = null; this._updateRemaining = null;
} }
} }
_startInterval(stateObj) { _startInterval(stateObj) {
this._clearInterval(); this._clearInterval();
this._calculateRemaining(stateObj); this._calculateRemaining(stateObj);
if (stateObj.state === "active") { if (stateObj.state === "active") {
this._updateRemaining = setInterval( this._updateRemaining = setInterval(
() => this._calculateRemaining(this._stateObj), () => this._calculateRemaining(this._stateObj),
1000 1000
); );
} }
} }
_calculateRemaining(stateObj) { _calculateRemaining(stateObj) {
this._timeRemaining = timerTimeRemaining(stateObj); this._timeRemaining = timerTimeRemaining(stateObj);
} }
_computeDisplay(stateObj, time) { _computeDisplay(stateObj, time) {
if (!stateObj) return null; if (!stateObj) return null;
if (stateObj.state === "idle" || time === 0) return stateObj.state; if (stateObj.state === "idle" || time === 0) return stateObj.state;
let display = secondsToDuration(time); let display = secondsToDuration(time);
if (stateObj.state === "paused") { if (stateObj.state === "paused") {
display += " (paused)"; display += " (paused)";
} }
return display; return display;
} }
_computeStateObj(states, entityId) { _computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null; return states && entityId in states ? states[entityId] : null;
} }
setConfig(config) { setConfig(config) {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Entity not configured."); throw new Error("Entity not configured.");
} }
this._config = config; this._config = config;
} }
} }
customElements.define("hui-timer-entity-row", HuiTimerEntityRow); customElements.define("hui-timer-entity-row", HuiTimerEntityRow);

View File

@@ -1,76 +1,76 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/hui-generic-entity-row"; import "../components/hui-generic-entity-row";
import "../../../components/entity/ha-entity-toggle"; import "../../../components/entity/ha-entity-toggle";
import computeStateDisplay from "../../../common/entity/compute_state_display"; import computeStateDisplay from "../../../common/entity/compute_state_display";
import LocalizeMixin from "../../../mixins/localize-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin";
/* /*
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
*/ */
class HuiToggleEntityRow extends LocalizeMixin(PolymerElement) { class HuiToggleEntityRow extends LocalizeMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
<hui-generic-entity-row <hui-generic-entity-row
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
> >
${this.toggleControlTemplate} ${this.toggleControlTemplate}
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get toggleControlTemplate() { static get toggleControlTemplate() {
return html` return html`
<template is="dom-if" if="[[_canToggle]]"> <template is="dom-if" if="[[_canToggle]]">
<ha-entity-toggle <ha-entity-toggle
hass="[[hass]]" hass="[[hass]]"
state-obj="[[_stateObj]]" state-obj="[[_stateObj]]"
></ha-entity-toggle> ></ha-entity-toggle>
</template> </template>
<template is="dom-if" if="[[!_canToggle]]"> <template is="dom-if" if="[[!_canToggle]]">
<div> <div>
[[_computeState(_stateObj)]] [[_computeState(_stateObj)]]
</div> </div>
</template> </template>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
_config: Object, _config: Object,
_stateObj: { _stateObj: {
type: Object, type: Object,
computed: "_computeStateObj(hass.states, _config.entity)", computed: "_computeStateObj(hass.states, _config.entity)",
}, },
_canToggle: { _canToggle: {
type: Boolean, type: Boolean,
computed: "_computeCanToggle(_stateObj.state)", computed: "_computeCanToggle(_stateObj.state)",
}, },
}; };
} }
_computeStateObj(states, entityId) { _computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null; return states && entityId in states ? states[entityId] : null;
} }
_computeCanToggle(state) { _computeCanToggle(state) {
return state === "on" || state === "off"; return state === "on" || state === "off";
} }
_computeState(stateObj) { _computeState(stateObj) {
return stateObj && computeStateDisplay(this.localize, stateObj); return stateObj && computeStateDisplay(this.localize, stateObj);
} }
setConfig(config) { setConfig(config) {
if (!config || !config.entity) { if (!config || !config.entity) {
throw new Error("Entity not configured."); throw new Error("Entity not configured.");
} }
this._config = config; this._config = config;
} }
} }
customElements.define("hui-toggle-entity-row", HuiToggleEntityRow); customElements.define("hui-toggle-entity-row", HuiToggleEntityRow);

View File

@@ -1,125 +1,125 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-button/paper-button"; import "@polymer/paper-button/paper-button";
import "../../layouts/hass-loading-screen"; import "../../layouts/hass-loading-screen";
import "../../layouts/hass-error-screen"; import "../../layouts/hass-error-screen";
import "./hui-root"; import "./hui-root";
class Lovelace extends PolymerElement { class Lovelace extends PolymerElement {
static get template() { static get template() {
return html` return html`
<style> <style>
paper-button { paper-button {
color: var(--primary-color); color: var(--primary-color);
font-weight: 500; font-weight: 500;
} }
</style> </style>
<template is='dom-if' if='[[_equal(_state, "loaded")]]' restamp> <template is='dom-if' if='[[_equal(_state, "loaded")]]' restamp>
<hui-root <hui-root
narrow="[[narrow]]" narrow="[[narrow]]"
show-menu="[[showMenu]]" show-menu="[[showMenu]]"
hass='[[hass]]' hass='[[hass]]'
route="[[route]]" route="[[route]]"
config='[[_config]]' config='[[_config]]'
columns='[[_columns]]' columns='[[_columns]]'
on-config-refresh='_fetchConfig' on-config-refresh='_fetchConfig'
></hui-root> ></hui-root>
</template> </template>
<template is='dom-if' if='[[_equal(_state, "loading")]]' restamp> <template is='dom-if' if='[[_equal(_state, "loading")]]' restamp>
<hass-loading-screen <hass-loading-screen
narrow="[[narrow]]" narrow="[[narrow]]"
show-menu="[[showMenu]]" show-menu="[[showMenu]]"
></hass-loading-screen> ></hass-loading-screen>
</template> </template>
<template is='dom-if' if='[[_equal(_state, "error")]]' restamp> <template is='dom-if' if='[[_equal(_state, "error")]]' restamp>
<hass-error-screen <hass-error-screen
title='Lovelace' title='Lovelace'
error='[[_errorMsg]]' error='[[_errorMsg]]'
narrow="[[narrow]]" narrow="[[narrow]]"
show-menu="[[showMenu]]" show-menu="[[showMenu]]"
> >
<paper-button on-click="_fetchConfig">Reload ui-lovelace.yaml</paper-button> <paper-button on-click="_fetchConfig">Reload ui-lovelace.yaml</paper-button>
</hass-error-screen> </hass-error-screen>
</template> </template>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
narrow: { narrow: {
type: Boolean, type: Boolean,
value: false, value: false,
}, },
showMenu: { showMenu: {
type: Boolean, type: Boolean,
value: false, value: false,
}, },
route: Object, route: Object,
_columns: { _columns: {
type: Number, type: Number,
value: 1, value: 1,
}, },
_state: { _state: {
type: String, type: String,
value: "loading", value: "loading",
}, },
_errorMsg: String, _errorMsg: String,
_config: { _config: {
type: Object, type: Object,
value: null, value: null,
}, },
}; };
} }
static get observers() { static get observers() {
return ["_updateColumns(narrow, showMenu)"]; return ["_updateColumns(narrow, showMenu)"];
} }
ready() { ready() {
this._fetchConfig(); this._fetchConfig();
this._updateColumns = this._updateColumns.bind(this); this._updateColumns = this._updateColumns.bind(this);
this.mqls = [300, 600, 900, 1200].map((width) => { this.mqls = [300, 600, 900, 1200].map((width) => {
const mql = matchMedia(`(min-width: ${width}px)`); const mql = matchMedia(`(min-width: ${width}px)`);
mql.addListener(this._updateColumns); mql.addListener(this._updateColumns);
return mql; return mql;
}); });
this._updateColumns(); this._updateColumns();
super.ready(); super.ready();
} }
_updateColumns() { _updateColumns() {
const matchColumns = this.mqls.reduce((cols, mql) => cols + mql.matches, 0); const matchColumns = this.mqls.reduce((cols, mql) => cols + mql.matches, 0);
// Do -1 column if the menu is docked and open // Do -1 column if the menu is docked and open
this._columns = Math.max(1, matchColumns - (!this.narrow && this.showMenu)); this._columns = Math.max(1, matchColumns - (!this.narrow && this.showMenu));
} }
async _fetchConfig() { async _fetchConfig() {
try { try {
const conf = await this.hass.callWS({ type: "lovelace/config" }); const conf = await this.hass.callWS({ type: "lovelace/config" });
this.setProperties({ this.setProperties({
_config: conf, _config: conf,
_state: "loaded", _state: "loaded",
}); });
} catch (err) { } catch (err) {
this.setProperties({ this.setProperties({
_state: "error", _state: "error",
_errorMsg: err.message, _errorMsg: err.message,
}); });
} }
} }
_equal(a, b) { _equal(a, b) {
return a === b; return a === b;
} }
} }
customElements.define("ha-panel-lovelace", Lovelace); customElements.define("ha-panel-lovelace", Lovelace);

View File

@@ -1,380 +1,380 @@
import "@polymer/app-layout/app-header-layout/app-header-layout"; import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-scroll-effects/effects/waterfall"; import "@polymer/app-layout/app-scroll-effects/effects/waterfall";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/app-route/app-route"; import "@polymer/app-route/app-route";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-menu-button/paper-menu-button"; import "@polymer/paper-menu-button/paper-menu-button";
import "@polymer/paper-tabs/paper-tab"; import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs"; import "@polymer/paper-tabs/paper-tabs";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import scrollToTarget from "../../common/dom/scroll-to-target"; import scrollToTarget from "../../common/dom/scroll-to-target";
import EventsMixin from "../../mixins/events-mixin"; import EventsMixin from "../../mixins/events-mixin";
import NavigateMixin from "../../mixins/navigate-mixin"; import NavigateMixin from "../../mixins/navigate-mixin";
import "../../layouts/ha-app-layout"; import "../../layouts/ha-app-layout";
import "../../components/ha-start-voice-button"; import "../../components/ha-start-voice-button";
import "../../components/ha-icon"; import "../../components/ha-icon";
import { loadModule, loadCSS, loadJS } from "../../common/dom/load_resource"; import { loadModule, loadCSS, loadJS } from "../../common/dom/load_resource";
import { subscribeNotifications } from "../../data/ws-notifications"; import { subscribeNotifications } from "../../data/ws-notifications";
import "./components/notifications/hui-notification-drawer"; import "./components/notifications/hui-notification-drawer";
import "./components/notifications/hui-notifications-button"; import "./components/notifications/hui-notifications-button";
import "./hui-unused-entities"; import "./hui-unused-entities";
import "./hui-view"; import "./hui-view";
import debounce from "../../common/util/debounce"; import debounce from "../../common/util/debounce";
import createCardElement from "./common/create-card-element"; import createCardElement from "./common/create-card-element";
import computeNotifications from "./common/compute-notifications"; import computeNotifications from "./common/compute-notifications";
// CSS and JS should only be imported once. Modules and HTML are safe. // CSS and JS should only be imported once. Modules and HTML are safe.
const CSS_CACHE = {}; const CSS_CACHE = {};
const JS_CACHE = {}; const JS_CACHE = {};
class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) { class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
static get template() { static get template() {
return html` return html`
<style include='ha-style'> <style include='ha-style'>
:host { :host {
-ms-user-select: none; -ms-user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
} }
ha-app-layout { ha-app-layout {
min-height: 100%; min-height: 100%;
} }
paper-tabs { paper-tabs {
margin-left: 12px; margin-left: 12px;
--paper-tabs-selection-bar-color: var(--text-primary-color, #FFF); --paper-tabs-selection-bar-color: var(--text-primary-color, #FFF);
text-transform: uppercase; text-transform: uppercase;
} }
app-toolbar a { app-toolbar a {
color: var(--text-primary-color, white); color: var(--text-primary-color, white);
} }
#view { #view {
min-height: calc(100vh - 112px); min-height: calc(100vh - 112px);
/** /**
* Since we only set min-height, if child nodes need percentage * Since we only set min-height, if child nodes need percentage
* heights they must use absolute positioning so we need relative * heights they must use absolute positioning so we need relative
* positioning here. * positioning here.
* *
* https://www.w3.org/TR/CSS2/visudet.html#the-height-property * https://www.w3.org/TR/CSS2/visudet.html#the-height-property
*/ */
position: relative; position: relative;
} }
#view.tabs-hidden { #view.tabs-hidden {
min-height: calc(100vh - 64px); min-height: calc(100vh - 64px);
} }
paper-item { paper-item {
cursor: pointer; cursor: pointer;
} }
</style> </style>
<app-route route="[[route]]" pattern="/:view" data="{{routeData}}"></app-route> <app-route route="[[route]]" pattern="/:view" data="{{routeData}}"></app-route>
<hui-notification-drawer <hui-notification-drawer
hass="[[hass]]" hass="[[hass]]"
notifications="[[_notifications]]" notifications="[[_notifications]]"
open="{{notificationsOpen}}" open="{{notificationsOpen}}"
narrow="[[narrow]]" narrow="[[narrow]]"
></hui-notification-drawer> ></hui-notification-drawer>
<ha-app-layout id="layout"> <ha-app-layout id="layout">
<app-header slot="header" effects="waterfall" fixed condenses> <app-header slot="header" effects="waterfall" fixed condenses>
<template is='dom-if' if="[[!_editMode]]"> <template is='dom-if' if="[[!_editMode]]">
<app-toolbar> <app-toolbar>
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button> <ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
<div main-title>[[_computeTitle(config)]]</div> <div main-title>[[_computeTitle(config)]]</div>
<hui-notifications-button <hui-notifications-button
hass="[[hass]]" hass="[[hass]]"
notifications-open="{{notificationsOpen}}" notifications-open="{{notificationsOpen}}"
notifications="[[_notifications]]" notifications="[[_notifications]]"
></hui-notifications-button> ></hui-notifications-button>
<ha-start-voice-button hass="[[hass]]"></ha-start-voice-button> <ha-start-voice-button hass="[[hass]]"></ha-start-voice-button>
<paper-menu-button <paper-menu-button
no-animations no-animations
horizontal-align="right" horizontal-align="right"
horizontal-offset="-5" horizontal-offset="-5"
> >
<paper-icon-button icon="hass:dots-vertical" slot="dropdown-trigger"></paper-icon-button> <paper-icon-button icon="hass:dots-vertical" slot="dropdown-trigger"></paper-icon-button>
<paper-listbox on-iron-select="_deselect" slot="dropdown-content"> <paper-listbox on-iron-select="_deselect" slot="dropdown-content">
<paper-item on-click="_handleRefresh">Refresh</paper-item> <paper-item on-click="_handleRefresh">Refresh</paper-item>
<paper-item on-click="_handleUnusedEntities">Unused entities</paper-item> <paper-item on-click="_handleUnusedEntities">Unused entities</paper-item>
<paper-item on-click="_editModeEnable">Configure UI</paper-item> <paper-item on-click="_editModeEnable">Configure UI</paper-item>
<paper-item on-click="_handleHelp">Help</paper-item> <paper-item on-click="_handleHelp">Help</paper-item>
</paper-listbox> </paper-listbox>
</paper-menu-button> </paper-menu-button>
</app-toolbar> </app-toolbar>
</template> </template>
<template is='dom-if' if="[[_editMode]]"> <template is='dom-if' if="[[_editMode]]">
<app-toolbar> <app-toolbar>
<paper-icon-button <paper-icon-button
icon='hass:close' icon='hass:close'
on-click='_editModeDisable' on-click='_editModeDisable'
></paper-icon-button> ></paper-icon-button>
<div main-title>Edit UI</div> <div main-title>Edit UI</div>
</app-toolbar> </app-toolbar>
</template> </template>
<div sticky hidden$="[[_computeTabsHidden(config.views)]]"> <div sticky hidden$="[[_computeTabsHidden(config.views)]]">
<paper-tabs scrollable selected="[[_curView]]" on-iron-activate="_handleViewSelected"> <paper-tabs scrollable selected="[[_curView]]" on-iron-activate="_handleViewSelected">
<template is="dom-repeat" items="[[config.views]]"> <template is="dom-repeat" items="[[config.views]]">
<paper-tab> <paper-tab>
<template is="dom-if" if="[[item.icon]]"> <template is="dom-if" if="[[item.icon]]">
<ha-icon title$="[[item.title]]" icon="[[item.icon]]"></ha-icon> <ha-icon title$="[[item.title]]" icon="[[item.icon]]"></ha-icon>
</template> </template>
<template is="dom-if" if="[[!item.icon]]"> <template is="dom-if" if="[[!item.icon]]">
[[_computeTabTitle(item.title)]] [[_computeTabTitle(item.title)]]
</template> </template>
</paper-tab> </paper-tab>
</template> </template>
</paper-tabs> </paper-tabs>
</div> </div>
</app-header> </app-header>
<div id='view' on-rebuild-view='_debouncedConfigChanged'></div> <div id='view' on-rebuild-view='_debouncedConfigChanged'></div>
</app-header-layout> </app-header-layout>
`; `;
} }
static get properties() { static get properties() {
return { return {
narrow: Boolean, narrow: Boolean,
showMenu: Boolean, showMenu: Boolean,
hass: { hass: {
type: Object, type: Object,
observer: "_hassChanged", observer: "_hassChanged",
}, },
config: { config: {
type: Object, type: Object,
observer: "_configChanged", observer: "_configChanged",
}, },
columns: { columns: {
type: Number, type: Number,
observer: "_columnsChanged", observer: "_columnsChanged",
}, },
_curView: { _curView: {
type: Number, type: Number,
value: 0, value: 0,
}, },
route: { route: {
type: Object, type: Object,
observer: "_routeChanged", observer: "_routeChanged",
}, },
notificationsOpen: { notificationsOpen: {
type: Boolean, type: Boolean,
value: false, value: false,
}, },
_persistentNotifications: { _persistentNotifications: {
type: Array, type: Array,
value: [], value: [],
}, },
_notifications: { _notifications: {
type: Array, type: Array,
computed: "_updateNotifications(hass.states, _persistentNotifications)", computed: "_updateNotifications(hass.states, _persistentNotifications)",
}, },
_editMode: { _editMode: {
type: Boolean, type: Boolean,
value: false, value: false,
observer: "_editModeChanged", observer: "_editModeChanged",
}, },
routeData: Object, routeData: Object,
}; };
} }
constructor() { constructor() {
super(); super();
this._debouncedConfigChanged = debounce( this._debouncedConfigChanged = debounce(
() => this._selectView(this._curView), () => this._selectView(this._curView),
100 100
); );
} }
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this._unsubNotifications = subscribeNotifications( this._unsubNotifications = subscribeNotifications(
this.hass.connection, this.hass.connection,
(notifications) => { (notifications) => {
this._persistentNotifications = notifications; this._persistentNotifications = notifications;
} }
); );
} }
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
if (typeof this._unsubNotifications === "function") { if (typeof this._unsubNotifications === "function") {
this._unsubNotifications(); this._unsubNotifications();
} }
} }
_updateNotifications(states, persistent) { _updateNotifications(states, persistent) {
if (!states) return persistent; if (!states) return persistent;
const configurator = computeNotifications(states); const configurator = computeNotifications(states);
return persistent.concat(configurator); return persistent.concat(configurator);
} }
_routeChanged(route) { _routeChanged(route) {
const views = this.config && this.config.views; const views = this.config && this.config.views;
if (route.path === "" && route.prefix === "/lovelace" && views) { if (route.path === "" && route.prefix === "/lovelace" && views) {
this.navigate(`/lovelace/${views[0].id || 0}`, true); this.navigate(`/lovelace/${views[0].id || 0}`, true);
} else if (this.routeData.view) { } else if (this.routeData.view) {
const view = this.routeData.view; const view = this.routeData.view;
let index = 0; let index = 0;
for (let i = 0; i < views.length; i++) { for (let i = 0; i < views.length; i++) {
if (views[i].id === view || i === parseInt(view)) { if (views[i].id === view || i === parseInt(view)) {
index = i; index = i;
break; break;
} }
} }
if (index !== this._curView) this._selectView(index); if (index !== this._curView) this._selectView(index);
} }
} }
_computeViewId(id, index) { _computeViewId(id, index) {
return id || index; return id || index;
} }
_computeTitle(config) { _computeTitle(config) {
return config.title || "Home Assistant"; return config.title || "Home Assistant";
} }
_computeTabsHidden(views) { _computeTabsHidden(views) {
return views.length < 2; return views.length < 2;
} }
_computeTabTitle(title) { _computeTabTitle(title) {
return title || "Unnamed view"; return title || "Unnamed view";
} }
_handleRefresh() { _handleRefresh() {
this.fire("config-refresh"); this.fire("config-refresh");
} }
_handleUnusedEntities() { _handleUnusedEntities() {
this._selectView("unused"); this._selectView("unused");
} }
_deselect(ev) { _deselect(ev) {
ev.target.selected = null; ev.target.selected = null;
} }
_handleHelp() { _handleHelp() {
window.open("https://www.home-assistant.io/lovelace/", "_blank"); window.open("https://www.home-assistant.io/lovelace/", "_blank");
} }
_editModeEnable() { _editModeEnable() {
this._editMode = true; this._editMode = true;
} }
_editModeDisable() { _editModeDisable() {
this._editMode = false; this._editMode = false;
} }
_editModeChanged() { _editModeChanged() {
this._selectView(this._curView); this._selectView(this._curView);
} }
_handleViewSelected(ev) { _handleViewSelected(ev) {
const index = ev.detail.selected; const index = ev.detail.selected;
if (index !== this._curView) { if (index !== this._curView) {
const id = this.config.views[index].id || index; const id = this.config.views[index].id || index;
this.navigate(`/lovelace/${id}`); this.navigate(`/lovelace/${id}`);
} }
scrollToTarget(this, this.$.layout.header.scrollTarget); scrollToTarget(this, this.$.layout.header.scrollTarget);
} }
_selectView(viewIndex) { _selectView(viewIndex) {
this._curView = viewIndex; this._curView = viewIndex;
// Recreate a new element to clear the applied themes. // Recreate a new element to clear the applied themes.
const root = this.$.view; const root = this.$.view;
if (root.lastChild) { if (root.lastChild) {
root.removeChild(root.lastChild); root.removeChild(root.lastChild);
} }
let view; let view;
let background = this.config.background || ""; let background = this.config.background || "";
if (viewIndex === "unused") { if (viewIndex === "unused") {
view = document.createElement("hui-unused-entities"); view = document.createElement("hui-unused-entities");
view.config = this.config; view.config = this.config;
} else { } else {
const viewConfig = this.config.views[this._curView]; const viewConfig = this.config.views[this._curView];
if (viewConfig.panel) { if (viewConfig.panel) {
view = createCardElement(viewConfig.cards[0]); view = createCardElement(viewConfig.cards[0]);
view.isPanel = true; view.isPanel = true;
view.editMode = this._editMode; view.editMode = this._editMode;
} else { } else {
view = document.createElement("hui-view"); view = document.createElement("hui-view");
view.config = viewConfig; view.config = viewConfig;
view.columns = this.columns; view.columns = this.columns;
view.editMode = this._editMode; view.editMode = this._editMode;
} }
if (viewConfig.background) background = viewConfig.background; if (viewConfig.background) background = viewConfig.background;
} }
this.$.view.style.background = background; this.$.view.style.background = background;
view.hass = this.hass; view.hass = this.hass;
root.appendChild(view); root.appendChild(view);
} }
_hassChanged(hass) { _hassChanged(hass) {
if (!this.$.view.lastChild) return; if (!this.$.view.lastChild) return;
this.$.view.lastChild.hass = hass; this.$.view.lastChild.hass = hass;
} }
_configChanged(config) { _configChanged(config) {
this._loadResources(config.resources || []); this._loadResources(config.resources || []);
// On config change, recreate the view from scratch. // On config change, recreate the view from scratch.
this._selectView(this._curView); this._selectView(this._curView);
this.$.view.classList.toggle("tabs-hidden", config.views.length < 2); this.$.view.classList.toggle("tabs-hidden", config.views.length < 2);
} }
_columnsChanged(columns) { _columnsChanged(columns) {
if (!this.$.view.lastChild) return; if (!this.$.view.lastChild) return;
this.$.view.lastChild.columns = columns; this.$.view.lastChild.columns = columns;
} }
_loadResources(resources) { _loadResources(resources) {
resources.forEach((resource) => { resources.forEach((resource) => {
switch (resource.type) { switch (resource.type) {
case "css": case "css":
if (resource.url in CSS_CACHE) break; if (resource.url in CSS_CACHE) break;
CSS_CACHE[resource.url] = loadCSS(resource.url); CSS_CACHE[resource.url] = loadCSS(resource.url);
break; break;
case "js": case "js":
if (resource.url in JS_CACHE) break; if (resource.url in JS_CACHE) break;
JS_CACHE[resource.url] = loadJS(resource.url); JS_CACHE[resource.url] = loadJS(resource.url);
break; break;
case "module": case "module":
loadModule(resource.url); loadModule(resource.url);
break; break;
case "html": case "html":
import(/* webpackChunkName: "import-href-polyfill" */ "../../resources/html-import/import-href").then( import(/* webpackChunkName: "import-href-polyfill" */ "../../resources/html-import/import-href").then(
({ importHref }) => importHref(resource.url) ({ importHref }) => importHref(resource.url)
); );
break; break;
default: default:
// eslint-disable-next-line // eslint-disable-next-line
console.warn("Unknown resource type specified: ${resource.type}"); console.warn("Unknown resource type specified: ${resource.type}");
} }
}); });
} }
} }
customElements.define("hui-root", HUIRoot); customElements.define("hui-root", HUIRoot);

View File

@@ -1,61 +1,61 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import computeUnusedEntities from "./common/compute-unused-entities"; import computeUnusedEntities from "./common/compute-unused-entities";
import createCardElement from "./common/create-card-element"; import createCardElement from "./common/create-card-element";
import "./cards/hui-entities-card.ts"; import "./cards/hui-entities-card.ts";
class HuiUnusedEntities extends PolymerElement { class HuiUnusedEntities extends PolymerElement {
static get template() { static get template() {
return html` return html`
<style> <style>
#root { #root {
max-width: 600px; max-width: 600px;
margin: 0 auto; margin: 0 auto;
padding: 8px 0; padding: 8px 0;
} }
</style> </style>
<div id="root"></div> <div id="root"></div>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: { hass: {
type: Object, type: Object,
observer: "_hassChanged", observer: "_hassChanged",
}, },
config: { config: {
type: Object, type: Object,
observer: "_configChanged", observer: "_configChanged",
}, },
}; };
} }
_configChanged(config) { _configChanged(config) {
const root = this.$.root; const root = this.$.root;
if (root.lastChild) root.removeChild(root.lastChild); if (root.lastChild) root.removeChild(root.lastChild);
const entities = computeUnusedEntities(this.hass, config).map((entity) => ({ const entities = computeUnusedEntities(this.hass, config).map((entity) => ({
entity, entity,
secondary_info: "entity-id", secondary_info: "entity-id",
})); }));
const cardConfig = { const cardConfig = {
type: "entities", type: "entities",
title: "Unused entities", title: "Unused entities",
entities, entities,
show_header_toggle: false, show_header_toggle: false,
}; };
const element = createCardElement(cardConfig); const element = createCardElement(cardConfig);
element.hass = this.hass; element.hass = this.hass;
root.appendChild(element); root.appendChild(element);
} }
_hassChanged(hass) { _hassChanged(hass) {
const root = this.$.root; const root = this.$.root;
if (!root.lastChild) return; if (!root.lastChild) return;
root.lastChild.hass = hass; root.lastChild.hass = hass;
} }
} }
customElements.define("hui-unused-entities", HuiUnusedEntities); customElements.define("hui-unused-entities", HuiUnusedEntities);

View File

@@ -1,218 +1,218 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../components/entity/ha-state-label-badge"; import "../../components/entity/ha-state-label-badge";
import "./components/hui-card-options.ts"; import "./components/hui-card-options.ts";
import applyThemesOnElement from "../../common/dom/apply_themes_on_element"; import applyThemesOnElement from "../../common/dom/apply_themes_on_element";
import createCardElement from "./common/create-card-element"; import createCardElement from "./common/create-card-element";
import computeCardSize from "./common/compute-card-size"; import computeCardSize from "./common/compute-card-size";
class HUIView extends PolymerElement { class HUIView extends PolymerElement {
static get template() { static get template() {
return html` return html`
<style> <style>
:host { :host {
display: block; display: block;
padding: 4px 4px 0; padding: 4px 4px 0;
transform: translateZ(0); transform: translateZ(0);
position: relative; position: relative;
} }
#badges { #badges {
margin: 8px 16px; margin: 8px 16px;
font-size: 85%; font-size: 85%;
text-align: center; text-align: center;
} }
#columns { #columns {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
} }
.column { .column {
flex-basis: 0; flex-basis: 0;
flex-grow: 1; flex-grow: 1;
max-width: 500px; max-width: 500px;
overflow-x: hidden; overflow-x: hidden;
} }
.column > * { .column > * {
display: block; display: block;
margin: 4px 4px 8px; margin: 4px 4px 8px;
} }
@media (max-width: 500px) { @media (max-width: 500px) {
:host { :host {
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
} }
.column > * { .column > * {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
} }
} }
@media (max-width: 599px) { @media (max-width: 599px) {
.column { .column {
max-width: 600px; max-width: 600px;
} }
} }
</style> </style>
<div id="badges"></div> <div id="badges"></div>
<div id="columns"></div> <div id="columns"></div>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: { hass: {
type: Object, type: Object,
observer: "_hassChanged", observer: "_hassChanged",
}, },
config: Object, config: Object,
columns: Number, columns: Number,
editMode: Boolean, editMode: Boolean,
}; };
} }
static get observers() { static get observers() {
return [ return [
// Put all properties in 1 observer so we only call configChanged once // Put all properties in 1 observer so we only call configChanged once
"_createBadges(config)", "_createBadges(config)",
"_createCards(config, columns, editMode)", "_createCards(config, columns, editMode)",
]; ];
} }
constructor() { constructor() {
super(); super();
this._cards = []; this._cards = [];
this._badges = []; this._badges = [];
} }
_createBadges(config) { _createBadges(config) {
const root = this.$.badges; const root = this.$.badges;
while (root.lastChild) { while (root.lastChild) {
root.removeChild(root.lastChild); root.removeChild(root.lastChild);
} }
if (!config || !config.badges || !Array.isArray(config.badges)) { if (!config || !config.badges || !Array.isArray(config.badges)) {
root.style.display = "none"; root.style.display = "none";
this._badges = []; this._badges = [];
return; return;
} }
const elements = []; const elements = [];
for (const entityId of config.badges) { for (const entityId of config.badges) {
if (!(entityId in this.hass.states)) continue; if (!(entityId in this.hass.states)) continue;
const element = document.createElement("ha-state-label-badge"); const element = document.createElement("ha-state-label-badge");
element.setProperties({ element.setProperties({
hass: this.hass, hass: this.hass,
state: this.hass.states[entityId], state: this.hass.states[entityId],
}); });
elements.push({ element, entityId }); elements.push({ element, entityId });
root.appendChild(element); root.appendChild(element);
} }
this._badges = elements; this._badges = elements;
root.style.display = elements.length > 0 ? "block" : "none"; root.style.display = elements.length > 0 ? "block" : "none";
} }
_createCards(config) { _createCards(config) {
const root = this.$.columns; const root = this.$.columns;
while (root.lastChild) { while (root.lastChild) {
root.removeChild(root.lastChild); root.removeChild(root.lastChild);
} }
if (!config || !config.cards || !Array.isArray(config.cards)) { if (!config || !config.cards || !Array.isArray(config.cards)) {
this._cards = []; this._cards = [];
return; return;
} }
const elements = []; const elements = [];
const elementsToAppend = []; const elementsToAppend = [];
for (const cardConfig of config.cards) { for (const cardConfig of config.cards) {
const element = createCardElement(cardConfig); const element = createCardElement(cardConfig);
element.hass = this.hass; element.hass = this.hass;
elements.push(element); elements.push(element);
if (!this.editMode) { if (!this.editMode) {
elementsToAppend.push(element); elementsToAppend.push(element);
continue; continue;
} }
const wrapper = document.createElement("hui-card-options"); const wrapper = document.createElement("hui-card-options");
wrapper.hass = this.hass; wrapper.hass = this.hass;
wrapper.cardId = cardConfig.id; wrapper.cardId = cardConfig.id;
wrapper.editMode = this.editMode; wrapper.editMode = this.editMode;
wrapper.appendChild(element); wrapper.appendChild(element);
elementsToAppend.push(wrapper); elementsToAppend.push(wrapper);
} }
let columns = []; let columns = [];
const columnEntityCount = []; const columnEntityCount = [];
for (let i = 0; i < this.columns; i++) { for (let i = 0; i < this.columns; i++) {
columns.push([]); columns.push([]);
columnEntityCount.push(0); columnEntityCount.push(0);
} }
// Find column with < 5 entities, else column with lowest count // Find column with < 5 entities, else column with lowest count
function getColumnIndex(size) { function getColumnIndex(size) {
let minIndex = 0; let minIndex = 0;
for (let i = 0; i < columnEntityCount.length; i++) { for (let i = 0; i < columnEntityCount.length; i++) {
if (columnEntityCount[i] < 5) { if (columnEntityCount[i] < 5) {
minIndex = i; minIndex = i;
break; break;
} }
if (columnEntityCount[i] < columnEntityCount[minIndex]) { if (columnEntityCount[i] < columnEntityCount[minIndex]) {
minIndex = i; minIndex = i;
} }
} }
columnEntityCount[minIndex] += size; columnEntityCount[minIndex] += size;
return minIndex; return minIndex;
} }
elements.forEach((el, index) => { elements.forEach((el, index) => {
const cardSize = computeCardSize(el); const cardSize = computeCardSize(el);
// Element to append might be the wrapped card when we're editing. // Element to append might be the wrapped card when we're editing.
columns[getColumnIndex(cardSize)].push(elementsToAppend[index]); columns[getColumnIndex(cardSize)].push(elementsToAppend[index]);
}); });
// Remove empty columns // Remove empty columns
columns = columns.filter((val) => val.length > 0); columns = columns.filter((val) => val.length > 0);
columns.forEach((column) => { columns.forEach((column) => {
const columnEl = document.createElement("div"); const columnEl = document.createElement("div");
columnEl.classList.add("column"); columnEl.classList.add("column");
column.forEach((el) => columnEl.appendChild(el)); column.forEach((el) => columnEl.appendChild(el));
root.appendChild(columnEl); root.appendChild(columnEl);
}); });
this._cards = elements; this._cards = elements;
if ("theme" in config) { if ("theme" in config) {
applyThemesOnElement(root, this.hass.themes, config.theme); applyThemesOnElement(root, this.hass.themes, config.theme);
} }
} }
_hassChanged(hass) { _hassChanged(hass) {
this._badges.forEach((badge) => { this._badges.forEach((badge) => {
const { element, entityId } = badge; const { element, entityId } = badge;
element.setProperties({ element.setProperties({
hass, hass,
state: hass.states[entityId], state: hass.states[entityId],
}); });
}); });
this._cards.forEach((element) => { this._cards.forEach((element) => {
element.hass = hass; element.hass = hass;
}); });
} }
} }
customElements.define("hui-view", HUIView); customElements.define("hui-view", HUIView);

View File

@@ -1,93 +1,93 @@
import { html, LitElement } from "@polymer/lit-element"; import { html, LitElement } from "@polymer/lit-element";
import "@polymer/paper-button/paper-button"; import "@polymer/paper-button/paper-button";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import callService from "../common/call-service"; import callService from "../common/call-service";
import { EntityRow, CallServiceConfig } from "../entity-rows/types"; import { EntityRow, CallServiceConfig } from "../entity-rows/types";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
class HuiCallServiceRow extends LitElement implements EntityRow { class HuiCallServiceRow extends LitElement implements EntityRow {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: CallServiceConfig; private _config?: CallServiceConfig;
static get properties() { static get properties() {
return { return {
hass: {}, hass: {},
_config: {}, _config: {},
}; };
} }
public setConfig(config: CallServiceConfig): void { public setConfig(config: CallServiceConfig): void {
if (!config || !config.name || !config.service) { if (!config || !config.name || !config.service) {
throw new Error("Error in card configuration."); throw new Error("Error in card configuration.");
} }
this._config = { icon: "hass:remote", action_name: "Run", ...config }; this._config = { icon: "hass:remote", action_name: "Run", ...config };
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config) { if (!this._config) {
return html``; return html``;
} }
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<ha-icon .icon="${this._config.icon}"></ha-icon> <ha-icon .icon="${this._config.icon}"></ha-icon>
<div class="flex"> <div class="flex">
<div> <div>
${this._config.name} ${this._config.name}
</div> </div>
<paper-button <paper-button
@click="${this._callService}" @click="${this._callService}"
>${this._config.action_name}</paper-button> >${this._config.action_name}</paper-button>
</div> </div>
`; `;
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
<style> <style>
:host { :host {
display: flex; display: flex;
align-items: center; align-items: center;
} }
ha-icon { ha-icon {
padding: 8px; padding: 8px;
color: var(--paper-item-icon-color); color: var(--paper-item-icon-color);
} }
.flex { .flex {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
margin-left: 16px; margin-left: 16px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.flex div { .flex div {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
paper-button { paper-button {
color: var(--primary-color); color: var(--primary-color);
font-weight: 500; font-weight: 500;
margin-right: -.57em; margin-right: -.57em;
} }
</style> </style>
`; `;
} }
private _callService() { private _callService() {
callService(this._config, this.hass); callService(this._config, this.hass);
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-call-service-row": HuiCallServiceRow; "hui-call-service-row": HuiCallServiceRow;
} }
} }
customElements.define("hui-call-service-row", HuiCallServiceRow); customElements.define("hui-call-service-row", HuiCallServiceRow);

View File

@@ -1,53 +1,53 @@
import { html, LitElement } from "@polymer/lit-element"; import { html, LitElement } from "@polymer/lit-element";
import { EntityRow, DividerConfig } from "../entity-rows/types"; import { EntityRow, DividerConfig } from "../entity-rows/types";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
class HuiDividerRow extends LitElement implements EntityRow { class HuiDividerRow extends LitElement implements EntityRow {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: DividerConfig; private _config?: DividerConfig;
static get properties() { static get properties() {
return { return {
_config: {}, _config: {},
}; };
} }
public setConfig(config): void { public setConfig(config): void {
if (!config) { if (!config) {
throw new Error("Error in card configuration."); throw new Error("Error in card configuration.");
} }
this._config = { this._config = {
style: { style: {
height: "1px", height: "1px",
"background-color": "var(--secondary-text-color)", "background-color": "var(--secondary-text-color)",
}, },
...config, ...config,
}; };
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config) { if (!this._config) {
return html``; return html``;
} }
const el = document.createElement("div"); const el = document.createElement("div");
Object.keys(this._config.style).forEach((prop) => { Object.keys(this._config.style).forEach((prop) => {
el.style.setProperty(prop, this._config!.style[prop]); el.style.setProperty(prop, this._config!.style[prop]);
}); });
return html` return html`
${el} ${el}
`; `;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-divider-row": HuiDividerRow; "hui-divider-row": HuiDividerRow;
} }
} }
customElements.define("hui-divider-row", HuiDividerRow); customElements.define("hui-divider-row", HuiDividerRow);

View File

@@ -1,70 +1,70 @@
import { html, LitElement } from "@polymer/lit-element"; import { html, LitElement } from "@polymer/lit-element";
import { EntityRow, SectionConfig } from "../entity-rows/types"; import { EntityRow, SectionConfig } from "../entity-rows/types";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
class HuiSectionRow extends LitElement implements EntityRow { class HuiSectionRow extends LitElement implements EntityRow {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: SectionConfig; private _config?: SectionConfig;
static get properties() { static get properties() {
return { return {
_config: {}, _config: {},
}; };
} }
public setConfig(config: SectionConfig): void { public setConfig(config: SectionConfig): void {
if (!config) { if (!config) {
throw new Error("Error in card configuration."); throw new Error("Error in card configuration.");
} }
this._config = config; this._config = config;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config) { if (!this._config) {
return html``; return html``;
} }
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<div class=divider></div> <div class=divider></div>
${ ${
this._config.label this._config.label
? html`<div class="label">${this._config.label}</div>` ? html`<div class="label">${this._config.label}</div>`
: html`` : html``
} }
`; `;
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
<style> <style>
.label { .label {
color: var(--primary-color); color: var(--primary-color);
margin-left: 8px; margin-left: 8px;
margin-bottom: 16px; margin-bottom: 16px;
margin-top: 16px; margin-top: 16px;
} }
.divider { .divider {
height: 1px; height: 1px;
background-color: var(--secondary-text-color); background-color: var(--secondary-text-color);
opacity: 0.25; opacity: 0.25;
margin-left: -16px; margin-left: -16px;
margin-right: -16px; margin-right: -16px;
margin-top: 8px; margin-top: 8px;
} }
</style> </style>
`; `;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-section-row": HuiSectionRow; "hui-section-row": HuiSectionRow;
} }
} }
customElements.define("hui-section-row", HuiSectionRow); customElements.define("hui-section-row", HuiSectionRow);

View File

@@ -1,75 +1,75 @@
import { html, LitElement } from "@polymer/lit-element"; import { html, LitElement } from "@polymer/lit-element";
import { EntityRow, WeblinkConfig } from "../entity-rows/types"; import { EntityRow, WeblinkConfig } from "../entity-rows/types";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import { TemplateResult } from "lit-html"; import { TemplateResult } from "lit-html";
class HuiWeblinkRow extends LitElement implements EntityRow { class HuiWeblinkRow extends LitElement implements EntityRow {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: WeblinkConfig; private _config?: WeblinkConfig;
static get properties() { static get properties() {
return { return {
_config: {}, _config: {},
}; };
} }
public setConfig(config: WeblinkConfig): void { public setConfig(config: WeblinkConfig): void {
if (!config || !config.url) { if (!config || !config.url) {
throw new Error("Invalid Configuration: 'url' required"); throw new Error("Invalid Configuration: 'url' required");
} }
this._config = { this._config = {
icon: "hass:link", icon: "hass:link",
name: config.url, name: config.url,
...config, ...config,
}; };
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config) { if (!this._config) {
return html``; return html``;
} }
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<a href="${this._config.url}"> <a href="${this._config.url}">
<ha-icon .icon="${this._config.icon}"></ha-icon> <ha-icon .icon="${this._config.icon}"></ha-icon>
<div>${this._config.name}</div> <div>${this._config.name}</div>
</a> </a>
`; `;
} }
private renderStyle(): TemplateResult { private renderStyle(): TemplateResult {
return html` return html`
<style> <style>
a { a {
display: flex; display: flex;
align-items: center; align-items: center;
color: var(--primary-color); color: var(--primary-color);
} }
ha-icon { ha-icon {
padding: 8px; padding: 8px;
color: var(--paper-item-icon-color); color: var(--paper-item-icon-color);
} }
div { div {
flex: 1; flex: 1;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-left: 16px; margin-left: 16px;
} }
</style> </style>
`; `;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hui-weblink-row": HuiWeblinkRow; "hui-weblink-row": HuiWeblinkRow;
} }
} }
customElements.define("hui-weblink-row", HuiWeblinkRow); customElements.define("hui-weblink-row", HuiWeblinkRow);

View File

@@ -1,53 +1,53 @@
import assert from "assert"; import assert from "assert";
import parseAspectRatio from "../../../src/common/util/parse-aspect-ratio"; import parseAspectRatio from "../../../src/common/util/parse-aspect-ratio";
describe("parseAspectRatio", () => { describe("parseAspectRatio", () => {
const ratio16by9 = { w: 16, h: 9 }; const ratio16by9 = { w: 16, h: 9 };
const ratio178 = { w: 1.78, h: 1 }; const ratio178 = { w: 1.78, h: 1 };
it("Parses 16x9", () => { it("Parses 16x9", () => {
const r = parseAspectRatio("16x9"); const r = parseAspectRatio("16x9");
assert.deepEqual(r, ratio16by9); assert.deepEqual(r, ratio16by9);
}); });
it("Parses 16:9", () => { it("Parses 16:9", () => {
const r = parseAspectRatio("16:9"); const r = parseAspectRatio("16:9");
assert.deepEqual(r, ratio16by9); assert.deepEqual(r, ratio16by9);
}); });
it("Parses 1.78x1", () => { it("Parses 1.78x1", () => {
const r = parseAspectRatio("1.78x1"); const r = parseAspectRatio("1.78x1");
assert.deepEqual(r, ratio178); assert.deepEqual(r, ratio178);
}); });
it("Parses 1.78:1", () => { it("Parses 1.78:1", () => {
const r = parseAspectRatio("1.78:1"); const r = parseAspectRatio("1.78:1");
assert.deepEqual(r, ratio178); assert.deepEqual(r, ratio178);
}); });
it("Parses 1.78", () => { it("Parses 1.78", () => {
const r = parseAspectRatio("1.78"); const r = parseAspectRatio("1.78");
assert.deepEqual(r, ratio178); assert.deepEqual(r, ratio178);
}); });
it("Skips null states", () => { it("Skips null states", () => {
const r = parseAspectRatio(null); const r = parseAspectRatio(null);
assert.equal(r, null); assert.equal(r, null);
}); });
it("Skips empty states", () => { it("Skips empty states", () => {
const r = parseAspectRatio(" "); const r = parseAspectRatio(" ");
assert.equal(r, null); assert.equal(r, null);
}); });
it("Skips invalid input", () => { it("Skips invalid input", () => {
const r = parseAspectRatio("mary had a little lamb"); const r = parseAspectRatio("mary had a little lamb");
assert.equal(r, null); assert.equal(r, null);
}); });
it("Skips invalid, but close input", () => { it("Skips invalid, but close input", () => {
const r = parseAspectRatio("mary:lamb"); const r = parseAspectRatio("mary:lamb");
assert.equal(r, null); assert.equal(r, null);
}); });
}); });