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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,50 +1,50 @@
import { html } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import computeCardSize from "../common/compute-card-size";
import { HuiStackCard } from "./hui-stack-card";
class HuiHorizontalStackCard extends HuiStackCard {
public getCardSize(): number {
let totalSize = 0;
if (this._cards) {
for (const element of this._cards) {
const elementSize = computeCardSize(element);
totalSize = elementSize > totalSize ? elementSize : totalSize;
}
}
return totalSize;
}
protected renderStyle(): TemplateResult {
return html`
<style>
#root {
display: flex;
}
#root > * {
flex: 1 1 0;
margin: 0 4px;
min-width: 0;
}
#root > *:first-child {
margin-left: 0;
}
#root > *:last-child {
margin-right: 0;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-horitzontal-stack-card": HuiHorizontalStackCard;
}
}
customElements.define("hui-horizontal-stack-card", HuiHorizontalStackCard);
import { html } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import computeCardSize from "../common/compute-card-size";
import { HuiStackCard } from "./hui-stack-card";
class HuiHorizontalStackCard extends HuiStackCard {
public getCardSize(): number {
let totalSize = 0;
if (this._cards) {
for (const element of this._cards) {
const elementSize = computeCardSize(element);
totalSize = elementSize > totalSize ? elementSize : totalSize;
}
}
return totalSize;
}
protected renderStyle(): TemplateResult {
return html`
<style>
#root {
display: flex;
}
#root > * {
flex: 1 1 0;
margin: 0 4px;
min-width: 0;
}
#root > *:first-child {
margin-left: 0;
}
#root > *:last-child {
margin-right: 0;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-horitzontal-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 "../../../components/ha-card";
import { LovelaceCard, LovelaceConfig } from "../types";
import { TemplateResult } from "lit-html";
interface Config extends LovelaceConfig {
aspect_ratio?: string;
title?: string;
url: string;
}
export class HuiIframeCard extends LitElement implements LovelaceCard {
protected _config?: Config;
static get properties(): PropertyDeclarations {
return {
_config: {},
};
}
public getCardSize(): number {
return 1 + this.offsetHeight / 50;
}
public setConfig(config: Config): void {
if (!config.url) {
throw new Error("URL required");
}
this._config = config;
}
protected render(): TemplateResult {
if (!this._config) {
return html``;
}
return html`
${this.renderStyle()}
<ha-card .header="${this._config.title}">
<div id="root">
<iframe src="${this._config.url}"></iframe>
</div>
</ha-card>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-card {
overflow: hidden;
}
#root {
width: 100%;
position: relative;
padding-top: ${this._config!.aspect_ratio || "50%"};
}
iframe {
position: absolute;
border: none;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-iframe-card": HuiIframeCard;
}
}
customElements.define("hui-iframe-card", HuiIframeCard);
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "../../../components/ha-card";
import { LovelaceCard, LovelaceConfig } from "../types";
import { TemplateResult } from "lit-html";
interface Config extends LovelaceConfig {
aspect_ratio?: string;
title?: string;
url: string;
}
export class HuiIframeCard extends LitElement implements LovelaceCard {
protected _config?: Config;
static get properties(): PropertyDeclarations {
return {
_config: {},
};
}
public getCardSize(): number {
return 1 + this.offsetHeight / 50;
}
public setConfig(config: Config): void {
if (!config.url) {
throw new Error("URL required");
}
this._config = config;
}
protected render(): TemplateResult {
if (!this._config) {
return html``;
}
return html`
${this.renderStyle()}
<ha-card .header="${this._config.title}">
<div id="root">
<iframe src="${this._config.url}"></iframe>
</div>
</ha-card>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-card {
overflow: hidden;
}
#root {
width: 100%;
position: relative;
padding-top: ${this._config!.aspect_ratio || "50%"};
}
iframe {
position: absolute;
border: none;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"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 computeDomain from "../../../common/entity/compute_domain";
export default class LegacyWrapperCard extends HTMLElement {
constructor(tag, domain) {
super();
this._tag = tag.toUpperCase();
this._domain = domain;
this._element = null;
}
getCardSize() {
return 3;
}
setConfig(config) {
if (!config.entity) {
throw new Error("No entity specified");
}
if (computeDomain(config.entity) !== this._domain) {
throw new Error(
`Specified entity needs to be of domain ${this._domain}.`
);
}
this._config = config;
}
set hass(hass) {
const entityId = this._config.entity;
if (entityId in hass.states) {
this._ensureElement(this._tag);
this.lastChild.hass = hass;
this.lastChild.stateObj = hass.states[entityId];
} else {
this._ensureElement("HUI-ERROR-CARD");
this.lastChild.setConfig(
createErrorCardConfig(
`No state available for ${entityId}`,
this._config
)
);
}
}
_ensureElement(tag) {
if (this.lastChild && this.lastChild.tagName === tag) return;
if (this.lastChild) {
this.removeChild(this.lastChild);
}
this.appendChild(document.createElement(tag));
}
}
import createErrorCardConfig from "../common/create-error-card-config";
import computeDomain from "../../../common/entity/compute_domain";
export default class LegacyWrapperCard extends HTMLElement {
constructor(tag, domain) {
super();
this._tag = tag.toUpperCase();
this._domain = domain;
this._element = null;
}
getCardSize() {
return 3;
}
setConfig(config) {
if (!config.entity) {
throw new Error("No entity specified");
}
if (computeDomain(config.entity) !== this._domain) {
throw new Error(
`Specified entity needs to be of domain ${this._domain}.`
);
}
this._config = config;
}
set hass(hass) {
const entityId = this._config.entity;
if (entityId in hass.states) {
this._ensureElement(this._tag);
this.lastChild.hass = hass;
this.lastChild.stateObj = hass.states[entityId];
} else {
this._ensureElement("HUI-ERROR-CARD");
this.lastChild.setConfig(
createErrorCardConfig(
`No state available for ${entityId}`,
this._config
)
);
}
}
_ensureElement(tag) {
if (this.lastChild && this.lastChild.tagName === tag) return;
if (this.lastChild) {
this.removeChild(this.lastChild);
}
this.appendChild(document.createElement(tag));
}
}

View File

@ -1,327 +1,327 @@
import {
html,
LitElement,
PropertyValues,
PropertyDeclarations,
} from "@polymer/lit-element";
import { fireEvent } from "../../../common/dom/fire_event";
import { styleMap } from "lit-html/directives/styleMap";
import computeStateName from "../../../common/entity/compute_state_name";
import stateIcon from "../../../common/entity/state_icon";
import { jQuery } from "../../../resources/jquery";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
import { HomeAssistant, LightEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard, LovelaceConfig } from "../types";
import { longPress } from "../common/directives/long-press-directive";
import { TemplateResult } from "lit-html";
const lightConfig = {
radius: 80,
step: 1,
circleShape: "pie",
startAngle: 315,
width: 5,
min: 1,
max: 100,
sliderType: "min-range",
lineCap: "round",
handleSize: "+12",
showTooltip: false,
};
interface Config extends LovelaceConfig {
entity: string;
name?: string;
}
export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public hass?: HomeAssistant;
private _config?: Config;
private _brightnessTimout?: number;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
};
}
public getCardSize(): number {
return 2;
}
public setConfig(config: Config): void {
if (!config.entity || config.entity.split(".")[0] !== "light") {
throw new Error("Specify an entity from within the light domain.");
}
this._config = config;
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
const stateObj = this.hass.states[this._config!.entity] as LightEntity;
return html`
${this.renderStyle()}
<ha-card>
${
!stateObj
? html`
<div class="not-found">Entity not available: ${
this._config.entity
}</div>`
: html`
<div id="light"></div>
<div id="tooltip">
<div class="icon-state">
<ha-icon
data-state="${stateObj.state}"
.icon="${stateIcon(stateObj)}"
style="${styleMap({
filter: this._computeBrightness(stateObj),
color: this._computeColor(stateObj),
})}"
@ha-click="${() => this._handleClick(false)}"
@ha-hold="${() => this._handleClick(true)}"
.longPress="${longPress()}"
></ha-icon>
<div
class="brightness"
@ha-click="${() => this._handleClick(false)}"
@ha-hold="${() => this._handleClick(true)}"
.longPress="${longPress()}"
></div>
<div class="name">${this._config.name ||
computeStateName(stateObj)}</div>
</div>
</div>
`
}
</ha-card>
`;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.get("hass")) {
return (
(changedProps.get("hass") as any).states[this._config!.entity] !==
this.hass!.states[this._config!.entity]
);
}
return (changedProps as unknown) as boolean;
}
protected firstUpdated(): void {
const brightness = this.hass!.states[this._config!.entity].attributes
.brightness;
jQuery("#light", this.shadowRoot).roundSlider({
...lightConfig,
change: (value) => this._setBrightness(value),
drag: (value) => this._dragEvent(value),
start: () => this._showBrightness(),
stop: () => this._hideBrightness(),
});
this.shadowRoot!.querySelector(".brightness")!.innerHTML =
(Math.round((brightness / 254) * 100) || 0) + "%";
}
protected updated(): void {
const attrs = this.hass!.states[this._config!.entity].attributes;
jQuery("#light", this.shadowRoot).roundSlider({
value: Math.round((attrs.brightness / 254) * 100) || 0,
});
}
private renderStyle(): TemplateResult {
return html`
${roundSliderStyle}
<style>
:host {
display: block;
}
ha-card {
position: relative;
overflow: hidden;
--brightness-font-color: white;
--brightness-font-text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;
--name-font-size: 1.2rem;
--brightness-font-size: 1.2rem;
--rail-border-color: transparent;
}
#tooltip {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100%;
text-align: center;
z-index: 15;
}
.icon-state {
display: block;
margin: auto;
width: 100%;
height: 100%;
transform: translate(0,25%);
}
#light {
margin: 0 auto;
padding-top: 16px;
padding-bottom: 16px;
}
#light .rs-bar.rs-transition.rs-first, .rs-bar.rs-transition.rs-second{
z-index: 20 !important;
}
#light .rs-range-color {
background-color: var(--primary-color);
}
#light .rs-path-color {
background-color: var(--disabled-text-color);
}
#light .rs-handle {
background-color: var(--paper-card-background-color, white);
padding: 7px;
border: 2px solid var(--disabled-text-color);
}
#light .rs-handle.rs-focus {
border-color:var(--primary-color);
}
#light .rs-handle:after {
border-color: var(--primary-color);
background-color: var(--primary-color);
}
#light .rs-border {
border-color: var(--rail-border-color);
}
#light .rs-inner.rs-bg-color.rs-border,
#light .rs-overlay.rs-transition.rs-bg-color {
background-color: var(--paper-card-background-color, white);
}
ha-icon {
margin: auto;
width: 76px;
height: 76px;
color: var(--paper-item-icon-color, #44739e);
cursor: pointer;
}
ha-icon[data-state=on] {
color: var(--paper-item-icon-active-color, #FDD835);
}
ha-icon[data-state=unavailable] {
color: var(--state-icon-unavailable-color);
}
.name {
padding-top: 40px;
font-size: var(--name-font-size);
}
.brightness {
font-size: var(--brightness-font-size);
position: absolute;
margin: 0 auto;
left: 50%;
top: 10%;
transform: translate(-50%);
opacity: 0;
transition: opacity .5s ease-in-out;
-moz-transition: opacity .5s ease-in-out;
-webkit-transition: opacity .5s ease-in-out;
cursor: pointer;
color: var(--brightness-font-color);
text-shadow: var(--brightness-font-text-shadow)
}
.show_brightness {
opacity: 1;
}
.not-found {
flex: 1;
background-color: yellow;
padding: 8px;
}
</style>
`;
}
private _dragEvent(e: any): void {
this.shadowRoot!.querySelector(".brightness")!.innerHTML = e.value + "%";
}
private _showBrightness(): void {
clearTimeout(this._brightnessTimout);
this.shadowRoot!.querySelector(".brightness")!.classList.add(
"show_brightness"
);
}
private _hideBrightness(): void {
this._brightnessTimout = window.setTimeout(() => {
this.shadowRoot!.querySelector(".brightness")!.classList.remove(
"show_brightness"
);
}, 500);
}
private _setBrightness(e: any): void {
this.hass!.callService("light", "turn_on", {
entity_id: this._config!.entity,
brightness_pct: e.value,
});
}
private _computeBrightness(stateObj: LightEntity): string {
if (!stateObj.attributes.brightness) {
return "";
}
const brightness = stateObj.attributes.brightness;
return `brightness(${(brightness + 245) / 5}%)`;
}
private _computeColor(stateObj: LightEntity): string {
if (!stateObj.attributes.hs_color) {
return "";
}
const [hue, sat] = stateObj.attributes.hs_color;
if (sat <= 10) {
return "";
}
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
}
private _handleClick(hold: boolean): void {
const entityId = this._config!.entity;
if (hold) {
fireEvent(this, "hass-more-info", {
entityId,
});
return;
}
this.hass!.callService("light", "toggle", {
entity_id: entityId,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-light-card": HuiLightCard;
}
}
customElements.define("hui-light-card", HuiLightCard);
import {
html,
LitElement,
PropertyValues,
PropertyDeclarations,
} from "@polymer/lit-element";
import { fireEvent } from "../../../common/dom/fire_event";
import { styleMap } from "lit-html/directives/styleMap";
import computeStateName from "../../../common/entity/compute_state_name";
import stateIcon from "../../../common/entity/state_icon";
import { jQuery } from "../../../resources/jquery";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
import { HomeAssistant, LightEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard, LovelaceConfig } from "../types";
import { longPress } from "../common/directives/long-press-directive";
import { TemplateResult } from "lit-html";
const lightConfig = {
radius: 80,
step: 1,
circleShape: "pie",
startAngle: 315,
width: 5,
min: 1,
max: 100,
sliderType: "min-range",
lineCap: "round",
handleSize: "+12",
showTooltip: false,
};
interface Config extends LovelaceConfig {
entity: string;
name?: string;
}
export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public hass?: HomeAssistant;
private _config?: Config;
private _brightnessTimout?: number;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
};
}
public getCardSize(): number {
return 2;
}
public setConfig(config: Config): void {
if (!config.entity || config.entity.split(".")[0] !== "light") {
throw new Error("Specify an entity from within the light domain.");
}
this._config = config;
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
const stateObj = this.hass.states[this._config!.entity] as LightEntity;
return html`
${this.renderStyle()}
<ha-card>
${
!stateObj
? html`
<div class="not-found">Entity not available: ${
this._config.entity
}</div>`
: html`
<div id="light"></div>
<div id="tooltip">
<div class="icon-state">
<ha-icon
data-state="${stateObj.state}"
.icon="${stateIcon(stateObj)}"
style="${styleMap({
filter: this._computeBrightness(stateObj),
color: this._computeColor(stateObj),
})}"
@ha-click="${() => this._handleClick(false)}"
@ha-hold="${() => this._handleClick(true)}"
.longPress="${longPress()}"
></ha-icon>
<div
class="brightness"
@ha-click="${() => this._handleClick(false)}"
@ha-hold="${() => this._handleClick(true)}"
.longPress="${longPress()}"
></div>
<div class="name">${this._config.name ||
computeStateName(stateObj)}</div>
</div>
</div>
`
}
</ha-card>
`;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.get("hass")) {
return (
(changedProps.get("hass") as any).states[this._config!.entity] !==
this.hass!.states[this._config!.entity]
);
}
return (changedProps as unknown) as boolean;
}
protected firstUpdated(): void {
const brightness = this.hass!.states[this._config!.entity].attributes
.brightness;
jQuery("#light", this.shadowRoot).roundSlider({
...lightConfig,
change: (value) => this._setBrightness(value),
drag: (value) => this._dragEvent(value),
start: () => this._showBrightness(),
stop: () => this._hideBrightness(),
});
this.shadowRoot!.querySelector(".brightness")!.innerHTML =
(Math.round((brightness / 254) * 100) || 0) + "%";
}
protected updated(): void {
const attrs = this.hass!.states[this._config!.entity].attributes;
jQuery("#light", this.shadowRoot).roundSlider({
value: Math.round((attrs.brightness / 254) * 100) || 0,
});
}
private renderStyle(): TemplateResult {
return html`
${roundSliderStyle}
<style>
:host {
display: block;
}
ha-card {
position: relative;
overflow: hidden;
--brightness-font-color: white;
--brightness-font-text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;
--name-font-size: 1.2rem;
--brightness-font-size: 1.2rem;
--rail-border-color: transparent;
}
#tooltip {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100%;
text-align: center;
z-index: 15;
}
.icon-state {
display: block;
margin: auto;
width: 100%;
height: 100%;
transform: translate(0,25%);
}
#light {
margin: 0 auto;
padding-top: 16px;
padding-bottom: 16px;
}
#light .rs-bar.rs-transition.rs-first, .rs-bar.rs-transition.rs-second{
z-index: 20 !important;
}
#light .rs-range-color {
background-color: var(--primary-color);
}
#light .rs-path-color {
background-color: var(--disabled-text-color);
}
#light .rs-handle {
background-color: var(--paper-card-background-color, white);
padding: 7px;
border: 2px solid var(--disabled-text-color);
}
#light .rs-handle.rs-focus {
border-color:var(--primary-color);
}
#light .rs-handle:after {
border-color: var(--primary-color);
background-color: var(--primary-color);
}
#light .rs-border {
border-color: var(--rail-border-color);
}
#light .rs-inner.rs-bg-color.rs-border,
#light .rs-overlay.rs-transition.rs-bg-color {
background-color: var(--paper-card-background-color, white);
}
ha-icon {
margin: auto;
width: 76px;
height: 76px;
color: var(--paper-item-icon-color, #44739e);
cursor: pointer;
}
ha-icon[data-state=on] {
color: var(--paper-item-icon-active-color, #FDD835);
}
ha-icon[data-state=unavailable] {
color: var(--state-icon-unavailable-color);
}
.name {
padding-top: 40px;
font-size: var(--name-font-size);
}
.brightness {
font-size: var(--brightness-font-size);
position: absolute;
margin: 0 auto;
left: 50%;
top: 10%;
transform: translate(-50%);
opacity: 0;
transition: opacity .5s ease-in-out;
-moz-transition: opacity .5s ease-in-out;
-webkit-transition: opacity .5s ease-in-out;
cursor: pointer;
color: var(--brightness-font-color);
text-shadow: var(--brightness-font-text-shadow)
}
.show_brightness {
opacity: 1;
}
.not-found {
flex: 1;
background-color: yellow;
padding: 8px;
}
</style>
`;
}
private _dragEvent(e: any): void {
this.shadowRoot!.querySelector(".brightness")!.innerHTML = e.value + "%";
}
private _showBrightness(): void {
clearTimeout(this._brightnessTimout);
this.shadowRoot!.querySelector(".brightness")!.classList.add(
"show_brightness"
);
}
private _hideBrightness(): void {
this._brightnessTimout = window.setTimeout(() => {
this.shadowRoot!.querySelector(".brightness")!.classList.remove(
"show_brightness"
);
}, 500);
}
private _setBrightness(e: any): void {
this.hass!.callService("light", "turn_on", {
entity_id: this._config!.entity,
brightness_pct: e.value,
});
}
private _computeBrightness(stateObj: LightEntity): string {
if (!stateObj.attributes.brightness) {
return "";
}
const brightness = stateObj.attributes.brightness;
return `brightness(${(brightness + 245) / 5}%)`;
}
private _computeColor(stateObj: LightEntity): string {
if (!stateObj.attributes.hs_color) {
return "";
}
const [hue, sat] = stateObj.attributes.hs_color;
if (sat <= 10) {
return "";
}
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
}
private _handleClick(hold: boolean): void {
const entityId = this._config!.entity;
if (hold) {
fireEvent(this, "hass-more-info", {
entityId,
});
return;
}
this.hass!.callService("light", "toggle", {
entity_id: entityId,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"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 { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-icon-button/paper-icon-button";
import Leaflet from "leaflet";
import "../../map/ha-entity-marker";
import setupLeafletMap from "../../../common/dom/setup-leaflet-map";
import processConfigEntities from "../common/process-config-entities";
import computeStateDomain from "../../../common/entity/compute_state_domain";
import computeStateName from "../../../common/entity/compute_state_name";
import debounce from "../../../common/util/debounce";
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
class HuiMapCard extends PolymerElement {
static get template() {
return html`
<style>
:host([is-panel]) ha-card {
left: 0;
top: 0;
width: 100%;
/**
* In panel mode we want a full height map. Since parent #view
* only sets min-height, we need absolute positioning here
*/
height: 100%;
position: absolute;
}
ha-card {
overflow: hidden;
}
#map {
z-index: 0;
border: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
paper-icon-button {
position: absolute;
top: 75px;
left: 7px;
}
#root {
position: relative;
}
:host([is-panel]) #root {
height: 100%;
}
</style>
<ha-card id="card" header="[[_config.title]]">
<div id="root">
<div id="map"></div>
<paper-icon-button
on-click="_fitMap"
icon="hass:image-filter-center-focus"
title="Reset focus"
></paper-icon-button>
</div>
</ha-card>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: "_drawEntities",
},
_config: Object,
isPanel: {
type: Boolean,
reflectToAttribute: true,
},
};
}
constructor() {
super();
this._debouncedResizeListener = debounce(this._resetMap.bind(this), 100);
}
ready() {
super.ready();
if (!this._config || this.isPanel) {
return;
}
this.$.root.style.paddingTop = this._config.aspect_ratio || "100%";
}
setConfig(config) {
if (!config) {
throw new Error("Error in card configuration.");
}
this._configEntities = processConfigEntities(config.entities);
this._config = config;
}
getCardSize() {
let ar = this._config.aspect_ratio || "100%";
ar = ar.substr(0, ar.length - 1);
return 1 + Math.floor(ar / 25) || 3;
}
connectedCallback() {
super.connectedCallback();
// Observe changes to map size and invalidate to prevent broken rendering
// Uses ResizeObserver in Chrome, otherwise window resize event
if (typeof ResizeObserver === "function") {
this._resizeObserver = new ResizeObserver(() =>
this._debouncedResizeListener()
);
this._resizeObserver.observe(this.$.map);
} else {
window.addEventListener("resize", this._debouncedResizeListener);
}
this._map = setupLeafletMap(this.$.map);
this._drawEntities(this.hass);
setTimeout(() => {
this._resetMap();
this._fitMap();
}, 1);
}
disconnectedCallback() {
super.disconnectedCallback();
if (this._map) {
this._map.remove();
}
if (this._resizeObserver) {
this._resizeObserver.unobserve(this.$.map);
} else {
window.removeEventListener("resize", this._debouncedResizeListener);
}
}
_resetMap() {
if (!this._map) {
return;
}
this._map.invalidateSize();
}
_fitMap() {
const zoom = this._config.default_zoom;
if (this._mapItems.length === 0) {
this._map.setView(
new Leaflet.LatLng(
this.hass.config.latitude,
this.hass.config.longitude
),
zoom || 14
);
return;
}
const bounds = new Leaflet.latLngBounds(
this._mapItems.map((item) => item.getLatLng())
);
this._map.fitBounds(bounds.pad(0.5));
if (zoom && this._map.getZoom() > zoom) {
this._map.setZoom(zoom);
}
}
_drawEntities(hass) {
const map = this._map;
if (!map) {
return;
}
if (this._mapItems) {
this._mapItems.forEach((marker) => marker.remove());
}
const mapItems = (this._mapItems = []);
this._configEntities.forEach((entity) => {
const entityId = entity.entity;
if (!(entityId in hass.states)) {
return;
}
const stateObj = hass.states[entityId];
const title = computeStateName(stateObj);
const {
latitude,
longitude,
passive,
icon,
radius,
entity_picture: entityPicture,
gps_accuracy: gpsAccuracy,
} = stateObj.attributes;
if (!(latitude && longitude)) {
return;
}
let markerIcon;
let iconHTML;
let el;
if (computeStateDomain(stateObj) === "zone") {
// DRAW ZONE
if (passive) return;
// create icon
if (icon) {
el = document.createElement("ha-icon");
el.setAttribute("icon", icon);
iconHTML = el.outerHTML;
} else {
iconHTML = title;
}
markerIcon = Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className: "",
});
// create market with the icon
mapItems.push(
Leaflet.marker([latitude, longitude], {
icon: markerIcon,
interactive: false,
title: title,
}).addTo(map)
);
// create circle around it
mapItems.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: "#FF9800",
radius: radius,
}).addTo(map)
);
return;
}
// DRAW ENTITY
// create icon
const entityName = title
.split(" ")
.map((part) => part[0])
.join("")
.substr(0, 3);
el = document.createElement("ha-entity-marker");
el.setAttribute("entity-id", entityId);
el.setAttribute("entity-name", entityName);
el.setAttribute("entity-picture", entityPicture || "");
/* 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. */
markerIcon = Leaflet.divIcon({
html: el.outerHTML,
iconSize: [48, 48],
className: "",
});
// create market with the icon
mapItems.push(
Leaflet.marker([latitude, longitude], {
icon: markerIcon,
title: computeStateName(stateObj),
}).addTo(map)
);
// create circle around if entity has accuracy
if (gpsAccuracy) {
mapItems.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: "#0288D1",
radius: gpsAccuracy,
}).addTo(map)
);
}
});
}
}
customElements.define("hui-map-card", HuiMapCard);
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-icon-button/paper-icon-button";
import Leaflet from "leaflet";
import "../../map/ha-entity-marker";
import setupLeafletMap from "../../../common/dom/setup-leaflet-map";
import processConfigEntities from "../common/process-config-entities";
import computeStateDomain from "../../../common/entity/compute_state_domain";
import computeStateName from "../../../common/entity/compute_state_name";
import debounce from "../../../common/util/debounce";
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
class HuiMapCard extends PolymerElement {
static get template() {
return html`
<style>
:host([is-panel]) ha-card {
left: 0;
top: 0;
width: 100%;
/**
* In panel mode we want a full height map. Since parent #view
* only sets min-height, we need absolute positioning here
*/
height: 100%;
position: absolute;
}
ha-card {
overflow: hidden;
}
#map {
z-index: 0;
border: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
paper-icon-button {
position: absolute;
top: 75px;
left: 7px;
}
#root {
position: relative;
}
:host([is-panel]) #root {
height: 100%;
}
</style>
<ha-card id="card" header="[[_config.title]]">
<div id="root">
<div id="map"></div>
<paper-icon-button
on-click="_fitMap"
icon="hass:image-filter-center-focus"
title="Reset focus"
></paper-icon-button>
</div>
</ha-card>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: "_drawEntities",
},
_config: Object,
isPanel: {
type: Boolean,
reflectToAttribute: true,
},
};
}
constructor() {
super();
this._debouncedResizeListener = debounce(this._resetMap.bind(this), 100);
}
ready() {
super.ready();
if (!this._config || this.isPanel) {
return;
}
this.$.root.style.paddingTop = this._config.aspect_ratio || "100%";
}
setConfig(config) {
if (!config) {
throw new Error("Error in card configuration.");
}
this._configEntities = processConfigEntities(config.entities);
this._config = config;
}
getCardSize() {
let ar = this._config.aspect_ratio || "100%";
ar = ar.substr(0, ar.length - 1);
return 1 + Math.floor(ar / 25) || 3;
}
connectedCallback() {
super.connectedCallback();
// Observe changes to map size and invalidate to prevent broken rendering
// Uses ResizeObserver in Chrome, otherwise window resize event
if (typeof ResizeObserver === "function") {
this._resizeObserver = new ResizeObserver(() =>
this._debouncedResizeListener()
);
this._resizeObserver.observe(this.$.map);
} else {
window.addEventListener("resize", this._debouncedResizeListener);
}
this._map = setupLeafletMap(this.$.map);
this._drawEntities(this.hass);
setTimeout(() => {
this._resetMap();
this._fitMap();
}, 1);
}
disconnectedCallback() {
super.disconnectedCallback();
if (this._map) {
this._map.remove();
}
if (this._resizeObserver) {
this._resizeObserver.unobserve(this.$.map);
} else {
window.removeEventListener("resize", this._debouncedResizeListener);
}
}
_resetMap() {
if (!this._map) {
return;
}
this._map.invalidateSize();
}
_fitMap() {
const zoom = this._config.default_zoom;
if (this._mapItems.length === 0) {
this._map.setView(
new Leaflet.LatLng(
this.hass.config.latitude,
this.hass.config.longitude
),
zoom || 14
);
return;
}
const bounds = new Leaflet.latLngBounds(
this._mapItems.map((item) => item.getLatLng())
);
this._map.fitBounds(bounds.pad(0.5));
if (zoom && this._map.getZoom() > zoom) {
this._map.setZoom(zoom);
}
}
_drawEntities(hass) {
const map = this._map;
if (!map) {
return;
}
if (this._mapItems) {
this._mapItems.forEach((marker) => marker.remove());
}
const mapItems = (this._mapItems = []);
this._configEntities.forEach((entity) => {
const entityId = entity.entity;
if (!(entityId in hass.states)) {
return;
}
const stateObj = hass.states[entityId];
const title = computeStateName(stateObj);
const {
latitude,
longitude,
passive,
icon,
radius,
entity_picture: entityPicture,
gps_accuracy: gpsAccuracy,
} = stateObj.attributes;
if (!(latitude && longitude)) {
return;
}
let markerIcon;
let iconHTML;
let el;
if (computeStateDomain(stateObj) === "zone") {
// DRAW ZONE
if (passive) return;
// create icon
if (icon) {
el = document.createElement("ha-icon");
el.setAttribute("icon", icon);
iconHTML = el.outerHTML;
} else {
iconHTML = title;
}
markerIcon = Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className: "",
});
// create market with the icon
mapItems.push(
Leaflet.marker([latitude, longitude], {
icon: markerIcon,
interactive: false,
title: title,
}).addTo(map)
);
// create circle around it
mapItems.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: "#FF9800",
radius: radius,
}).addTo(map)
);
return;
}
// DRAW ENTITY
// create icon
const entityName = title
.split(" ")
.map((part) => part[0])
.join("")
.substr(0, 3);
el = document.createElement("ha-entity-marker");
el.setAttribute("entity-id", entityId);
el.setAttribute("entity-name", entityName);
el.setAttribute("entity-picture", entityPicture || "");
/* 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. */
markerIcon = Leaflet.divIcon({
html: el.outerHTML,
iconSize: [48, 48],
className: "",
});
// create market with the icon
mapItems.push(
Leaflet.marker([latitude, longitude], {
icon: markerIcon,
title: computeStateName(stateObj),
}).addTo(map)
);
// create circle around if entity has accuracy
if (gpsAccuracy) {
mapItems.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: "#0288D1",
radius: gpsAccuracy,
}).addTo(map)
);
}
});
}
}
customElements.define("hui-map-card", HuiMapCard);

View File

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

View File

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

View File

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

View File

@ -1,111 +1,111 @@
import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import createHuiElement from "../common/create-hui-element";
import { LovelaceCard, LovelaceConfig } from "../types";
import { HomeAssistant } from "../../../types";
import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
interface Config extends LovelaceConfig {
title?: string;
image: string;
elements: LovelaceElementConfig[];
}
class HuiPictureElementsCard extends LitElement implements LovelaceCard {
private _config?: Config;
private _hass?: HomeAssistant;
static get properties() {
return {
_config: {},
};
}
set hass(hass: HomeAssistant) {
this._hass = hass;
for (const el of this.shadowRoot!.querySelectorAll("#root > *")) {
const element = el as LovelaceElement;
element.hass = this._hass;
}
}
public getCardSize(): number {
return 4;
}
public setConfig(config: Config): void {
if (!config) {
throw new Error("Invalid Configuration");
} else if (!config.image) {
throw new Error("Invalid Configuration: image required");
} else if (!Array.isArray(config.elements)) {
throw new Error("Invalid Configuration: elements required");
}
this._config = config;
}
protected render(): TemplateResult {
if (!this._config) {
return html``;
}
return html`
${this.renderStyle()}
<ha-card .header="${this._config.title}">
<div id="root">
<img src="${this._config.image}">
${this._config.elements.map((elementConfig: LovelaceElementConfig) =>
this._createHuiElement(elementConfig)
)}
</div>
</ha-card>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-card {
overflow: hidden;
}
#root {
position: relative;
overflow: hidden;
}
#root img {
display: block;
width: 100%;
}
.element {
position: absolute;
transform: translate(-50%, -50%);
}
</style>
`;
}
private _createHuiElement(
elementConfig: LovelaceElementConfig
): LovelaceElement {
const element = createHuiElement(elementConfig) as LovelaceElement;
element.hass = this._hass;
element.classList.add("element");
Object.keys(elementConfig.style).forEach((prop) => {
element.style.setProperty(prop, elementConfig.style[prop]);
});
return element;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-picture-elements-card": HuiPictureElementsCard;
}
}
customElements.define("hui-picture-elements-card", HuiPictureElementsCard);
import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import createHuiElement from "../common/create-hui-element";
import { LovelaceCard, LovelaceConfig } from "../types";
import { HomeAssistant } from "../../../types";
import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
interface Config extends LovelaceConfig {
title?: string;
image: string;
elements: LovelaceElementConfig[];
}
class HuiPictureElementsCard extends LitElement implements LovelaceCard {
private _config?: Config;
private _hass?: HomeAssistant;
static get properties() {
return {
_config: {},
};
}
set hass(hass: HomeAssistant) {
this._hass = hass;
for (const el of this.shadowRoot!.querySelectorAll("#root > *")) {
const element = el as LovelaceElement;
element.hass = this._hass;
}
}
public getCardSize(): number {
return 4;
}
public setConfig(config: Config): void {
if (!config) {
throw new Error("Invalid Configuration");
} else if (!config.image) {
throw new Error("Invalid Configuration: image required");
} else if (!Array.isArray(config.elements)) {
throw new Error("Invalid Configuration: elements required");
}
this._config = config;
}
protected render(): TemplateResult {
if (!this._config) {
return html``;
}
return html`
${this.renderStyle()}
<ha-card .header="${this._config.title}">
<div id="root">
<img src="${this._config.image}">
${this._config.elements.map((elementConfig: LovelaceElementConfig) =>
this._createHuiElement(elementConfig)
)}
</div>
</ha-card>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-card {
overflow: hidden;
}
#root {
position: relative;
overflow: hidden;
}
#root img {
display: block;
width: 100%;
}
.element {
position: absolute;
transform: translate(-50%, -50%);
}
</style>
`;
}
private _createHuiElement(
elementConfig: LovelaceElementConfig
): LovelaceElement {
const element = createHuiElement(elementConfig) as LovelaceElement;
element.hass = this._hass;
element.classList.add("element");
Object.keys(elementConfig.style).forEach((prop) => {
element.style.setProperty(prop, elementConfig.style[prop]);
});
return element;
}
}
declare global {
interface HTMLElementTagNameMap {
"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 { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import "../components/hui-image";
import computeDomain from "../../../common/entity/compute_domain";
import computeStateDisplay from "../../../common/entity/compute_state_display";
import computeStateName from "../../../common/entity/compute_state_name";
import toggleEntity from "../common/entity/toggle-entity";
import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import { longPressBind } from "../common/directives/long-press-directive";
const UNAVAILABLE = "Unavailable";
/*
* @appliesMixin LocalizeMixin
* @appliesMixin EventsMixin
*/
class HuiPictureEntityCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style>
ha-card {
min-height: 75px;
overflow: hidden;
position: relative;
}
ha-card.canInteract {
cursor: pointer;
}
.footer {
@apply --paper-font-common-nowrap;
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
padding: 16px;
font-size: 16px;
line-height: 16px;
color: white;
}
.both {
display: flex;
justify-content: space-between;
}
.state {
text-align: right;
}
</style>
<ha-card id='card'>
<hui-image
hass="[[hass]]"
image="[[_config.image]]"
state-image="[[_config.state_image]]"
camera-image="[[_getCameraImage(_config)]]"
entity="[[_config.entity]]"
aspect-ratio="[[_config.aspect_ratio]]"
></hui-image>
<template is="dom-if" if="[[_showNameAndState(_config)]]">
<div class="footer both">
<div>[[_name]]</div>
<div>[[_state]]</div>
</div>
</template>
<template is="dom-if" if="[[_showName(_config)]]">
<div class="footer">
[[_name]]
</div>
</template>
<template is="dom-if" if="[[_showState(_config)]]">
<div class="footer state">
[[_state]]
</div>
</template>
</ha-card>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: "_hassChanged",
},
_config: Object,
_name: String,
_state: String,
};
}
getCardSize() {
return 3;
}
setConfig(config) {
if (!config || !config.entity) {
throw new Error("Error in card configuration.");
}
this._entityDomain = computeDomain(config.entity);
if (
this._entityDomain !== "camera" &&
(!config.image && !config.state_image && !config.camera_image)
) {
throw new Error("No image source configured.");
}
this._config = config;
}
ready() {
super.ready();
const card = this.shadowRoot.querySelector("#card");
longPressBind(card);
card.addEventListener("ha-click", () => this._cardClicked(false));
card.addEventListener("ha-hold", () => this._cardClicked(true));
}
_hassChanged(hass) {
const config = this._config;
const entityId = config.entity;
const stateObj = hass.states[entityId];
// Nothing changed
if (
(!stateObj && this._oldState === UNAVAILABLE) ||
(stateObj && stateObj.state === this._oldState)
) {
return;
}
let name;
let state;
let stateLabel;
let available;
if (stateObj) {
name = config.name || computeStateName(stateObj);
state = stateObj.state;
stateLabel = computeStateDisplay(this.localize, stateObj);
available = true;
} else {
name = config.name || entityId;
state = UNAVAILABLE;
stateLabel = this.localize("state.default.unavailable");
available = false;
}
this.setProperties({
_name: name,
_state: stateLabel,
_oldState: state,
});
this.$.card.classList.toggle("canInteract", available);
}
_showNameAndState(config) {
return config.show_name !== false && config.show_state !== false;
}
_showName(config) {
return config.show_name !== false && config.show_state === false;
}
_showState(config) {
return config.show_name === false && config.show_state !== false;
}
_cardClicked(hold) {
const config = this._config;
const entityId = config.entity;
if (!(entityId in this.hass.states)) return;
const action = hold ? config.hold_action : config.tap_action || "more-info";
switch (action) {
case "toggle":
toggleEntity(this.hass, entityId);
break;
case "more-info":
this.fire("hass-more-info", { entityId });
break;
default:
}
}
_getCameraImage(config) {
return this._entityDomain === "camera"
? config.entity
: config.camera_image;
}
}
customElements.define("hui-picture-entity-card", HuiPictureEntityCard);
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import "../components/hui-image";
import computeDomain from "../../../common/entity/compute_domain";
import computeStateDisplay from "../../../common/entity/compute_state_display";
import computeStateName from "../../../common/entity/compute_state_name";
import toggleEntity from "../common/entity/toggle-entity";
import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import { longPressBind } from "../common/directives/long-press-directive";
const UNAVAILABLE = "Unavailable";
/*
* @appliesMixin LocalizeMixin
* @appliesMixin EventsMixin
*/
class HuiPictureEntityCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style>
ha-card {
min-height: 75px;
overflow: hidden;
position: relative;
}
ha-card.canInteract {
cursor: pointer;
}
.footer {
@apply --paper-font-common-nowrap;
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
padding: 16px;
font-size: 16px;
line-height: 16px;
color: white;
}
.both {
display: flex;
justify-content: space-between;
}
.state {
text-align: right;
}
</style>
<ha-card id='card'>
<hui-image
hass="[[hass]]"
image="[[_config.image]]"
state-image="[[_config.state_image]]"
camera-image="[[_getCameraImage(_config)]]"
entity="[[_config.entity]]"
aspect-ratio="[[_config.aspect_ratio]]"
></hui-image>
<template is="dom-if" if="[[_showNameAndState(_config)]]">
<div class="footer both">
<div>[[_name]]</div>
<div>[[_state]]</div>
</div>
</template>
<template is="dom-if" if="[[_showName(_config)]]">
<div class="footer">
[[_name]]
</div>
</template>
<template is="dom-if" if="[[_showState(_config)]]">
<div class="footer state">
[[_state]]
</div>
</template>
</ha-card>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: "_hassChanged",
},
_config: Object,
_name: String,
_state: String,
};
}
getCardSize() {
return 3;
}
setConfig(config) {
if (!config || !config.entity) {
throw new Error("Error in card configuration.");
}
this._entityDomain = computeDomain(config.entity);
if (
this._entityDomain !== "camera" &&
(!config.image && !config.state_image && !config.camera_image)
) {
throw new Error("No image source configured.");
}
this._config = config;
}
ready() {
super.ready();
const card = this.shadowRoot.querySelector("#card");
longPressBind(card);
card.addEventListener("ha-click", () => this._cardClicked(false));
card.addEventListener("ha-hold", () => this._cardClicked(true));
}
_hassChanged(hass) {
const config = this._config;
const entityId = config.entity;
const stateObj = hass.states[entityId];
// Nothing changed
if (
(!stateObj && this._oldState === UNAVAILABLE) ||
(stateObj && stateObj.state === this._oldState)
) {
return;
}
let name;
let state;
let stateLabel;
let available;
if (stateObj) {
name = config.name || computeStateName(stateObj);
state = stateObj.state;
stateLabel = computeStateDisplay(this.localize, stateObj);
available = true;
} else {
name = config.name || entityId;
state = UNAVAILABLE;
stateLabel = this.localize("state.default.unavailable");
available = false;
}
this.setProperties({
_name: name,
_state: stateLabel,
_oldState: state,
});
this.$.card.classList.toggle("canInteract", available);
}
_showNameAndState(config) {
return config.show_name !== false && config.show_state !== false;
}
_showName(config) {
return config.show_name !== false && config.show_state === false;
}
_showState(config) {
return config.show_name === false && config.show_state !== false;
}
_cardClicked(hold) {
const config = this._config;
const entityId = config.entity;
if (!(entityId in this.hass.states)) return;
const action = hold ? config.hold_action : config.tap_action || "more-info";
switch (action) {
case "toggle":
toggleEntity(this.hass, entityId);
break;
case "more-info":
this.fire("hass-more-info", { entityId });
break;
default:
}
}
_getCameraImage(config) {
return this._entityDomain === "camera"
? config.entity
: config.camera_image;
}
}
customElements.define("hui-picture-entity-card", HuiPictureEntityCard);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,68 +1,68 @@
import "@polymer/paper-button/paper-button";
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant } from "../../../types";
let registeredDialog = false;
export class HuiCardOptions extends LitElement {
public cardId?: string;
protected hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return {
hass: {},
};
}
public connectedCallback() {
super.connectedCallback();
if (!registeredDialog) {
registeredDialog = true;
fireEvent(this, "register-dialog", {
dialogShowEvent: "show-edit-card",
dialogTag: "hui-dialog-edit-card",
dialogImport: () => import("../editor/hui-dialog-edit-card"),
});
}
}
protected render() {
return html`
<style>
div {
border-top: 1px solid #e8e8e8;
padding: 5px 16px;
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;
text-align: right;
}
paper-button {
color: var(--primary-color);
font-weight: 500;
}
</style>
<slot></slot>
<div>
<paper-button
@click="${this._editCard}"
>EDIT</paper-button>
</div>
`;
}
private _editCard() {
fireEvent(this, "show-edit-card", {
hass: this.hass,
cardId: this.cardId,
reloadLovelace: () => fireEvent(this, "config-refresh"),
});
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-card-options": HuiCardOptions;
}
}
customElements.define("hui-card-options", HuiCardOptions);
import "@polymer/paper-button/paper-button";
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant } from "../../../types";
let registeredDialog = false;
export class HuiCardOptions extends LitElement {
public cardId?: string;
protected hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return {
hass: {},
};
}
public connectedCallback() {
super.connectedCallback();
if (!registeredDialog) {
registeredDialog = true;
fireEvent(this, "register-dialog", {
dialogShowEvent: "show-edit-card",
dialogTag: "hui-dialog-edit-card",
dialogImport: () => import("../editor/hui-dialog-edit-card"),
});
}
}
protected render() {
return html`
<style>
div {
border-top: 1px solid #e8e8e8;
padding: 5px 16px;
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;
text-align: right;
}
paper-button {
color: var(--primary-color);
font-weight: 500;
}
</style>
<slot></slot>
<div>
<paper-button
@click="${this._editCard}"
>EDIT</paper-button>
</div>
`;
}
private _editCard() {
fireEvent(this, "show-edit-card", {
hass: this.hass,
cardId: this.cardId,
reloadLovelace: () => fireEvent(this, "config-refresh"),
});
}
}
declare global {
interface HTMLElementTagNameMap {
"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 { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { DOMAINS_TOGGLE } from "../../../common/const";
import turnOnOffEntities from "../common/entity/turn-on-off-entities";
class HuiEntitiesToggle extends PolymerElement {
static get template() {
return html`
<style>
:host {
width: 38px;
display: block;
}
paper-toggle-button {
cursor: pointer;
--paper-toggle-button-label-spacing: 0;
padding: 13px 5px;
margin: -4px -5px;
}
</style>
<template is="dom-if" if="[[_toggleEntities.length]]">
<paper-toggle-button checked="[[_computeIsChecked(hass, _toggleEntities)]]" on-change="_callService"></paper-toggle-button>
</template>
`;
}
static get properties() {
return {
hass: Object,
entities: Array,
_toggleEntities: {
type: Array,
computed: "_computeToggleEntities(hass, entities)",
},
};
}
_computeToggleEntities(hass, entityIds) {
return entityIds.filter(
(entityId) =>
entityId in hass.states && DOMAINS_TOGGLE.has(entityId.split(".", 1)[0])
);
}
_computeIsChecked(hass, entityIds) {
return entityIds.some((entityId) => hass.states[entityId].state === "on");
}
_callService(ev) {
const turnOn = ev.target.checked;
turnOnOffEntities(this.hass, this._toggleEntities, turnOn);
}
}
customElements.define("hui-entities-toggle", HuiEntitiesToggle);
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { DOMAINS_TOGGLE } from "../../../common/const";
import turnOnOffEntities from "../common/entity/turn-on-off-entities";
class HuiEntitiesToggle extends PolymerElement {
static get template() {
return html`
<style>
:host {
width: 38px;
display: block;
}
paper-toggle-button {
cursor: pointer;
--paper-toggle-button-label-spacing: 0;
padding: 13px 5px;
margin: -4px -5px;
}
</style>
<template is="dom-if" if="[[_toggleEntities.length]]">
<paper-toggle-button checked="[[_computeIsChecked(hass, _toggleEntities)]]" on-change="_callService"></paper-toggle-button>
</template>
`;
}
static get properties() {
return {
hass: Object,
entities: Array,
_toggleEntities: {
type: Array,
computed: "_computeToggleEntities(hass, entities)",
},
};
}
_computeToggleEntities(hass, entityIds) {
return entityIds.filter(
(entityId) =>
entityId in hass.states && DOMAINS_TOGGLE.has(entityId.split(".", 1)[0])
);
}
_computeIsChecked(hass, entityIds) {
return entityIds.some((entityId) => hass.states[entityId].state === "on");
}
_callService(ev) {
const turnOn = ev.target.checked;
turnOnOffEntities(this.hass, this._toggleEntities, turnOn);
}
}
customElements.define("hui-entities-toggle", HuiEntitiesToggle);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,52 +1,52 @@
import yaml from "js-yaml";
import "@polymer/paper-input/paper-textarea";
import createCardElement from "../common/create-card-element";
import createErrorCardConfig from "../common/create-error-card-config";
import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types";
export class HuiYAMLCardPreview extends HTMLElement {
private _hass?: HomeAssistant;
set hass(value: HomeAssistant) {
this._hass = value;
if (this.lastChild) {
(this.lastChild as LovelaceCard).hass = value;
}
}
set yaml(value: string) {
if (this.lastChild) {
this.removeChild(this.lastChild);
}
if (value === "") {
return;
}
let conf;
try {
conf = yaml.safeLoad(value);
} catch (err) {
conf = createErrorCardConfig(`Invalid YAML: ${err.message}`, undefined);
}
const element = createCardElement(conf);
if (this._hass) {
element.hass = this._hass;
}
this.appendChild(element);
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-yaml-card-preview": HuiYAMLCardPreview;
}
}
customElements.define("hui-yaml-card-preview", HuiYAMLCardPreview);
import yaml from "js-yaml";
import "@polymer/paper-input/paper-textarea";
import createCardElement from "../common/create-card-element";
import createErrorCardConfig from "../common/create-error-card-config";
import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types";
export class HuiYAMLCardPreview extends HTMLElement {
private _hass?: HomeAssistant;
set hass(value: HomeAssistant) {
this._hass = value;
if (this.lastChild) {
(this.lastChild as LovelaceCard).hass = value;
}
}
set yaml(value: string) {
if (this.lastChild) {
this.removeChild(this.lastChild);
}
if (value === "") {
return;
}
let conf;
try {
conf = yaml.safeLoad(value);
} catch (err) {
conf = createErrorCardConfig(`Invalid YAML: ${err.message}`, undefined);
}
const element = createCardElement(conf);
if (this._hass) {
element.hass = this._hass;
}
this.appendChild(element);
}
}
declare global {
interface HTMLElementTagNameMap {
"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 { fireEvent } from "../../../common/dom/fire_event";
import "@polymer/paper-input/paper-textarea";
export class HuiYAMLEditor extends LitElement {
public yaml?: string;
static get properties(): PropertyDeclarations {
return {
yaml: {},
};
}
protected render() {
return html`
<style>
paper-textarea {
--paper-input-container-shared-input-style_-_font-family: monospace;
}
</style>
<paper-textarea
value="${this.yaml}"
@value-changed="${this._valueChanged}"
></paper-textarea>
`;
}
private _valueChanged(ev) {
this.yaml = ev.target.value;
fireEvent(this, "yaml-changed", { yaml: ev.target.value });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-yaml-editor": HuiYAMLEditor;
}
}
customElements.define("hui-yaml-editor", HuiYAMLEditor);
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { fireEvent } from "../../../common/dom/fire_event";
import "@polymer/paper-input/paper-textarea";
export class HuiYAMLEditor extends LitElement {
public yaml?: string;
static get properties(): PropertyDeclarations {
return {
yaml: {},
};
}
protected render() {
return html`
<style>
paper-textarea {
--paper-input-container-shared-input-style_-_font-family: monospace;
}
</style>
<paper-textarea
value="${this.yaml}"
@value-changed="${this._valueChanged}"
></paper-textarea>
`;
}
private _valueChanged(ev) {
this.yaml = ev.target.value;
fireEvent(this, "yaml-changed", { yaml: ev.target.value });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-yaml-editor": HuiYAMLEditor;
}
}
customElements.define("hui-yaml-editor", HuiYAMLEditor);

View File

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

View File

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

View File

@ -1,74 +1,74 @@
import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "../../../components/buttons/ha-call-service-button";
import { LovelaceElement, LovelaceElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
export class HuiServiceButtonElement extends LitElement
implements LovelaceElement {
public hass?: HomeAssistant;
private _config?: LovelaceElementConfig;
private _domain?: string;
private _service?: string;
static get properties() {
return { _config: {} };
}
public setConfig(config: LovelaceElementConfig): void {
if (!config || !config.service) {
throw Error("Invalid Configuration: 'service' required");
}
[this._domain, this._service] = config.service.split(".", 2);
if (!this._domain) {
throw Error("Invalid Configuration: 'service' does not have a domain");
}
if (!this._service) {
throw Error(
"Invalid Configuration: 'service' does not have a service name"
);
}
this._config = config;
}
protected render(): TemplateResult {
if (!this._config) {
return html``;
}
return html`
${this.renderStyle()}
<ha-call-service-button
.hass="${this.hass}"
.domain="${this._domain}"
.service="${this._service}"
.service-data="${this._config.service_data}"
>${this._config.title}</ha-call-service-button>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-call-service-button {
color: var(--primary-color);
white-space: nowrap;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-service-button-element": HuiServiceButtonElement;
}
}
customElements.define("hui-service-button-element", HuiServiceButtonElement);
import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "../../../components/buttons/ha-call-service-button";
import { LovelaceElement, LovelaceElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
export class HuiServiceButtonElement extends LitElement
implements LovelaceElement {
public hass?: HomeAssistant;
private _config?: LovelaceElementConfig;
private _domain?: string;
private _service?: string;
static get properties() {
return { _config: {} };
}
public setConfig(config: LovelaceElementConfig): void {
if (!config || !config.service) {
throw Error("Invalid Configuration: 'service' required");
}
[this._domain, this._service] = config.service.split(".", 2);
if (!this._domain) {
throw Error("Invalid Configuration: 'service' does not have a domain");
}
if (!this._service) {
throw Error(
"Invalid Configuration: 'service' does not have a service name"
);
}
this._config = config;
}
protected render(): TemplateResult {
if (!this._config) {
return html``;
}
return html`
${this.renderStyle()}
<ha-call-service-button
.hass="${this.hass}"
.domain="${this._domain}"
.service="${this._service}"
.service-data="${this._config.service_data}"
>${this._config.title}</ha-call-service-button>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-call-service-button {
color: var(--primary-color);
white-space: nowrap;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"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 "../../../components/entity/ha-state-label-badge";
import computeStateName from "../../../common/entity/compute_state_name";
import { LovelaceElement, LovelaceElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { TemplateResult } from "lit-html";
export class HuiStateBadgeElement extends LitElement
implements LovelaceElement {
public hass?: HomeAssistant;
private _config?: LovelaceElementConfig;
static get properties() {
return { hass: {}, _config: {} };
}
public setConfig(config: LovelaceElementConfig): void {
if (!config.entity) {
throw Error("Invalid Configuration: 'entity' required");
}
this._config = config;
}
protected render(): TemplateResult {
if (
!this._config ||
!this.hass ||
!this.hass.states[this._config.entity!]
) {
return html``;
}
const state = this.hass.states[this._config.entity!];
return html`
<ha-state-label-badge
.hass=${this.hass}
.state=${state}
.title="${computeStateName(state)}"
></ha-state-label-badge>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-state-badge-element": HuiStateBadgeElement;
}
}
customElements.define("hui-state-badge-element", HuiStateBadgeElement);
import { html, LitElement } from "@polymer/lit-element";
import "../../../components/entity/ha-state-label-badge";
import computeStateName from "../../../common/entity/compute_state_name";
import { LovelaceElement, LovelaceElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { TemplateResult } from "lit-html";
export class HuiStateBadgeElement extends LitElement
implements LovelaceElement {
public hass?: HomeAssistant;
private _config?: LovelaceElementConfig;
static get properties() {
return { hass: {}, _config: {} };
}
public setConfig(config: LovelaceElementConfig): void {
if (!config.entity) {
throw Error("Invalid Configuration: 'entity' required");
}
this._config = config;
}
protected render(): TemplateResult {
if (
!this._config ||
!this.hass ||
!this.hass.states[this._config.entity!]
) {
return html``;
}
const state = this.hass.states[this._config.entity!];
return html`
<ha-state-label-badge
.hass=${this.hass}
.state=${state}
.title="${computeStateName(state)}"
></ha-state-label-badge>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"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 { TemplateResult } from "lit-html";
import "../../../components/entity/state-badge";
import { computeTooltip } from "../common/compute-tooltip";
import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceElement, LovelaceElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
export class HuiStateIconElement extends hassLocalizeLitMixin(LitElement)
implements LovelaceElement {
public hass?: HomeAssistant;
private _config?: LovelaceElementConfig;
static get properties() {
return { hass: {}, _config: {} };
}
public setConfig(config: LovelaceElementConfig): void {
if (!config.entity) {
throw Error("Invalid Configuration: 'entity' required");
}
this._config = config;
}
protected render(): TemplateResult {
if (
!this._config ||
!this.hass ||
!this.hass.states[this._config.entity!]
) {
return html``;
}
const state = this.hass!.states[this._config.entity!];
return html`
${this.renderStyle()}
<state-badge
.stateObj=${state}
.title="${computeTooltip(this.hass!, this._config)}"
@ha-click="${this._handleClick}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
></state-badge>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
:host {
cursor: pointer;
}
</style>
`;
}
private _handleClick() {
handleClick(this, this.hass!, this._config!, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true);
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-state-icon-element": HuiStateIconElement;
}
}
customElements.define("hui-state-icon-element", HuiStateIconElement);
import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "../../../components/entity/state-badge";
import { computeTooltip } from "../common/compute-tooltip";
import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceElement, LovelaceElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
export class HuiStateIconElement extends hassLocalizeLitMixin(LitElement)
implements LovelaceElement {
public hass?: HomeAssistant;
private _config?: LovelaceElementConfig;
static get properties() {
return { hass: {}, _config: {} };
}
public setConfig(config: LovelaceElementConfig): void {
if (!config.entity) {
throw Error("Invalid Configuration: 'entity' required");
}
this._config = config;
}
protected render(): TemplateResult {
if (
!this._config ||
!this.hass ||
!this.hass.states[this._config.entity!]
) {
return html``;
}
const state = this.hass!.states[this._config.entity!];
return html`
${this.renderStyle()}
<state-badge
.stateObj=${state}
.title="${computeTooltip(this.hass!, this._config)}"
@ha-click="${this._handleClick}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
></state-badge>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
:host {
cursor: pointer;
}
</style>
`;
}
private _handleClick() {
handleClick(this, this.hass!, this._config!, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true);
}
}
declare global {
interface HTMLElementTagNameMap {
"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 "../../../components/entity/ha-state-label-badge";
import computeStateDisplay from "../../../common/entity/compute_state_display";
import { computeTooltip } from "../common/compute-tooltip";
import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceElement, LovelaceElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { TemplateResult } from "lit-html";
interface Config extends LovelaceElementConfig {
prefix?: string;
suffix?: string;
}
class HuiStateLabelElement extends hassLocalizeLitMixin(LitElement)
implements LovelaceElement {
public hass?: HomeAssistant;
private _config?: Config;
static get properties() {
return { hass: {}, _config: {} };
}
public setConfig(config: Config): void {
if (!config.entity) {
throw Error("Invalid Configuration: 'entity' required");
}
this._config = config;
}
protected render(): TemplateResult {
if (!this._config) {
return html``;
}
const state = this.hass!.states[this._config.entity!];
return html`
${this.renderStyle()}
<div
.title="${computeTooltip(this.hass!, this._config)}"
@ha-click="${() => handleClick(this, this.hass!, this._config!, false)}"
@ha-hold="${() => handleClick(this, this.hass!, this._config!, true)}"
.longPress="${longPress()}"
>
${this._config.prefix}${
state ? computeStateDisplay(this.localize, state) : "-"
}${this._config.suffix}
</div>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
:host {
cursor: pointer;
}
div {
padding: 8px;
white-space: nowrap;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-state-label-element": HuiStateLabelElement;
}
}
customElements.define("hui-state-label-element", HuiStateLabelElement);
import { html, LitElement } from "@polymer/lit-element";
import "../../../components/entity/ha-state-label-badge";
import computeStateDisplay from "../../../common/entity/compute_state_display";
import { computeTooltip } from "../common/compute-tooltip";
import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceElement, LovelaceElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { TemplateResult } from "lit-html";
interface Config extends LovelaceElementConfig {
prefix?: string;
suffix?: string;
}
class HuiStateLabelElement extends hassLocalizeLitMixin(LitElement)
implements LovelaceElement {
public hass?: HomeAssistant;
private _config?: Config;
static get properties() {
return { hass: {}, _config: {} };
}
public setConfig(config: Config): void {
if (!config.entity) {
throw Error("Invalid Configuration: 'entity' required");
}
this._config = config;
}
protected render(): TemplateResult {
if (!this._config) {
return html``;
}
const state = this.hass!.states[this._config.entity!];
return html`
${this.renderStyle()}
<div
.title="${computeTooltip(this.hass!, this._config)}"
@ha-click="${() => handleClick(this, this.hass!, this._config!, false)}"
@ha-hold="${() => handleClick(this, this.hass!, this._config!, true)}"
.longPress="${longPress()}"
>
${this._config.prefix}${
state ? computeStateDisplay(this.localize, state) : "-"
}${this._config.suffix}
</div>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
:host {
cursor: pointer;
}
div {
padding: 8px;
white-space: nowrap;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"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 { TemplateResult } from "lit-html";
import "../../../components/ha-climate-state";
import "../components/hui-generic-entity-row";
import { HomeAssistant } from "../../../types";
import { EntityRow, EntityConfig } from "./types";
class HuiClimateEntityRow extends LitElement implements EntityRow {
public hass?: HomeAssistant;
private _config?: EntityConfig;
static get properties() {
return {
hass: {},
_config: {},
};
}
public setConfig(config: EntityConfig): void {
if (!config || !config.entity) {
throw new Error("Invalid Configuration: 'entity' required");
}
this._config = config;
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
return html`
${this.renderStyle()}
<hui-generic-entity-row
.hass=${this.hass}
.config=${this._config}
>
<ha-climate-state
.hass=${this.hass}
.stateObj=${this.hass.states[this._config.entity]}
></ha-climate-state>
</hui-generic-entity-row>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-climate-state {
text-align: right;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-climate-entity-row": HuiClimateEntityRow;
}
}
customElements.define("hui-climate-entity-row", HuiClimateEntityRow);
import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "../../../components/ha-climate-state";
import "../components/hui-generic-entity-row";
import { HomeAssistant } from "../../../types";
import { EntityRow, EntityConfig } from "./types";
class HuiClimateEntityRow extends LitElement implements EntityRow {
public hass?: HomeAssistant;
private _config?: EntityConfig;
static get properties() {
return {
hass: {},
_config: {},
};
}
public setConfig(config: EntityConfig): void {
if (!config || !config.entity) {
throw new Error("Invalid Configuration: 'entity' required");
}
this._config = config;
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
return html`
${this.renderStyle()}
<hui-generic-entity-row
.hass=${this.hass}
.config=${this._config}
>
<ha-climate-state
.hass=${this.hass}
.stateObj=${this.hass.states[this._config.entity]}
></ha-climate-state>
</hui-generic-entity-row>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-climate-state {
text-align: right;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"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 { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/hui-generic-entity-row";
import "../../../components/ha-cover-controls";
import "../../../components/ha-cover-tilt-controls";
import CoverEntity from "../../../util/cover-model";
class HuiCoverEntityRow extends PolymerElement {
static get template() {
return html`
${this.styleTemplate}
<hui-generic-entity-row
hass="[[hass]]"
config="[[_config]]"
>
${this.coverControlTemplate}
</hui-generic-entity-row>
`;
}
static get styleTemplate() {
return html`
<style>
ha-cover-controls,
ha-cover-tilt-controls {
margin-right: -.57em;
}
</style>
`;
}
static get coverControlTemplate() {
return html`
<template is="dom-if" if="[[!_entityObj.isTiltOnly]]">
<ha-cover-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-controls>
</template>
<template is="dom-if" if="[[_entityObj.isTiltOnly]]">
<ha-cover-tilt-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-tilt-controls>
</template>
`;
}
static get properties() {
return {
hass: Object,
_config: Object,
_stateObj: {
type: Object,
computed: "_computeStateObj(hass.states, _config.entity)",
},
_entityObj: {
type: Object,
computed: "_computeEntityObj(hass, _stateObj)",
},
};
}
_computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null;
}
_computeEntityObj(hass, stateObj) {
return stateObj ? new CoverEntity(hass, stateObj) : null;
}
setConfig(config) {
if (!config || !config.entity) {
throw new Error("Entity not configured.");
}
this._config = config;
}
}
customElements.define("hui-cover-entity-row", HuiCoverEntityRow);
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/hui-generic-entity-row";
import "../../../components/ha-cover-controls";
import "../../../components/ha-cover-tilt-controls";
import CoverEntity from "../../../util/cover-model";
class HuiCoverEntityRow extends PolymerElement {
static get template() {
return html`
${this.styleTemplate}
<hui-generic-entity-row
hass="[[hass]]"
config="[[_config]]"
>
${this.coverControlTemplate}
</hui-generic-entity-row>
`;
}
static get styleTemplate() {
return html`
<style>
ha-cover-controls,
ha-cover-tilt-controls {
margin-right: -.57em;
}
</style>
`;
}
static get coverControlTemplate() {
return html`
<template is="dom-if" if="[[!_entityObj.isTiltOnly]]">
<ha-cover-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-controls>
</template>
<template is="dom-if" if="[[_entityObj.isTiltOnly]]">
<ha-cover-tilt-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-tilt-controls>
</template>
`;
}
static get properties() {
return {
hass: Object,
_config: Object,
_stateObj: {
type: Object,
computed: "_computeStateObj(hass.states, _config.entity)",
},
_entityObj: {
type: Object,
computed: "_computeEntityObj(hass, _stateObj)",
},
};
}
_computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null;
}
_computeEntityObj(hass, stateObj) {
return stateObj ? new CoverEntity(hass, stateObj) : null;
}
setConfig(config) {
if (!config || !config.entity) {
throw new Error("Entity not configured.");
}
this._config = config;
}
}
customElements.define("hui-cover-entity-row", HuiCoverEntityRow);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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