Merge pull request #2429 from home-assistant/dev

2019109.0
This commit is contained in:
Paulus Schoutsen 2019-01-09 14:39:11 -08:00 committed by GitHub
commit 67c032c85a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
142 changed files with 6455 additions and 1787 deletions

View File

@ -7,7 +7,7 @@ import { demoConfig } from "../data/demo_config";
import { demoServices } from "../data/demo_services";
import demoResources from "../data/demo_resources";
import demoStates from "../data/demo_states";
import createCardElement from "../../../src/panels/lovelace/common/create-card-element";
import { createCardElement } from "../../../src/panels/lovelace/common/create-card-element";
class DemoCard extends PolymerElement {
static get template() {
@ -78,6 +78,10 @@ class DemoCard extends PolymerElement {
hass.resources = demoResources;
hass.language = "en";
hass.states = demoStates;
hass.themes = {
default_theme: "default",
themes: {},
};
el.hass = hass;
}

View File

@ -29,6 +29,31 @@ const ENTITIES = [
friendly_name: "Home",
icon: "mdi:home",
}),
getEntity("zone", "bushfire", "zoning", {
latitude: -33.8611,
longitude: 151.203,
radius: 35000,
friendly_name: "Bushfire Zone",
icon: "mdi:home",
}),
getEntity("geo_location", "nelsons_creek", "15", {
source: "bushfire_demo",
latitude: -34.07792,
longitude: 151.03219,
friendly_name: "Nelsons Creek",
}),
getEntity("geo_location", "forest_rd_nowra_hill", "8", {
source: "bushfire_demo",
latitude: -33.69452,
longitude: 151.19577,
friendly_name: "Forest Rd, Nowra Hill",
}),
getEntity("geo_location", "stoney_ridge_rd_kremnos", "20", {
source: "bushfire_demo",
latitude: -33.66584,
longitude: 150.97209,
friendly_name: "Stoney Ridge Rd, Kremnos",
}),
];
const CONFIGS = [
@ -116,6 +141,24 @@ const CONFIGS = [
- light.bed_light
`,
},
{
heading: "Geo Location Entities",
config: `
- type: map
geo_location_sources:
- bushfire_demo
`,
},
{
heading: "Geo Location Entities with Home Zone",
config: `
- type: map
geo_location_sources:
- bushfire_demo
entities:
- zone.bushfire
`,
},
];
class DemoMap extends PolymerElement {

View File

@ -9,6 +9,9 @@ const ENTITIES = [
getEntity("light", "kitchen_lights", "on", {
friendly_name: "Kitchen Lights",
}),
getEntity("light", "bed_light", "on", {
friendly_name: "Bed Lights",
}),
getEntity("device_tracker", "demo_paulus", "work", {
source_type: "gps",
latitude: 32.877105,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

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

View File

@ -8,6 +8,7 @@ import "../components/ha-icon";
import EventsMixin from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin";
import { computeRTL } from "../common/util/compute_rtl";
/*
* @appliesMixin LocalizeMixin
@ -30,12 +31,16 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
.header {
font-family: var(--paper-font-headline_-_font-family);
-webkit-font-smoothing: var(--paper-font-headline_-_-webkit-font-smoothing);
-webkit-font-smoothing: var(
--paper-font-headline_-_-webkit-font-smoothing
);
font-size: var(--paper-font-headline_-_font-size);
font-weight: var(--paper-font-headline_-_font-weight);
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
text-rendering: var(--paper-font-common-expensive-kerning_-_text-rendering);
text-rendering: var(
--paper-font-common-expensive-kerning_-_text-rendering
);
opacity: var(--dark-primary-opacity);
padding: 24px 16px 16px;
display: flex;
@ -48,6 +53,11 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
color: var(--secondary-text-color);
}
:host([rtl]) .name {
margin-left: 0px;
margin-right: 16px;
}
.now {
display: flex;
justify-content: space-between;
@ -61,18 +71,31 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
margin-right: 32px;
}
:host([rtl]) .main {
margin-right: 0px;
}
.main ha-icon {
--iron-icon-height: 72px;
--iron-icon-width: 72px;
margin-right: 8px;
}
:host([rtl]) .main ha-icon {
margin-right: 0px;
}
.main .temp {
font-size: 52px;
line-height: 1em;
position: relative;
}
:host([rtl]) .main .temp {
direction: ltr;
margin-right: 28px;
}
.main .temp span {
font-size: 24px;
line-height: 1em;
@ -80,6 +103,14 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
top: 4px;
}
.measurand {
display: inline-block;
}
:host([rtl]) .measurand {
direction: ltr;
}
.forecast {
margin-top: 16px;
display: flex;
@ -96,13 +127,17 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
text-align: center;
}
:host([rtl]) .forecast .temp {
direction: ltr;
}
.weekday {
font-weight: bold;
}
.attributes,
.templow,
.precipitation { {
.precipitation {
color: var(--secondary-text-color);
}
</style>
@ -130,7 +165,9 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
>
<div>
[[localize('ui.card.weather.attributes.air_pressure')]]:
[[stateObj.attributes.pressure]] [[getUnit('air_pressure')]]
<span class="measurand">
[[stateObj.attributes.pressure]] [[getUnit('air_pressure')]]
</span>
</div>
</template>
<template
@ -139,7 +176,9 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
>
<div>
[[localize('ui.card.weather.attributes.humidity')]]:
[[stateObj.attributes.humidity]] %
<span class="measurand"
>[[stateObj.attributes.humidity]] %</span
>
</div>
</template>
<template
@ -148,8 +187,10 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
>
<div>
[[localize('ui.card.weather.attributes.wind_speed')]]:
[[getWind(stateObj.attributes.wind_speed,
stateObj.attributes.wind_bearing, localize)]]
<span class="measurand">
[[getWindSpeed(stateObj.attributes.wind_speed)]]
</span>
[[getWindBearing(stateObj.attributes.wind_bearing, localize)]]
</div>
</template>
</div>
@ -202,6 +243,11 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
type: Array,
computed: "computeForecast(stateObj.attributes.forecast)",
},
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "_computeRTL(hass)",
},
};
}
@ -295,14 +341,18 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
return degree;
}
getWind(speed, bearing, localize) {
getWindSpeed(speed) {
return `${speed} ${this.getUnit("length")}/h`;
}
getWindBearing(bearing, localize) {
if (bearing != null) {
const cardinalDirection = this.windBearingToText(bearing);
return `${speed} ${this.getUnit("length")}/h (${localize(
return `(${localize(
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
) || cardinalDirection})`;
}
return `${speed} ${this.getUnit("length")}/h`;
return ``;
}
_showValue(item) {
@ -324,5 +374,9 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
{ hour: "numeric" }
);
}
_computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("ha-weather-card", HaWeatherCard);

View File

@ -18,8 +18,8 @@ interface RefreshTokenResponse {
declare global {
interface Window {
externalApp?: {
getExternalAuth(payload: BasePayload);
revokeExternalAuth(payload: BasePayload);
getExternalAuth(payload: string);
revokeExternalAuth(payload: string);
};
webkit?: {
messageHandlers: {
@ -67,7 +67,7 @@ export default class ExternalAuth extends Auth {
const callbackPayload = { callback: CALLBACK_SET_TOKEN };
if (window.externalApp) {
window.externalApp.getExternalAuth(callbackPayload);
window.externalApp.getExternalAuth(JSON.stringify(callbackPayload));
} else {
window.webkit!.messageHandlers.getExternalAuth.postMessage(
callbackPayload
@ -92,7 +92,7 @@ export default class ExternalAuth extends Auth {
const callbackPayload = { callback: CALLBACK_REVOKE_TOKEN };
if (window.externalApp) {
window.externalApp.revokeExternalAuth(callbackPayload);
window.externalApp.revokeExternalAuth(JSON.stringify(callbackPayload));
} else {
window.webkit!.messageHandlers.revokeExternalAuth.postMessage(
callbackPayload

View File

@ -2,5 +2,6 @@ import { HassEntity } from "home-assistant-js-websocket";
import computeObjectId from "./compute_object_id";
export default (stateObj: HassEntity): string =>
stateObj.attributes.friendly_name ||
computeObjectId(stateObj.entity_id).replace(/_/g, " ");
stateObj.attributes.friendly_name === undefined
? computeObjectId(stateObj.entity_id).replace(/_/g, " ")
: stateObj.attributes.friendly_name || "";

View File

@ -0,0 +1,3 @@
export const afterNextRender = (cb: () => void): void => {
requestAnimationFrame(() => setTimeout(cb, 0));
};

View File

@ -1,25 +1,208 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../ha-label-badge";
import {
LitElement,
html,
PropertyValues,
PropertyDeclarations,
} from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { HassEntity } from "home-assistant-js-websocket";
import { classMap } from "lit-html/directives/classMap";
import computeStateDomain from "../../common/entity/compute_state_domain";
import computeStateName from "../../common/entity/compute_state_name";
import domainIcon from "../../common/entity/domain_icon";
import stateIcon from "../../common/entity/state_icon";
import timerTimeRemaining from "../../common/entity/timer_time_remaining";
import attributeClassNames from "../../common/entity/attribute_class_names";
import secondsToDuration from "../../common/datetime/seconds_to_duration";
import { fireEvent } from "../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../mixins/lit-localize-mixin";
import EventsMixin from "../../mixins/events-mixin";
import LocalizeMixin from "../../mixins/localize-mixin";
import "../ha-label-badge";
/*
* @appliesMixin LocalizeMixin
* @appliesMixin EventsMixin
*/
class HaStateLabelBadge extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
export class HaStateLabelBadge extends hassLocalizeLitMixin(LitElement) {
public state?: HassEntity;
private _connected?: boolean;
private _updateRemaining?: number;
private _timerTimeRemaining?: number;
public connectedCallback(): void {
super.connectedCallback();
this._connected = true;
this.startInterval(this.state);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this._connected = false;
this.clearInterval();
}
protected render(): TemplateResult {
const state = this.state;
if (!state) {
return html`
${this.renderStyle()}
<ha-label-badge label="not found"></ha-label-badge>
`;
}
const domain = computeStateDomain(state);
return html`
${this.renderStyle()}
<ha-label-badge
class="${
classMap({
[domain]: true,
"has-unit_of_measurement":
"unit_of_measurement" in state.attributes,
})
}"
.value="${this._computeValue(domain, state)}"
.icon="${this._computeIcon(domain, state)}"
.image="${state.attributes.entity_picture}"
.label="${this._computeLabel(domain, state, this._timerTimeRemaining)}"
.description="${computeStateName(state)}"
></ha-label-badge>
`;
}
static get properties(): PropertyDeclarations {
return {
hass: {},
state: {},
_timerTimeRemaining: {},
};
}
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.addEventListener("click", (ev) => {
ev.stopPropagation();
if (this.state) {
fireEvent(this, "hass-more-info", { entityId: this.state.entity_id });
}
});
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (this._connected && changedProperties.has("state")) {
this.startInterval(this.state);
}
}
private _computeValue(domain: string, state: HassEntity) {
switch (domain) {
case "binary_sensor":
case "device_tracker":
case "updater":
case "sun":
case "alarm_control_panel":
case "timer":
return null;
case "sensor":
default:
return state.state === "unknown"
? "-"
: this.localize(`component.${domain}.state.${state.state}`) ||
state.state;
}
}
private _computeIcon(domain: string, state: HassEntity) {
if (state.state === "unavailable") {
return null;
}
switch (domain) {
case "alarm_control_panel":
if (state.state === "pending") {
return "hass:clock-fast";
}
if (state.state === "armed_away") {
return "hass:nature";
}
if (state.state === "armed_home") {
return "hass:home-variant";
}
if (state.state === "armed_night") {
return "hass:weather-night";
}
if (state.state === "armed_custom_bypass") {
return "hass:security-home";
}
if (state.state === "triggered") {
return "hass:alert-circle";
}
// state == 'disarmed'
return domainIcon(domain, state.state);
case "binary_sensor":
case "device_tracker":
case "updater":
return stateIcon(state);
case "sun":
return state.state === "above_horizon"
? domainIcon(domain)
: "hass:brightness-3";
case "timer":
return state.state === "active" ? "hass:timer" : "hass:timer-off";
default:
return null;
}
}
private _computeLabel(domain, state, _timerTimeRemaining) {
if (
state.state === "unavailable" ||
["device_tracker", "alarm_control_panel"].includes(domain)
) {
// Localize the state with a special state_badge namespace, which has variations of
// the state translations that are truncated to fit within the badge label. Translations
// are only added for device_tracker and alarm_control_panel.
return (
this.localize(`state_badge.${domain}.${state.state}`) ||
this.localize(`state_badge.default.${state.state}`) ||
state.state
);
}
if (domain === "timer") {
return secondsToDuration(_timerTimeRemaining);
}
return state.attributes.unit_of_measurement || null;
}
private clearInterval() {
if (this._updateRemaining) {
clearInterval(this._updateRemaining);
this._updateRemaining = undefined;
}
}
private startInterval(stateObj) {
this.clearInterval();
if (stateObj && computeStateDomain(stateObj) === "timer") {
this.calculateTimerRemaining(stateObj);
if (stateObj.state === "active") {
this._updateRemaining = window.setInterval(
() => this.calculateTimerRemaining(this.state),
1000
);
}
}
}
private calculateTimerRemaining(stateObj) {
this._timerTimeRemaining = timerTimeRemaining(stateObj);
}
private renderStyle(): TemplateResult {
return html`
<style>
:host {
@ -61,175 +244,13 @@ class HaStateLabelBadge extends LocalizeMixin(EventsMixin(PolymerElement)) {
);
}
</style>
<ha-label-badge
class$="[[computeClassNames(state)]]"
value="[[computeValue(localize, state)]]"
icon="[[computeIcon(state)]]"
image="[[computeImage(state)]]"
label="[[computeLabel(localize, state, _timerTimeRemaining)]]"
description="[[computeDescription(state)]]"
></ha-label-badge>
`;
}
}
static get properties() {
return {
hass: Object,
state: {
type: Object,
observer: "stateChanged",
},
_timerTimeRemaining: {
type: Number,
value: 0,
},
};
}
connectedCallback() {
super.connectedCallback();
this.startInterval(this.state);
}
disconnectedCallback() {
super.disconnectedCallback();
this.clearInterval();
}
ready() {
super.ready();
this.addEventListener("click", (ev) => this.badgeTap(ev));
}
badgeTap(ev) {
ev.stopPropagation();
this.fire("hass-more-info", { entityId: this.state.entity_id });
}
computeClassNames(state) {
const classes = [computeStateDomain(state)];
classes.push(attributeClassNames(state, ["unit_of_measurement"]));
return classes.join(" ");
}
computeValue(localize, state) {
const domain = computeStateDomain(state);
switch (domain) {
case "binary_sensor":
case "device_tracker":
case "updater":
case "sun":
case "alarm_control_panel":
case "timer":
return null;
case "sensor":
default:
return state.state === "unknown"
? "-"
: localize(`component.${domain}.state.${state.state}`) || state.state;
}
}
computeIcon(state) {
if (state.state === "unavailable") {
return null;
}
const domain = computeStateDomain(state);
switch (domain) {
case "alarm_control_panel":
if (state.state === "pending") {
return "hass:clock-fast";
}
if (state.state === "armed_away") {
return "hass:nature";
}
if (state.state === "armed_home") {
return "hass:home-variant";
}
if (state.state === "armed_night") {
return "hass:weather-night";
}
if (state.state === "armed_custom_bypass") {
return "hass:security-home";
}
if (state.state === "triggered") {
return "hass:alert-circle";
}
// state == 'disarmed'
return domainIcon(domain, state.state);
case "binary_sensor":
case "device_tracker":
case "updater":
return stateIcon(state);
case "sun":
return state.state === "above_horizon"
? domainIcon(domain)
: "hass:brightness-3";
case "timer":
return state.state === "active" ? "hass:timer" : "hass:timer-off";
default:
return null;
}
}
computeImage(state) {
return state.attributes.entity_picture || null;
}
computeLabel(localize, state, _timerTimeRemaining) {
const domain = computeStateDomain(state);
if (
state.state === "unavailable" ||
["device_tracker", "alarm_control_panel"].includes(domain)
) {
// Localize the state with a special state_badge namespace, which has variations of
// the state translations that are truncated to fit within the badge label. Translations
// are only added for device_tracker and alarm_control_panel.
return (
localize(`state_badge.${domain}.${state.state}`) ||
localize(`state_badge.default.${state.state}`) ||
state.state
);
}
if (domain === "timer") {
return secondsToDuration(_timerTimeRemaining);
}
return state.attributes.unit_of_measurement || null;
}
computeDescription(state) {
return computeStateName(state);
}
stateChanged(stateObj) {
this.updateStyles();
this.startInterval(stateObj);
}
clearInterval() {
if (this._updateRemaining) {
clearInterval(this._updateRemaining);
this._updateRemaining = null;
}
}
startInterval(stateObj) {
this.clearInterval();
if (computeStateDomain(stateObj) === "timer") {
this.calculateTimerRemaining(stateObj);
if (stateObj.state === "active") {
this._updateRemaining = setInterval(
() => this.calculateTimerRemaining(this.state),
1000
);
}
}
}
calculateTimerRemaining(stateObj) {
this._timerTimeRemaining = timerTimeRemaining(stateObj);
declare global {
interface HTMLElementTagNameMap {
"ha-state-label-badge": HaStateLabelBadge;
}
}

View File

@ -81,8 +81,15 @@ class HaClimateControl extends EventsMixin(PolymerElement) {
this.$.target_temperature.classList.toggle("in-flux", inFlux);
}
_round(val) {
// round value to precision derived from step
// insired by https://github.com/soundar24/roundSlider/blob/master/src/roundslider.js
const s = this.step.toString().split(".");
return s[1] ? parseFloat(val.toFixed(s[1].length)) : Math.round(val);
}
incrementValue() {
const newval = this.value + this.step;
const newval = this._round(this.value + this.step);
if (this.value < this.max) {
this.last_changed = Date.now();
this.temperatureStateInFlux(true);
@ -102,7 +109,7 @@ class HaClimateControl extends EventsMixin(PolymerElement) {
}
decrementValue() {
const newval = this.value - this.step;
const newval = this._round(this.value - this.step);
if (this.value > this.min) {
this.last_changed = Date.now();
this.temperatureStateInFlux(true);

View File

@ -1,9 +1,83 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import {
LitElement,
PropertyDeclarations,
PropertyValues,
} from "@polymer/lit-element";
import { TemplateResult, html } from "lit-html";
import { classMap } from "lit-html/directives/classMap";
import "./ha-icon";
class HaLabelBadge extends PolymerElement {
static get template() {
class HaLabelBadge extends LitElement {
public value?: string;
public icon?: string;
public label?: string;
public description?: string;
public image?: string;
static get properties(): PropertyDeclarations {
return {
value: {},
icon: {},
label: {},
description: {},
image: {},
};
}
protected render(): TemplateResult {
return html`
${this.renderStyle()}
<div class="badge-container">
<div class="label-badge" id="badge">
<div
class="${
classMap({
value: true,
big: Boolean(this.value && this.value.length > 4),
})
}"
>
${
this.icon && !this.value && !this.image
? html`
<ha-icon .icon="${this.icon}"></ha-icon>
`
: ""
}
${
this.value && !this.image
? html`
<span>${this.value}</span>
`
: ""
}
</div>
${
this.label
? html`
<div
class="${
classMap({ label: true, big: this.label.length > 5 })
}"
>
<span>${this.label}</span>
</div>
`
: ""
}
</div>
${
this.description
? html`
<div class="title">${this.description}</div>
`
: ""
}
</div>
`;
}
protected renderStyle(): TemplateResult {
return html`
<style>
.badge-container {
@ -74,69 +148,25 @@ class HaLabelBadge extends PolymerElement {
text-overflow: ellipsis;
line-height: normal;
}
[hidden] {
display: none !important;
}
</style>
<div class="badge-container">
<div class="label-badge" id="badge">
<div class$="[[computeValueClasses(value)]]">
<ha-icon
icon="[[icon]]"
hidden$="[[computeHideIcon(icon, value, image)]]"
></ha-icon>
<span hidden$="[[computeHideValue(value, image)]]">[[value]]</span>
</div>
<div
hidden$="[[computeHideLabel(label)]]"
class$="[[computeLabelClasses(label)]]"
>
<span>[[label]]</span>
</div>
</div>
<div class="title" hidden$="[[!description]]">[[description]]</div>
</div>
`;
}
static get properties() {
return {
value: String,
icon: String,
label: String,
description: String,
image: {
type: String,
observer: "imageChanged",
},
};
}
computeValueClasses(value) {
return value && value.length > 4 ? "value big" : "value";
}
computeLabelClasses(label) {
return label && label.length > 5 ? "label big" : "label";
}
computeHideLabel(label) {
return !label || !label.trim();
}
computeHideIcon(icon, value, image) {
return !icon || value || image;
}
computeHideValue(value, image) {
return !value || image;
}
imageChanged(newVal) {
this.$.badge.style.backgroundImage = newVal ? "url(" + newVal + ")" : "";
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("image")) {
this.shadowRoot!.getElementById("badge")!.style.backgroundImage = this
.image
? `url(${this.image})`
: "";
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-label-badge": HaLabelBadge;
}
}
customElements.define("ha-label-badge", HaLabelBadge);

View File

@ -0,0 +1,18 @@
import { HomeAssistant } from "../types";
export const callAlarmAction = (
hass: HomeAssistant,
entity: string,
action:
| "arm_away"
| "arm_home"
| "arm_night"
| "arm_custom_bypass"
| "disarm",
code: string
) => {
hass!.callService("alarm_control_panel", "alarm_" + action, {
entity_id: entity,
code,
});
};

View File

@ -3,6 +3,9 @@ import { HomeAssistant } from "../types";
export interface LovelaceConfig {
title?: string;
views: LovelaceViewConfig[];
background?: string;
resources?: Array<{ type: "css" | "js" | "module" | "html"; url: string }>;
excluded_entities?: string[];
}
export interface LovelaceViewConfig {
@ -13,6 +16,8 @@ export interface LovelaceViewConfig {
path?: string;
icon?: string;
theme?: string;
panel?: boolean;
background?: string;
}
export interface LovelaceCardConfig {

View File

@ -1,15 +1,18 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import LocalizeMixin from "../../../mixins/localize-mixin";
class MoreInfoScript extends PolymerElement {
class MoreInfoScript extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex iron-flex-alignment"></style>
<div class="layout vertical">
<div class="data-entry layout justified horizontal">
<div class="key">Last Action</div>
<div class="key">
[[localize('ui.dialogs.more_info_control.script.last_action')]]
</div>
<div class="value">[[stateObj.attributes.last_action]]</div>
</div>
</div>

View File

@ -28,7 +28,9 @@ class MoreInfoSun extends LocalizeMixin(PolymerElement) {
</div>
</template>
<div class="data-entry layout justified horizontal">
<div class="key">Elevation</div>
<div class="key">
[[localize('ui.dialogs.more_info_control.sun.elevation')]]
</div>
<div class="value">[[stateObj.attributes.elevation]]</div>
</div>
`;
@ -63,7 +65,10 @@ class MoreInfoSun extends LocalizeMixin(PolymerElement) {
}
itemCaption(type) {
return type === "ris" ? "Rising " : "Setting ";
if (type === "ris") {
return this.localize("ui.dialogs.more_info_control.sun.rising");
}
return this.localize("ui.dialogs.more_info_control.sun.setting");
}
itemDate(type) {

View File

@ -1,7 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import LocalizeMixin from "../../../mixins/localize-mixin";
class MoreInfoUpdater extends PolymerElement {
class MoreInfoUpdater extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style>
@ -15,7 +16,7 @@ class MoreInfoUpdater extends PolymerElement {
class="link"
href="https://www.home-assistant.io/docs/installation/updating/"
target="_blank"
>Update Instructions</a
>[[localize('ui.dialogs.more_info_control.updater.title')]]</a
>
</div>
`;

View File

@ -26,7 +26,7 @@ const isExternal = location.search.includes("external_auth=1");
const authProm = isExternal
? () =>
import("../common/auth/external_auth").then(
import(/* webpackChunkName: "external_auth" */ "../common/auth/external_auth").then(
(mod) => new mod.default(hassUrl)
)
: () =>

View File

@ -37,13 +37,15 @@ export const hassLocalizeLitMixin = <T extends LitElement>(
public connectedCallback(): void {
super.connectedCallback();
let language;
let resources;
if (this.hass) {
language = this.hass.language;
resources = this.hass.resources;
if (this.localize === empty) {
let language;
let resources;
if (this.hass) {
language = this.hass.language;
resources = this.hass.resources;
}
this.localize = this.__computeLocalize(language, resources);
}
this.localize = this.__computeLocalize(language, resources);
}
public updated(changedProperties: PropertyValues) {

View File

@ -1,4 +1,5 @@
import IntlMessageFormat from "intl-messageformat/src/main";
import { HomeAssistant } from "../types";
/**
* Adapted from Polymer app-localize-behavior.
@ -32,6 +33,7 @@ export interface FormatsType {
export type LocalizeFunc = (key: string, ...args: any[]) => string;
export interface LocalizeMixin {
hass?: HomeAssistant;
localize: LocalizeFunc;
}

View File

@ -169,7 +169,8 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
fireEvent(this, "register-dialog", {
dialogShowEvent: "manage-cloud-webhook",
dialogTag: "cloud-webhook-manage-dialog",
dialogImport: () => import("./cloud-webhook-manage-dialog"),
dialogImport: () =>
import(/* webpackChunkName: "cloud-webhook-manage-dialog" */ "./cloud-webhook-manage-dialog"),
});
}
}

View File

@ -172,7 +172,8 @@ class HaConfigManagerDashboard extends LocalizeMixin(
this.fire("register-dialog", {
dialogShowEvent: "show-config-flow",
dialogTag: "ha-config-flow",
dialogImport: () => import("./ha-config-flow"),
dialogImport: () =>
import(/* webpackChunkName: "ha-config-flow" */ "./ha-config-flow"),
});
}
}

View File

@ -50,7 +50,7 @@ class HaConfigNavigation extends LocalizeMixin(NavigateMixin(PolymerElement)) {
pages: {
type: Array,
value: ["core", "customize", "automation", "script", "zwave"],
value: ["core", "customize", "automation", "script", "zha", "zwave"],
},
};
}

View File

@ -17,6 +17,7 @@ import(/* webpackChunkName: "panel-config-customize" */ "./customize/ha-config-c
import(/* webpackChunkName: "panel-config-dashboard" */ "./dashboard/ha-config-dashboard");
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script");
import(/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users");
import(/* webpackChunkName: "panel-config-zha" */ "./zha/ha-config-zha");
import(/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave");
/*
@ -106,6 +107,18 @@ class HaPanelConfig extends EventsMixin(NavigateMixin(PolymerElement)) {
></ha-config-script>
</template>
<template
is="dom-if"
if="[[_equals(_routeData.page, &quot;zha&quot;)]]"
restamp
>
<ha-config-zha
page-name="zha"
hass="[[hass]]"
is-wide="[[isWide]]"
></ha-config-zha>
</template>
<template
is="dom-if"
if="[[_equals(_routeData.page, &quot;zwave&quot;)]]"

View File

@ -90,7 +90,8 @@ class HaUserPicker extends EventsMixin(
this.fire("register-dialog", {
dialogShowEvent: "show-add-user",
dialogTag: "ha-dialog-add-user",
dialogImport: () => import("./ha-dialog-add-user"),
dialogImport: () =>
import(/* webpackChunkName: "ha-dialog-add-user" */ "./ha-dialog-add-user"),
});
}
}

View File

@ -0,0 +1,80 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { HomeAssistant } from "../../../types";
import "../../../layouts/ha-app-layout";
import "../../../resources/ha-style";
import "./zha-network";
export class HaConfigZha extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
private _haStyle?: DocumentFragment;
private _ironFlex?: DocumentFragment;
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
};
}
protected render(): TemplateResult {
return html`
${this.renderStyle()}
<ha-app-layout has-scrolling-region="">
<app-header slot="header" fixed="">
<app-toolbar>
<paper-icon-button
icon="hass:arrow-left"
@click="${this._onBackTapped}"
></paper-icon-button>
</app-toolbar>
</app-header>
<zha-network
id="zha-network"
.is-wide="${this.isWide}"
.hass="${this.hass}"
></zha-network>
</ha-app-layout>
`;
}
private renderStyle(): TemplateResult {
if (!this._haStyle) {
this._haStyle = document.importNode(
(document.getElementById("ha-style")!
.children[0] as HTMLTemplateElement).content,
true
);
}
if (!this._ironFlex) {
this._ironFlex = document.importNode(
(document.getElementById("iron-flex")!
.children[0] as HTMLTemplateElement).content,
true
);
}
return html`
${this._ironFlex} ${this._haStyle}
`;
}
private _onBackTapped(): void {
history.back();
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-zha": HaConfigZha;
}
}
customElements.define("ha-config-zha", HaConfigZha);

View File

@ -0,0 +1,129 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-button/paper-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-card/paper-card";
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import "../ha-config-section";
import { HomeAssistant } from "../../../types";
import "../../../resources/ha-style";
export class ZHANetwork extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
public showDescription: boolean;
private _haStyle?: DocumentFragment;
private _ironFlex?: DocumentFragment;
constructor() {
super();
this.showDescription = false;
}
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
showDescription: {},
};
}
protected render(): TemplateResult {
return html`
${this.renderStyle()}
<ha-config-section .is-wide="${this.isWide}">
<div style="position: relative" slot="header">
<span>Zigbee Home Automation network management</span>
<paper-icon-button class="toggle-help-icon" @click="${
this._onHelpTap
}" icon="hass:help-circle"></paper-icon-button>
</div>
<span slot="introduction">Commands that affect entire network</span>
<paper-card class="content">
<div class="card-actions">
<ha-call-service-button .hass="${
this.hass
}" domain="zha" service="permit">Permit</ha-call-service-button>
${
this.showDescription
? html`
<ha-service-description
.hass="${this.hass}"
domain="zha"
service="permit"
/>
`
: ""
}
</paper-card>
</ha-config-section>
`;
}
private _onHelpTap(): void {
this.showDescription = !this.showDescription;
}
private renderStyle(): TemplateResult {
if (!this._haStyle) {
this._haStyle = document.importNode(
(document.getElementById("ha-style")!
.children[0] as HTMLTemplateElement).content,
true
);
}
if (!this._ironFlex) {
this._ironFlex = document.importNode(
(document.getElementById("iron-flex")!
.children[0] as HTMLTemplateElement).content,
true
);
}
return html`
${this._ironFlex} ${this._haStyle}
<style>
.content {
margin-top: 24px;
}
paper-card {
display: block;
margin: 0 auto;
max-width: 600px;
}
.card-actions.warning ha-call-service-button {
color: var(--google-red-500);
}
.toggle-help-icon {
position: absolute;
top: -6px;
right: 0;
color: var(--primary-color);
}
ha-service-description {
display: block;
color: grey;
}
[hidden] {
display: none;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-network": ZHANetwork;
}
}
customElements.define("zha-network", ZHANetwork);

View File

@ -108,6 +108,16 @@ class ZwaveGroups extends PolymerElement {
Remove From Group
</ha-call-service-button>
</template>
<template is="dom-if" if="[[_isBroadcastNodeInGroup]]">
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="change_association"
service-data="[[_removeBroadcastNodeServiceData]]"
>
Remove Broadcast
</ha-call-service-button>
</template>
</div>
</template>
</paper-card>
@ -165,6 +175,16 @@ class ZwaveGroups extends PolymerElement {
type: String,
value: "",
},
_removeBroadcastNodeServiceData: {
type: String,
value: "",
},
_isBroadcastNodeInGroup: {
type: Boolean,
value: false,
},
};
}
@ -201,6 +221,7 @@ class ZwaveGroups extends PolymerElement {
_computeOtherGroupNodes(selectedGroup) {
if (selectedGroup === -1) return -1;
this.setProperties({ _isBroadcastNodeInGroup: false });
const associations = Object.values(
this.groups[selectedGroup].value.association_instances
);
@ -212,6 +233,17 @@ class ZwaveGroups extends PolymerElement {
const id = assoc[0];
const instance = assoc[1];
const node = this.nodes.find((n) => n.attributes.node_id === id);
if (id === 255) {
this.setProperties({
_isBroadcastNodeInGroup: true,
_removeBroadcastNodeServiceData: {
node_id: this.nodes[this.selectedNode].attributes.node_id,
association: "remove",
target_node_id: 255,
group: this.groups[selectedGroup].key,
},
});
}
if (!node) {
return `Unknown Node (${id}: (${instance} ? ${id}.${instance} : ${id}))`;
}
@ -288,6 +320,7 @@ class ZwaveGroups extends PolymerElement {
_otherGroupNodes: Object.values(
groupData[this._selectedGroup].value.associations
),
_isBroadcastNodeInGroup: false,
});
const oldGroup = this._selectedGroup;
this.setProperties({ _selectedGroup: -1 });

View File

@ -133,7 +133,8 @@ class OzwLog extends EventsMixin(PolymerElement) {
this.fire("register-dialog", {
dialogShowEvent: "show-ozwlog-dialog",
dialogTag: "zwave-log-dialog",
dialogImport: () => import("./zwave-log-dialog"),
dialogImport: () =>
import(/* webpackChunkName: "zwave-log-dialog" */ "./zwave-log-dialog"),
});
}
}

View File

@ -312,7 +312,8 @@ class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) {
this.fire("register-dialog", {
dialogShowEvent: "show-loaded-components",
dialogTag: "ha-loaded-components",
dialogImport: () => import("./ha-loaded-components"),
dialogImport: () =>
import(/* webpackChunkName: "ha-loaded-components" */ "./ha-loaded-components"),
});
}

View File

@ -28,6 +28,7 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
.content {
padding: 16px;
direction: ltr;
}
ha-entity-picker,

View File

@ -1,258 +0,0 @@
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.name
? this._config.name
: 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

@ -0,0 +1,316 @@
import {
html,
LitElement,
PropertyValues,
PropertyDeclarations,
} from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { classMap } from "lit-html/directives/classMap";
import { LovelaceCard } from "../types";
import { HomeAssistant } from "../../../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { callAlarmAction } from "../../../data/alarm_control_panel";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import "../../../components/ha-card";
import "../../../components/ha-label-badge";
import {
createErrorCardConfig,
createErrorCardElement,
} from "./hui-error-card";
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",
};
const BUTTONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "clear"];
export interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
states?: string[];
}
class HuiAlarmPanelCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement() {
await import(/* webpackChunkName: "hui-alarm-panel-card-editor" */ "../editor/config-elements/hui-alarm-panel-card-editor");
return document.createElement("hui-alarm-panel-card-editor");
}
public static getStubConfig() {
return { states: ["arm_home", "arm_away"] };
}
public hass?: HomeAssistant;
private _config?: Config;
private _code?: string;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
_code: {},
};
}
public getCardSize(): number {
return 4;
}
public setConfig(config: Config): void {
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._code = "";
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("_config") || changedProps.has("_code")) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (oldHass) {
return (
oldHass.states[this._config!.entity] !==
this.hass!.states[this._config!.entity]
);
}
return true;
}
protected render(): TemplateResult {
if (!this._config || !this.hass) {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
const element = createErrorCardElement(
createErrorCardConfig("Entity not Found!", this._config)
);
return html`
${element}
`;
}
return html`
${this.renderStyle()}
<ha-card .header="${this._config.name || this._label(stateObj.state)}">
<ha-label-badge
class="${classMap({ [stateObj.state]: true })}"
.icon="${ICONS[stateObj.state] || "hass:shield-outline"}"
.label="${this._stateIconLabel(stateObj.state)}"
></ha-label-badge>
<div id="armActions" class="actions">
${
(stateObj.state === "disarmed"
? this._config.states!
: ["disarm"]
).map((state) => {
return html`
<paper-button
noink
raised
.action="${state}"
@click="${this._handleActionClick}"
>${this._label(state)}</paper-button
>
`;
})
}
</div>
${
!stateObj.attributes.code_format
? html``
: html`
<paper-input
label="Alarm Code"
type="password"
.value="${this._code}"
></paper-input>
`
}
${
stateObj.attributes.code_format !== "Number"
? html``
: html`
<div id="keypad">
${
BUTTONS.map((value) => {
return value === ""
? html`
<paper-button disabled></paper-button>
`
: html`
<paper-button
noink
raised
.value="${value}"
@click="${this._handlePadClick}"
>${
value === "clear"
? this._label("clear_code")
: value
}</paper-button
>
`;
})
}
</div>
`
}
</ha-card>
`;
}
private _stateIconLabel(state: string): string {
const stateLabel = state.split("_").pop();
return stateLabel === "disarmed" ||
stateLabel === "triggered" ||
!stateLabel
? ""
: stateLabel;
}
private _label(state: string): string {
return (
this.localize(`state.alarm_control_panel.${state}`) ||
this.localize(`ui.card.alarm_control_panel.${state}`)
);
}
private _handlePadClick(e: MouseEvent): void {
const val = (e.currentTarget! as any).value;
this._code = val === "clear" ? "" : this._code + val;
}
private _handleActionClick(e: MouseEvent): void {
callAlarmAction(
this.hass!,
this._config!.entity_id,
(e.currentTarget! as any).action,
this._code!
);
this._code = "";
}
private renderStyle(): TemplateResult {
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, 0.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);
--label-badge-background-color: var(--paper-card-background-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: 0 auto 8px;
max-width: 150px;
font-size: calc(var(--base-unit));
text-align: center;
}
.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;
flex-wrap: wrap;
margin: auto;
width: 300px;
}
#keypad paper-button {
margin-bottom: 5%;
width: 30%;
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);
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-alarm-panel-card": HuiAlarmPanelCard;
}
}
customElements.define("hui-alarm-panel-card", HuiAlarmPanelCard);

View File

@ -1,4 +1,4 @@
import createCardElement from "../common/create-card-element";
import { createCardElement } from "../common/create-card-element";
import { computeCardSize } from "../common/compute-card-size";
import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types";

View File

@ -17,7 +17,7 @@ import { EntityConfig, EntityRow } from "../entity-rows/types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { processConfigEntities } from "../common/process-config-entities";
import createRowElement from "../common/create-row-element";
import { createRowElement } from "../common/create-row-element";
import computeDomain from "../../../common/entity/compute_domain";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
@ -40,7 +40,7 @@ export interface Config extends LovelaceCardConfig {
class HuiEntitiesCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import("../editor/config-elements/hui-entities-card-editor");
await import(/* webpackChunkName: "hui-entities-card-editor" */ "../editor/config-elements/hui-entities-card-editor");
return document.createElement("hui-entities-card-editor");
}

View File

@ -17,12 +17,12 @@ import computeStateName from "../../../common/entity/compute_state_name";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { HomeAssistant, LightEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { longPress } from "../common/directives/long-press-directive";
import { handleClick } from "../common/handle-click";
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
icon?: string;
@ -33,6 +33,18 @@ interface Config extends LovelaceCardConfig {
class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-entity-button-card-editor" */ "../editor/config-elements/hui-entity-button-card-editor");
return document.createElement("hui-entity-button-card-editor");
}
public static getStubConfig(): object {
return {
tap_action: { action: "more-info" },
hold_action: { action: "none" },
};
}
public hass?: HomeAssistant;
private _config?: Config;

View File

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

View File

@ -3,13 +3,27 @@ import { html, LitElement } from "@polymer/lit-element";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
interface Config extends LovelaceCardConfig {
error: string;
origConfig: LovelaceCardConfig;
}
class HuiErrorCard extends LitElement implements LovelaceCard {
export const createErrorCardElement = (config) => {
const el = document.createElement("hui-error-card");
el.setConfig(config);
return el;
};
export const createErrorCardConfig = (error, origConfig) => ({
type: "error",
error,
origConfig,
});
export class HuiErrorCard extends LitElement implements LovelaceCard {
public hass?: HomeAssistant;
private _config?: Config;
static get properties() {

View File

@ -5,30 +5,40 @@ import {
PropertyValues,
} from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { styleMap } from "lit-html/directives/styleMap";
import { LovelaceCard } from "../types";
import "../../../components/ha-card";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import isValidEntityId from "../../../common/entity/valid_entity_id";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import computeStateName from "../../../common/entity/compute_state_name";
import "../../../components/ha-card";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import {
createErrorCardConfig,
createErrorCardElement,
} from "./hui-error-card";
interface Config extends LovelaceCardConfig {
export interface SeverityConfig {
green?: number;
yellow?: number;
red?: number;
}
export interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
unit?: string;
min?: number;
max?: number;
severity?: object;
severity?: SeverityConfig;
theme?: string;
}
const severityMap = {
export const severityMap = {
red: "var(--label-badge-red)",
green: "var(--label-badge-green)",
yellow: "var(--label-badge-yellow)",
@ -36,6 +46,14 @@ const severityMap = {
};
class HuiGaugeCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-gauge-card-editor" */ "../editor/config-elements/hui-gauge-card-editor");
return document.createElement("hui-gauge-card-editor");
}
public static getStubConfig(): object {
return {};
}
public hass?: HomeAssistant;
private _config?: Config;
@ -65,11 +83,23 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
let state;
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;
} else {
state = Number(stateObj.state);
if (isNaN(state)) {
error = "Entity is non-numeric: " + this._config.entity;
}
}
if (error) {
return html`
${createErrorCardElement(createErrorCardConfig(error, this._config))}
`;
}
return html`
@ -84,7 +114,15 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
<div class="container">
<div class="gauge-a"></div>
<div class="gauge-b"></div>
<div class="gauge-c" id="gauge"></div>
<div
class="gauge-c"
style="${
styleMap({
transform: `rotate(${this._translateTurn(state)}turn)`,
"background-color": this._computeSeverity(state),
})
}"
></div>
<div class="gauge-data">
<div id="percent">
${stateObj.state}
@ -109,38 +147,74 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
return hasConfigOrEntityChanged(this, changedProps);
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass) {
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!
)}`;
protected firstUpdated(): void {
(this.shadowRoot!.querySelector(
"ha-card"
)! as HTMLElement).style.setProperty(
"--base-unit",
this._computeBaseUnit()
);
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes !== this.hass.themes) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}
private _computeSeverity(numberValue: number): string {
const sections = this._config!.severity;
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): number {
const { min, max } = this._config!;
const maxTurnValue = Math.min(Math.max(value, min!), max!);
return (5 * (maxTurnValue - min!)) / (max! - 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 });
}
private renderStyle(): TemplateResult {
return html`
<style>
@ -223,53 +297,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
</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 {

View File

@ -41,7 +41,7 @@ export interface Config extends LovelaceCardConfig {
export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import("../editor/config-elements/hui-glance-card-editor");
await import(/* webpackChunkName: "hui-glance-card-editor" */ "../editor/config-elements/hui-glance-card-editor");
return document.createElement("hui-glance-card-editor");
}
public static getStubConfig(): object {

View File

@ -2,18 +2,26 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "../../../components/ha-card";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { TemplateResult } from "lit-html";
import { styleMap } from "lit-html/directives/styleMap";
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
aspect_ratio?: string;
title?: string;
url: string;
}
export class HuiIframeCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-iframe-card-editor" */ "../editor/config-elements/hui-iframe-card-editor");
return document.createElement("hui-iframe-card-editor");
}
public static getStubConfig(): object {
return { url: "https://www.home-assistant.io", aspect_ratio: "50%" };
}
protected _config?: Config;
static get properties(): PropertyDeclarations {

View File

@ -1,4 +1,4 @@
import createErrorCardConfig from "../common/create-error-card-config";
import { createErrorCardConfig } from "./hui-error-card";
import computeDomain from "../../../common/entity/compute_domain";
export default class LegacyWrapperCard extends HTMLElement {

View File

@ -10,7 +10,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { styleMap } from "lit-html/directives/styleMap";
import { HomeAssistant, LightEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { longPress } from "../common/directives/long-press-directive";
import { hasConfigOrEntityChanged } from "../common/has-changed";
@ -39,7 +39,7 @@ const lightConfig = {
animation: false,
};
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
theme?: string;
@ -47,6 +47,14 @@ interface Config extends LovelaceCardConfig {
export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-light-card-editor" */ "../editor/config-elements/hui-light-card-editor");
return document.createElement("hui-light-card-editor");
}
public static getStubConfig(): object {
return {};
}
public hass?: HomeAssistant;
private _config?: Config;
private _brightnessTimout?: number;

View File

@ -11,7 +11,24 @@ import computeStateName from "../../../common/entity/compute_state_name";
import debounce from "../../../common/util/debounce";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
// should be interface when converted to TS
export const Config = {
title: "",
aspect_ratio: "",
default_zoom: 14,
entities: [],
};
class HuiMapCard extends PolymerElement {
static async getConfigElement() {
await import(/* webpackChunkName: "hui-map-card-editor" */ "../editor/config-elements/hui-map-card-editor");
return document.createElement("hui-map-card-editor");
}
static getStubConfig() {
return { entities: [] };
}
static get template() {
return html`
<style>
@ -111,8 +128,24 @@ class HuiMapCard extends PolymerElement {
throw new Error("Error in card configuration.");
}
this._configEntities = processConfigEntities(config.entities);
if (!config.entities && !config.geo_location_sources) {
throw new Error(
"Either entities or geo_location_sources must be defined"
);
}
if (config.entities && !Array.isArray(config.entities)) {
throw new Error("Entities need to be an array");
}
if (
config.geo_location_sources &&
!Array.isArray(config.geo_location_sources)
) {
throw new Error("Geo_location_sources needs to be an array");
}
this._config = config;
this._configGeoLocationSources = config.geo_location_sources;
this._configEntities = config.entities;
}
getCardSize() {
@ -205,7 +238,24 @@ class HuiMapCard extends PolymerElement {
}
const mapItems = (this._mapItems = []);
this._configEntities.forEach((entity) => {
let allEntities = [];
if (this._configEntities) {
allEntities = allEntities.concat(this._configEntities);
}
if (this._configGeoLocationSources) {
Object.keys(this.hass.states).forEach((entityId) => {
const stateObj = this.hass.states[entityId];
if (
computeStateDomain(stateObj) === "geo_location" &&
this._configGeoLocationSources.includes(stateObj.attributes.source)
) {
allEntities.push(entityId);
}
});
}
allEntities = processConfigEntities(allEntities);
allEntities.forEach((entity) => {
const entityId = entity.entity;
if (!(entityId in hass.states)) {
return;

View File

@ -4,16 +4,24 @@ import { classMap } from "lit-html/directives/classMap";
import "../../../components/ha-card";
import "../../../components/ha-markdown";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { TemplateResult } from "lit-html";
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
content: string;
title?: string;
}
export class HuiMarkdownCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-markdown-card-editor" */ "../editor/config-elements/hui-markdown-card-editor");
return document.createElement("hui-markdown-card-editor");
}
public static getStubConfig(): object {
return { content: " " };
}
private _config?: Config;
static get properties(): PropertyDeclarations {

View File

@ -2,7 +2,21 @@ import "../../../cards/ha-media_player-card";
import LegacyWrapperCard from "./hui-legacy-wrapper-card";
// should be interface when converted to TS
export const Config = {
entity: "",
};
class HuiMediaControlCard extends LegacyWrapperCard {
static async getConfigElement() {
await import(/* webpackChunkName: "hui-media-control-card-editor" */ "../editor/config-elements/hui-media-control-card-editor");
return document.createElement("hui-media-control-card-editor");
}
static getStubConfig() {
return {};
}
constructor() {
super("ha-media_player-card", "media_player");
}

View File

@ -2,7 +2,7 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "../../../components/ha-card";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { TemplateResult } from "lit-html";
@ -10,20 +10,31 @@ import { classMap } from "lit-html/directives/classMap";
import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
image?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
}
export class HuiPictureCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-picture-card-editor" */ "../editor/config-elements/hui-picture-card-editor");
return document.createElement("hui-picture-card-editor");
}
public static getStubConfig(): object {
return {
image:
"https://www.home-assistant.io/images/merchandise/shirt-frontpage.png",
tap_action: { action: "none" },
hold_action: { action: "none" },
};
}
public hass?: HomeAssistant;
protected _config?: Config;
static get properties(): PropertyDeclarations {
return {
_config: {},
};
return { _config: {} };
}
public getCardSize(): number {

View File

@ -1,7 +1,7 @@
import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import createHuiElement from "../common/create-hui-element";
import { createHuiElement } from "../common/create-hui-element";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";

View File

@ -16,6 +16,10 @@ import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { LovelaceCard } from "../types";
import { handleClick } from "../common/handle-click";
import { UNAVAILABLE } from "../../../data/entity";
import {
createErrorCardElement,
createErrorCardConfig,
} from "./hui-error-card";
interface Config extends LovelaceCardConfig {
entity: string;
@ -62,11 +66,25 @@ class HuiPictureEntityCard extends hassLocalizeLitMixin(LitElement)
}
protected render(): TemplateResult {
if (!this._config || !this.hass || !this.hass.states[this._config.entity]) {
if (!this._config || !this.hass) {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
return html`
${
createErrorCardElement(
createErrorCardConfig(
`Entity not found: ${this._config.entity}`,
this._config
)
)
}
`;
}
const name = this._config.name || computeStateName(stateObj);
const state = computeStateDisplay(
this.localize,

View File

@ -2,7 +2,22 @@ import "../../../cards/ha-plant-card";
import LegacyWrapperCard from "./hui-legacy-wrapper-card";
// should be interface when converted to TS
export const Config = {
name: "",
entity: "",
};
class HuiPlantStatusCard extends LegacyWrapperCard {
static async getConfigElement() {
await import(/* webpackChunkName: "hui-plant-status-card-editor" */ "../editor/config-elements/hui-plant-status-card-editor");
return document.createElement("hui-plant-status-card-editor");
}
static getStubConfig() {
return {};
}
constructor() {
super("ha-plant-card", "plant");
}

View File

@ -8,7 +8,7 @@ import {
import { TemplateResult } from "lit-html";
import "@polymer/paper-spinner/paper-spinner";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
@ -133,7 +133,7 @@ const coordinates = (
return calcPoints(history, hours, width, detail, min, max);
};
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
icon?: string;
@ -145,6 +145,15 @@ interface Config extends LovelaceCardConfig {
}
class HuiSensorCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-sensor-card-editor" */ "../editor/config-elements/hui-sensor-card-editor");
return document.createElement("hui-sensor-card-editor");
}
public static getStubConfig(): object {
return {};
}
public hass?: HomeAssistant;
private _config?: Config;
private _history?: any;

View File

@ -9,7 +9,7 @@ import "../../../components/ha-icon";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import {
fetchItems,
@ -19,12 +19,20 @@ import {
addItem,
} from "../../../data/shopping-list";
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
title?: string;
}
class HuiShoppingListCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-shopping-list-editor" */ "../editor/config-elements/hui-shopping-list-editor");
return document.createElement("hui-shopping-list-card-editor");
}
public static getStubConfig(): object {
return {};
}
public hass?: HomeAssistant;
private _config?: Config;
private _uncheckedItems?: ShoppingListItem[];
@ -33,6 +41,7 @@ class HuiShoppingListCard extends hassLocalizeLitMixin(LitElement)
static get properties() {
return {
hass: {},
_config: {},
_uncheckedItems: {},
_checkedItems: {},
@ -117,7 +126,7 @@ class HuiShoppingListCard extends hassLocalizeLitMixin(LitElement)
<paper-item-body>
<paper-input
no-label-float
value="${item.name}"
.value="${item.name}"
.itemId="${item.id}"
@change="${this._saveEdit}"
></paper-input>
@ -168,7 +177,7 @@ class HuiShoppingListCard extends hassLocalizeLitMixin(LitElement)
<paper-item-body>
<paper-input
no-label-float
value="${item.name}"
.value="${item.name}"
.itemId="${item.id}"
@change="${this._saveEdit}"
></paper-input>

View File

@ -1,7 +1,7 @@
import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import createCardElement from "../common/create-card-element";
import { createCardElement } from "../common/create-card-element";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";

View File

@ -7,22 +7,23 @@ import {
import { classMap } from "lit-html/directives/classMap";
import { TemplateResult } from "lit-html";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import computeStateName from "../../../common/entity/compute_state_name";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { HomeAssistant, ClimateEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import { loadRoundslider } from "../../../resources/jquery.roundslider.ondemand";
import { afterNextRender } from "../../../common/util/render-status";
import { UNIT_F } from "../../../common/const";
const thermostatConfig = {
radius: 150,
step: 1,
circleShape: "pie",
startAngle: 315,
width: 5,
@ -44,7 +45,7 @@ const modeIcons = {
idle: "hass:power-sleep",
};
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
entity: string;
theme?: string;
name?: string;
@ -54,12 +55,30 @@ function formatTemp(temps: string[]): string {
return temps.filter(Boolean).join("-");
}
function computeTemperatureStepSize(hass: HomeAssistant, config: Config) {
const stateObj = hass.states[config.entity];
if (stateObj.attributes.target_temp_step) {
return stateObj.attributes.target_temp_step;
}
return hass.config.unit_system.temperature === UNIT_F ? 1 : 0.5;
}
export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-thermostat-card-editor" */ "../editor/config-elements/hui-thermostat-card-editor");
return document.createElement("hui-thermostat-card-editor");
}
public static getStubConfig(): object {
return { entity: "" };
}
public hass?: HomeAssistant;
private _config?: Config;
private _roundSliderStyle?: TemplateResult;
private _jQuery?: any;
private _broadCard?: boolean;
static get properties(): PropertyDeclarations {
return {
@ -87,7 +106,6 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
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";
@ -96,8 +114,8 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
<ha-card
class="${classMap({
[mode]: true,
large: broadCard,
small: !broadCard,
large: this._broadCard!,
small: !this._broadCard,
})}">
<div id="root">
<div id="thermostat"></div>
@ -138,8 +156,49 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
return hasConfigOrEntityChanged(this, changedProps);
}
protected async firstUpdated(): Promise<void> {
protected firstUpdated(): void {
this._initialLoad();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass || !changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes !== this.hass.themes) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
const stateObj = this.hass.states[this._config.entity] as ClimateEntity;
if (
this._jQuery &&
// If jQuery changed, we just rendered in firstUpdated
!changedProps.has("_jQuery") &&
(!oldHass || oldHass.states[this._config.entity] !== stateObj)
) {
const [sliderValue, uiValue] = this._genSliderValue(stateObj);
this._jQuery("#thermostat", this.shadowRoot).roundSlider({
value: sliderValue,
});
this._updateSetTemp(uiValue);
}
}
private async _initialLoad(): Promise<void> {
const radius = this.clientWidth / 3;
this._broadCard = this.clientWidth > 390;
(this.shadowRoot!.querySelector(
"#thermostat"
)! as HTMLElement).style.minHeight = radius * 2 + "px";
const loaded = await loadRoundslider();
await new Promise((resolve) => afterNextRender(resolve));
this._roundSliderStyle = loaded.roundSliderStyle;
this._jQuery = loaded.jQuery;
@ -152,26 +211,27 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
? "range"
: "min-range";
const [sliderValue, uiValue] = this._genSliderValue(stateObj);
const step = computeTemperatureStepSize(this.hass!, this._config!);
this._jQuery("#thermostat", this.shadowRoot).roundSlider({
...thermostatConfig,
radius: this.clientWidth / 3,
radius,
min: stateObj.attributes.min_temp,
max: stateObj.attributes.max_temp,
sliderType: _sliderType,
create: () => this._loaded(),
change: (value) => this._setTemperature(value),
drag: (value) => this._dragEvent(value),
value: sliderValue,
step,
});
this._updateSetTemp(uiValue);
}
protected updated(changedProps: PropertyValues): void {
if (!this._config || !this.hass || !this._jQuery) {
return;
}
const stateObj = this.hass.states[this._config.entity] as ClimateEntity;
let sliderValue;
let uiValue;
private _genSliderValue(stateObj: ClimateEntity): [string | number, string] {
let sliderValue: string | number;
let uiValue: string;
if (
stateObj.attributes.target_temp_low &&
@ -185,18 +245,73 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
String(stateObj.attributes.target_temp_high),
]);
} else {
sliderValue = uiValue = stateObj.attributes.temperature;
sliderValue = stateObj.attributes.temperature;
uiValue = "" + stateObj.attributes.temperature;
}
this._jQuery("#thermostat", this.shadowRoot).roundSlider({
value: sliderValue,
return [sliderValue, uiValue];
}
private _loaded(): void {
(this.shadowRoot!.querySelector(
"#thermostat"
)! as HTMLElement).style.minHeight = null;
}
private _updateSetTemp(value: string): void {
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = value;
}
private _dragEvent(e): void {
this._updateSetTemp(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,
});
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = uiValue;
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes !== this.hass.themes) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}
private renderStyle(): TemplateResult {
@ -374,60 +489,6 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
</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 {

View File

@ -2,7 +2,22 @@ import "../../../cards/ha-weather-card";
import LegacyWrapperCard from "./hui-legacy-wrapper-card";
// should be interface when converted to TS
export const Config = {
entity: "",
name: "",
};
class HuiWeatherForecastCard extends LegacyWrapperCard {
static async getConfigElement() {
await import(/* webpackChunkName: "hui-weather-forecast-card-editor" */ "../editor/config-elements/hui-weather-forecast-card-editor");
return document.createElement("hui-weather-forecast-card-editor");
}
static getStubConfig() {
return {};
}
constructor() {
super("ha-weather-card", "weather");
}

View File

@ -1,38 +0,0 @@
const EXCLUDED_DOMAINS = ["zone"];
function computeUsedEntities(config) {
const entities = new Set();
function addEntityId(entity) {
if (typeof entity === "string") {
entities.add(entity);
} else if (entity.entity) {
entities.add(entity.entity);
}
}
function addEntities(obj) {
if (obj.entity) addEntityId(obj.entity);
if (obj.entities) obj.entities.forEach((entity) => addEntityId(entity));
if (obj.card) addEntities(obj.card);
if (obj.cards) obj.cards.forEach((card) => addEntities(card));
if (obj.badges) obj.badges.forEach((badge) => addEntityId(badge));
}
config.views.forEach((view) => addEntities(view));
return entities;
}
export default function computeUnusedEntities(hass, config) {
const usedEntities = computeUsedEntities(config);
return Object.keys(hass.states)
.filter(
(entity) =>
!usedEntities.has(entity) &&
!(
config.excluded_entities && config.excluded_entities.includes(entity)
) &&
!EXCLUDED_DOMAINS.includes(entity.split(".", 1)[0])
)
.sort();
}

View File

@ -0,0 +1,54 @@
import { LovelaceConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
const EXCLUDED_DOMAINS = ["zone"];
const computeUsedEntities = (config) => {
const entities = new Set();
const addEntityId = (entity) => {
if (typeof entity === "string") {
entities.add(entity);
} else if (entity.entity) {
entities.add(entity.entity);
}
};
const addEntities = (obj) => {
if (obj.entity) {
addEntityId(obj.entity);
}
if (obj.entities) {
obj.entities.forEach((entity) => addEntityId(entity));
}
if (obj.card) {
addEntities(obj.card);
}
if (obj.cards) {
obj.cards.forEach((card) => addEntities(card));
}
if (obj.badges) {
obj.badges.forEach((badge) => addEntityId(badge));
}
};
config.views.forEach((view) => addEntities(view));
return entities;
};
export const computeUnusedEntities = (
hass: HomeAssistant,
config: LovelaceConfig
): string[] => {
const usedEntities = computeUsedEntities(config);
return Object.keys(hass.states)
.filter(
(entity) =>
!usedEntities.has(entity) &&
!(
config.excluded_entities && config.excluded_entities.includes(entity)
) &&
!EXCLUDED_DOMAINS.includes(entity.split(".", 1)[0])
)
.sort();
};

View File

@ -5,7 +5,11 @@ import "../cards/hui-conditional-card";
import "../cards/hui-entities-card";
import "../cards/hui-entity-button-card";
import "../cards/hui-entity-filter-card";
import "../cards/hui-error-card";
import {
createErrorCardElement,
createErrorCardConfig,
HuiErrorCard,
} from "../cards/hui-error-card";
import "../cards/hui-glance-card";
import "../cards/hui-history-graph-card";
import "../cards/hui-horizontal-stack-card";
@ -25,8 +29,8 @@ import "../cards/hui-shopping-list-card";
import "../cards/hui-thermostat-card";
import "../cards/hui-weather-forecast-card";
import "../cards/hui-gauge-card";
import createErrorCardConfig from "./create-error-card-config";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
const CARD_TYPES = new Set([
"alarm-panel",
@ -58,24 +62,29 @@ const CARD_TYPES = new Set([
const CUSTOM_TYPE_PREFIX = "custom:";
const TIMEOUT = 2000;
function _createElement(tag, config) {
const element = document.createElement(tag);
const _createElement = (
tag: string,
config: LovelaceCardConfig
): LovelaceCard | HuiErrorCard => {
const element = document.createElement(tag) as LovelaceCard;
try {
element.setConfig(config);
} catch (err) {
// eslint-disable-next-line
// tslint: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));
}
const _createErrorElement = (
error: string,
config: LovelaceCardConfig
): HuiErrorCard => createErrorCardElement(createErrorCardConfig(error, config));
export default function createCardElement(config) {
export const createCardElement = (
config: LovelaceCardConfig
): LovelaceCard | HuiErrorCard => {
if (!config || typeof config !== "object" || !config.type) {
return _createErrorElement("No card type configured.", config);
}
@ -111,4 +120,4 @@ export default function createCardElement(config) {
}
return _createElement(`hui-${config.type}-card`, config);
}
};

View File

@ -1,7 +0,0 @@
export default function createErrorConfig(error, origConfig) {
return {
type: "error",
error,
origConfig,
};
}

View File

@ -6,7 +6,12 @@ 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";
import {
createErrorCardElement,
createErrorCardConfig,
HuiErrorCard,
} from "../cards/hui-error-card";
import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
const CUSTOM_TYPE_PREFIX = "custom:";
const ELEMENT_TYPES = new Set([
@ -19,22 +24,25 @@ const ELEMENT_TYPES = new Set([
]);
const TIMEOUT = 2000;
function _createElement(tag, config) {
const element = document.createElement(tag);
const _createElement = (
tag: string,
config: LovelaceElementConfig
): LovelaceElement | HuiErrorCard => {
const element = document.createElement(tag) as LovelaceElement;
try {
element.setConfig(config);
} catch (err) {
// eslint-disable-next-line
// tslint: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));
}
const _createErrorElement = (
error: string,
config: LovelaceElementConfig
): HuiErrorCard => createErrorCardElement(createErrorCardConfig(error, config));
function _hideErrorElement(element) {
element.style.display = "None";
@ -43,7 +51,9 @@ function _hideErrorElement(element) {
}, TIMEOUT);
}
export default function createHuiElement(config) {
export const createHuiElement = (
config: LovelaceElementConfig
): LovelaceElement | HuiErrorCard => {
if (!config || typeof config !== "object" || !config.type) {
return _createErrorElement("No element type configured.", config);
}
@ -76,4 +86,4 @@ export default function createHuiElement(config) {
}
return _createElement(`hui-${config.type}-element`, config);
}
};

View File

@ -1,5 +1,10 @@
import { fireEvent } from "../../../common/dom/fire_event";
import {
createErrorCardElement,
createErrorCardConfig,
HuiErrorCard,
} from "../cards/hui-error-card";
import "../entity-rows/hui-climate-entity-row";
import "../entity-rows/hui-cover-entity-row";
import "../entity-rows/hui-group-entity-row";
@ -18,8 +23,7 @@ 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";
import { EntityConfig, EntityRow } from "../entity-rows/types";
const CUSTOM_TYPE_PREFIX = "custom:";
const SPECIAL_TYPES = new Set([
@ -51,32 +55,37 @@ const DOMAIN_TO_ELEMENT_TYPE = {
};
const TIMEOUT = 2000;
function _createElement(tag, config) {
const element = document.createElement(tag);
const _createElement = (
tag: string,
config: EntityConfig
): EntityRow | HuiErrorCard => {
const element = document.createElement(tag) as EntityRow;
try {
if ("setConfig" in element) element.setConfig(config);
element.setConfig(config);
} catch (err) {
// eslint-disable-next-line
// tslint: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));
}
const _createErrorElement = (
error: string,
config: EntityConfig
): HuiErrorCard => createErrorCardElement(createErrorCardConfig(error, config));
function _hideErrorElement(element) {
const _hideErrorElement = (element) => {
element.style.display = "None";
return window.setTimeout(() => {
element.style.display = "";
}, TIMEOUT);
}
};
export default function createRowElement(config) {
export const createRowElement = (
config: EntityConfig
): EntityRow | HuiErrorCard => {
let tag;
if (
@ -116,4 +125,4 @@ export default function createRowElement(config) {
tag = `hui-${DOMAIN_TO_ELEMENT_TYPE[domain] || "text"}-entity-row`;
return _createElement(tag, config);
}
};

View File

@ -42,6 +42,11 @@ const computeCards = (
type: "alarm-panel",
entity: entityId,
});
} else if (domain === "camera") {
cards.push({
type: "picture-entity",
entity: entityId,
});
} else if (domain === "climate") {
cards.push({
type: "thermostat",

View File

@ -0,0 +1,132 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "../../../components/ha-service-picker";
import { HomeAssistant } from "../../../types";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import { EditorTarget } from "../editor/types";
import {
ActionConfig,
NavigateActionConfig,
CallServiceActionConfig,
} from "../../../data/lovelace";
declare global {
// for fire event
interface HASSDomEvents {
"action-changed": undefined;
}
// for add event listener
interface HTMLElementEventMap {
"action-changed": HASSDomEvent<undefined>;
}
}
export class HuiActionEditor extends LitElement {
public config?: ActionConfig;
public label?: string;
public actions?: string[];
protected hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return { hass: {}, config: {}, label: {}, actions: {} };
}
get _action(): string {
return this.config!.action || "";
}
get _navigation_path(): string {
const config = this.config! as NavigateActionConfig;
return config.navigation_path || "";
}
get _service(): string {
const config = this.config! as CallServiceActionConfig;
return config.service || "";
}
protected render(): TemplateResult {
if (!this.hass || !this.actions) {
return html``;
}
return html`
<paper-dropdown-menu
.label="${this.label}"
.configValue="${"action"}"
@value-changed="${this._valueChanged}"
>
<paper-listbox
slot="dropdown-content"
.selected="${this.actions.indexOf(this._action)}"
>
${
this.actions.map((action) => {
return html`
<paper-item>${action}</paper-item>
`;
})
}
</paper-listbox>
</paper-dropdown-menu>
${
this._action === "navigate"
? html`
<paper-input
label="Navigation Path"
.value="${this._navigation_path}"
.configValue="${"navigation_path"}"
@value-changed="${this._valueChanged}"
></paper-input>
`
: ""
}
${
this.config && this.config.action === "call-service"
? html`
<ha-service-picker
.hass="${this.hass}"
.value="${this._service}"
.configValue="${"service"}"
@value-changed="${this._valueChanged}"
></ha-service-picker>
<h3>Toggle Editor to input Service Data</h3>
`
: ""
}
`;
}
private _valueChanged(ev: Event): void {
if (!this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (
this.config &&
this.config[this[`${target.configValue}`]] === target.value
) {
return;
}
if (target.configValue === "action") {
this.config = { action: "none" };
}
if (target.configValue) {
this.config = { ...this.config!, [target.configValue!]: target.value };
fireEvent(this, "action-changed");
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-action-editor": HuiActionEditor;
}
}
customElements.define("hui-action-editor", HuiActionEditor);

View File

@ -1,20 +1,23 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "@polymer/paper-button/paper-button";
import "@polymer/paper-menu-button/paper-menu-button";
import "@polymer/paper-icon-button/paper-icon-button";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import "@polymer/paper-listbox/paper-listbox";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { confDeleteCard } from "../editor/delete-card";
import { HomeAssistant } from "../../../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { Lovelace } from "../types";
import { swapCard } from "../editor/config-util";
import { showMoveCardViewDialog } from "../editor/card-editor/show-move-card-view-dialog";
export class HuiCardOptions extends hassLocalizeLitMixin(LitElement) {
public cardConfig?: LovelaceCardConfig;
protected hass?: HomeAssistant;
protected lovelace?: Lovelace;
protected path?: [number, number];
public hass?: HomeAssistant;
public lovelace?: Lovelace;
public path?: [number, number];
static get properties(): PropertyDeclarations {
return { hass: {}, lovelace: {}, path: {} };
@ -23,49 +26,93 @@ export class HuiCardOptions extends hassLocalizeLitMixin(LitElement) {
protected render() {
return html`
<style>
div {
div.options {
border-top: 1px solid #e8e8e8;
padding: 5px 16px;
padding: 5px 8px;
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.12) 0px 1px 5px -4px,
rgba(0, 0, 0, 0.2) 0px 3px 1px -2px;
display: flex;
}
div.options .primary-actions {
flex: 1;
margin: auto;
}
div.options .secondary-actions {
flex: 4;
text-align: right;
}
paper-button {
color: var(--primary-color);
font-weight: 500;
letter-spacing: 0.05em;
font-size: 16px;
padding: 0;
margin: 0;
}
paper-icon-button.delete {
paper-icon-button {
color: var(--primary-text-color);
}
paper-icon-button.move-arrow[disabled] {
color: var(--disabled-text-color);
}
paper-menu-button {
color: var(--secondary-text-color);
float: right;
padding: 0;
}
paper-item.header {
color: var(--primary-text-color);
text-transform: uppercase;
font-weight: 500;
font-size: 14px;
}
paper-item {
cursor: pointer;
}
</style>
<slot></slot>
<div>
<paper-button @click="${this._editCard}"
>${
this.localize("ui.panel.lovelace.editor.edit_card.edit")
}</paper-button
>
<paper-icon-button
icon="hass:arrow-up"
@click="${this._cardUp}"
?disabled="${this.path![1] === 0}"
></paper-icon-button>
<paper-icon-button
icon="hass:arrow-down"
@click="${this._cardDown}"
?disabled="${
this.lovelace!.config.views[this.path![0]].cards!.length ===
this.path![1] + 1
}"
></paper-icon-button>
<paper-icon-button
class="delete"
icon="hass:delete"
@click="${this._deleteCard}"
title="${this.localize("ui.panel.lovelace.editor.edit_card.delete")}"
></paper-icon-button>
<div class="options">
<div class="primary-actions">
<paper-button @click="${this._editCard}"
>${
this.localize("ui.panel.lovelace.editor.edit_card.edit")
}</paper-button
>
</div>
<div class="secondary-actions">
<paper-icon-button
title="Move card down"
class="move-arrow"
icon="hass:arrow-down"
@click="${this._cardDown}"
?disabled="${
this.lovelace!.config.views[this.path![0]].cards!.length ===
this.path![1] + 1
}"
></paper-icon-button>
<paper-icon-button
title="Move card up"
class="move-arrow"
icon="hass:arrow-up"
@click="${this._cardUp}"
?disabled="${this.path![1] === 0}"
></paper-icon-button>
<paper-menu-button>
<paper-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item @click="${this._moveCard}">Move Card</paper-item>
<paper-item @click="${this._deleteCard}"
>${
this.localize("ui.panel.lovelace.editor.edit_card.delete")
}</paper-item
>
</paper-listbox>
</paper-menu-button>
</div>
</div>
`;
}
@ -93,6 +140,13 @@ export class HuiCardOptions extends hassLocalizeLitMixin(LitElement) {
);
}
private _moveCard(): void {
showMoveCardViewDialog(this, {
path: this.path!,
lovelace: this.lovelace!,
});
}
private _deleteCard(): void {
confDeleteCard(this.lovelace!, this.path!);
}

View File

@ -19,7 +19,7 @@ declare global {
export class HuiThemeSelectionEditor extends hassLocalizeLitMixin(LitElement) {
public value?: string;
protected hass?: HomeAssistant;
public hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return {

View File

@ -46,7 +46,7 @@ export class HuiNotificationsButton extends EventsMixin(PolymerElement) {
static get properties() {
return {
notificationsOpen: {
open: {
type: Boolean,
notify: true,
},
@ -58,7 +58,7 @@ export class HuiNotificationsButton extends EventsMixin(PolymerElement) {
}
_clicked() {
this.notificationsOpen = true;
this.open = true;
}
_hasNotifications(notifications) {

View File

@ -1,12 +1,12 @@
import "@polymer/paper-input/paper-textarea";
import createCardElement from "../../common/create-card-element";
import createErrorCardConfig from "../../common/create-error-card-config";
import { createCardElement } from "../../common/create-card-element";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardConfig } from "../../../../data/lovelace";
import { LovelaceCard } from "../../types";
import { ConfigError } from "../types";
import { getCardElementTag } from "../../common/get-card-element-tag";
import { createErrorCardConfig } from "../../cards/hui-error-card";
export class HuiCardPreview extends HTMLElement {
private _hass?: HomeAssistant;

View File

@ -0,0 +1,106 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-item/paper-item";
// tslint:disable-next-line:no-duplicate-imports
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { moveCard } from "../config-util";
import { MoveCardViewDialogParams } from "./show-move-card-view-dialog";
export class HuiDialogMoveCardView extends hassLocalizeLitMixin(LitElement) {
private _params?: MoveCardViewDialogParams;
static get properties(): PropertyDeclarations {
return {
_params: {},
};
}
public async showDialog(params: MoveCardViewDialogParams): Promise<void> {
this._params = params;
await this.updateComplete;
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
return html`
<style>
paper-item {
margin: 8px;
cursor: pointer;
}
paper-item[active] {
color: var(--primary-color);
}
paper-item[active]:before {
border-radius: 4px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
content: "";
background-color: var(--primary-color);
opacity: 0.12;
transition: opacity 15ms linear;
will-change: opacity;
}
</style>
<paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
>
<h2>Choose view to move card</h2>
${
this._params!.lovelace!.config.views.map((view, index) => {
return html`
<paper-item
?active="${this._params!.path![0] === index}"
@click="${this._moveCard}"
.index="${index}"
>${view.title}</paper-item
>
`;
})
}
</paper-dialog>
`;
}
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
}
private _moveCard(e: Event): void {
const newView = (e.currentTarget! as any).index;
const path = this._params!.path!;
if (newView === path[0]) {
return;
}
const lovelace = this._params!.lovelace!;
lovelace.saveConfig(moveCard(lovelace.config, path, [newView!]));
this._dialog.close();
}
private _openedChanged(ev: MouseEvent) {
if (!(ev.detail as any).value) {
this._params = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-dialog-move-card-view": HuiDialogMoveCardView;
}
}
customElements.define("hui-dialog-move-card-view", HuiDialogMoveCardView);

View File

@ -160,6 +160,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
? html`
<div class="paper-dialog-buttons">
<paper-button
class="toggle-button"
?hidden="${!this._configValue || !this._configValue.value}"
?disabled="${
this._configElement === null || this._configState !== "OK"
@ -236,6 +237,9 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
margin-bottom: 4px;
display: block;
}
.toggle-button {
margin-right: auto;
}
</style>
`;
}

View File

@ -21,7 +21,8 @@ const registerEditCardDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () => import("./hui-dialog-edit-card"),
dialogImport: () =>
import(/* webpackChunkName: "hui-dialog-edit-card" */ "./hui-dialog-edit-card"),
});
export const showEditCardDialog = (

View File

@ -0,0 +1,35 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import { Lovelace } from "../../types";
declare global {
// for fire event
interface HASSDomEvents {
"show-move-card-view": MoveCardViewDialogParams;
}
}
let registeredDialog = false;
export interface MoveCardViewDialogParams {
path: [number, number];
lovelace: Lovelace;
}
const registerEditCardDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent: "show-move-card-view",
dialogTag: "hui-dialog-move-card-view",
dialogImport: () =>
import(/* webpackChunkName: "hui-dialog-move-card-view" */ "./hui-dialog-move-card-view"),
});
export const showMoveCardViewDialog = (
element: HTMLElement,
moveCardViewDialogParams: MoveCardViewDialogParams
) => {
if (!registeredDialog) {
registeredDialog = true;
registerEditCardDialog(element);
}
fireEvent(element, "show-move-card-view", moveCardViewDialogParams);
};

View File

@ -0,0 +1,195 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-alarm-panel-card";
import { configElementStyle } from "./config-elements-style";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-icon";
const cardConfigStruct = struct({
type: "string",
entity: "string?",
name: "string?",
states: "array?",
});
export class HuiAlarmPanelCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _entity(): string {
return this._config!.entity || "";
}
get _name(): string {
return this._config!.name || "";
}
get _states(): string[] {
return this._config!.states || [];
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const states = ["arm_home", "arm_away", "arm_night", "arm_custom_bypass"];
return html`
${configElementStyle} ${this.renderStyle()}
<div class="card-config">
<div class="side-by-side">
<paper-input
label="Name"
.value="${this._name}"
.configValue="${"name"}"
@value-changed="${this._valueChanged}"
></paper-input>
<ha-entity-picker
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="alarm_control_panel"
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
</div>
<span>Used States</span> ${
this._states.map((state, index) => {
return html`
<div class="states">
<paper-item>${state}</paper-item>
<ha-icon
class="deleteState"
.value="${index}"
icon="hass:close"
@click=${this._stateRemoved}
></ha-icon>
</div>
`;
})
}
<paper-dropdown-menu
label="Available States"
@value-changed="${this._stateAdded}"
>
<paper-listbox slot="dropdown-content">
${
states.map((state) => {
return html`
<paper-item>${state}</paper-item>
`;
})
}
</paper-listbox>
</paper-dropdown-menu>
</div>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
.states {
display: flex;
flex-direction: row;
}
.deleteState {
visibility: hidden;
}
.states:hover > .deleteState {
visibility: visible;
}
ha-icon {
padding-top: 12px;
}
</style>
`;
}
private _stateRemoved(ev: EntitiesEditorEvent): void {
if (!this._config || !this._states || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
const index = Number(target.value);
if (index > -1) {
const newStates = this._states;
newStates.splice(index, 1);
this._config = {
...this._config,
states: newStates,
};
fireEvent(this, "config-changed", { config: this._config });
}
}
private _stateAdded(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (!target.value || this._states.indexOf(target.value) >= 0) {
return;
}
const newStates = this._states;
newStates.push(target.value);
this._config = {
...this._config,
states: newStates,
};
target.value = "";
fireEvent(this, "config-changed", { config: this._config });
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-alarm-panel-card-editor": HuiAlarmPanelCardEditor;
}
}
customElements.define("hui-alarm-panel-card-editor", HuiAlarmPanelCardEditor);

View File

@ -1,13 +1,12 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { struct } from "../../common/structs/struct";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { processEditorEntities } from "../process-editor-entities";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
@ -59,8 +58,7 @@ export class HuiEntitiesCardEditor extends hassLocalizeLitMixin(LitElement)
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = { type: "entities", ...config };
this._config = config;
this._configEntities = processEditorEntities(config.entities);
}
@ -74,7 +72,7 @@ export class HuiEntitiesCardEditor extends hassLocalizeLitMixin(LitElement)
<div class="card-config">
<paper-input
label="Title"
value="${this._title}"
.value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
@ -117,11 +115,15 @@ export class HuiEntitiesCardEditor extends hassLocalizeLitMixin(LitElement)
this._config.entities = ev.detail.entities;
this._configEntities = processEditorEntities(this._config.entities);
} else if (target.configValue) {
this._config = {
...this._config,
[target.configValue]:
target.checked !== undefined ? target.checked : target.value,
};
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue]:
target.checked !== undefined ? target.checked : target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });

View File

@ -0,0 +1,165 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import { struct } from "../../common/structs/struct";
import {
EntitiesEditorEvent,
EditorTarget,
actionConfigStruct,
} from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-entity-button-card";
import { configElementStyle } from "./config-elements-style";
import { ActionConfig } from "../../../../data/lovelace";
import "../../components/hui-action-editor";
import "../../components/hui-theme-select-editor";
import "../../components/hui-entity-editor";
const cardConfigStruct = struct({
type: "string",
entity: "string?",
name: "string?",
icon: "string?",
tap_action: actionConfigStruct,
hold_action: actionConfigStruct,
theme: "string?",
});
export class HuiEntityButtonCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _entity(): string {
return this._config!.entity || "";
}
get _name(): string {
return this._config!.name || "";
}
get _icon(): string {
return this._config!.icon || "";
}
get _tap_action(): ActionConfig {
return this._config!.tap_action || { action: "more-info" };
}
get _hold_action(): ActionConfig {
return this._config!.hold_action || { action: "none" };
}
get _theme(): string {
return this._config!.theme || "default";
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const actions = ["more-info", "toggle", "navigate", "call-service", "none"];
return html`
${configElementStyle}
<div class="card-config">
<ha-entity-picker
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
<div class="side-by-side">
<paper-input
label="Name (Optional)"
.value="${this._name}"
.configValue="${"name"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="Icon (Optional)"
.value="${this._icon}"
.configValue="${"icon"}"
@value-changed="${this._valueChanged}"
></paper-input>
</div>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
<div class="side-by-side">
<hui-action-editor
label="Tap Action"
.hass="${this.hass}"
.config="${this._tap_action}"
.actions="${actions}"
.configValue="${"tap_action"}"
@action-changed="${this._valueChanged}"
></hui-action-editor>
<hui-action-editor
label="Hold Action"
.hass="${this.hass}"
.config="${this._hold_action}"
.actions="${actions}"
.configValue="${"hold_action"}"
@action-changed="${this._valueChanged}"
></hui-action-editor>
</div>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (
this[`_${target.configValue}`] === target.value ||
this[`_${target.configValue}`] === target.config
) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value ? target.value : target.config,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-entity-button-card-editor": HuiEntityButtonCardEditor;
}
}
customElements.define(
"hui-entity-button-card-editor",
HuiEntityButtonCardEditor
);

View File

@ -0,0 +1,244 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config, SeverityConfig } from "../../cards/hui-gauge-card";
import { configElementStyle } from "./config-elements-style";
import "../../components/hui-theme-select-editor";
import "../../components/hui-entity-editor";
const cardConfigStruct = struct({
type: "string",
name: "string?",
entity: "string?",
unit: "string?",
min: "number?",
max: "number?",
severity: "object?",
theme: "string?",
});
export class HuiGaugeCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
private _useSeverity?: boolean;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._useSeverity = config.severity ? true : false;
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _name(): string {
return this._config!.name || "";
}
get _entity(): string {
return this._config!.entity || "";
}
get _unit(): string {
return this._config!.unit || "";
}
get _theme(): string {
return this._config!.theme || "default";
}
get _min(): number {
return this._config!.number || 0;
}
get _max(): number {
return this._config!.max || 100;
}
get _severity(): SeverityConfig | undefined {
return this._config!.severity || undefined;
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${configElementStyle} ${this.renderStyle()}
<div class="card-config">
<div class="side-by-side">
<paper-input
label="Name"
.value="${this._name}"
.configValue=${"name"}
@value-changed="${this._valueChanged}"
></paper-input>
<ha-entity-picker
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="sensor"
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
</div>
<div class="side-by-side">
<paper-input
label="Unit"
.value="${this._unit}"
.configValue=${"unit"}
@value-changed="${this._valueChanged}"
></paper-input>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
<div class="side-by-side">
<paper-input
type="number"
label="Minimum"
.value="${this._min}"
.configValue=${"min"}
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
type="number"
label="Maximum"
.value="${this._max}"
.configValue=${"max"}
@value-changed="${this._valueChanged}"
></paper-input>
</div>
<div class="side-by-side">
<paper-toggle-button
?checked="${this._useSeverity !== false}"
@change="${this._toggleSeverity}"
>Define Severity?</paper-toggle-button
>
<div class="severity">
<paper-input
type="number"
label="Green"
.value="${this._severity ? this._severity.green : 0}"
.configValue=${"green"}
@value-changed="${this._severityChanged}"
></paper-input>
<paper-input
type="number"
label="Yellow"
.value="${this._severity ? this._severity.yellow : 0}"
.configValue=${"yellow"}
@value-changed="${this._severityChanged}"
></paper-input>
<paper-input
type="number"
label="Red"
.value="${this._severity ? this._severity.red : 0}"
.configValue=${"red"}
@value-changed="${this._severityChanged}"
></paper-input>
</div>
</div>
</div>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
.severity {
display: none;
width: 100%;
padding-left: 16px;
flex-direction: row;
flex-wrap: wrap;
}
.severity > * {
flex: 1 0 30%;
padding-right: 4px;
}
paper-toggle-button[checked] ~ .severity {
display: flex;
}
</style>
`;
}
private _toggleSeverity(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
this._config.severity = target.checked
? {
green: 0,
yellow: 0,
red: 0,
}
: undefined;
fireEvent(this, "config-changed", { config: this._config });
}
private _severityChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
const severity = {
...this._config.severity,
[target.configValue!]: Number(target.value),
};
this._config = {
...this._config,
severity,
};
fireEvent(this, "config-changed", { config: this._config });
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (target.configValue) {
if (
target.value === "" ||
(target.type === "number" && isNaN(Number(target.value)))
) {
delete this._config[target.configValue!];
} else {
let value: any = target.value;
if (target.type === "number") {
value = Number(value);
}
this._config = { ...this._config, [target.configValue!]: value };
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-gauge-card-editor": HuiGaugeCardEditor;
}
}
customElements.define("hui-gauge-card-editor", HuiGaugeCardEditor);

View File

@ -1,11 +1,11 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { struct } from "../../common/structs/struct";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { struct } from "../../common/structs/struct";
import { processEditorEntities } from "../process-editor-entities";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
@ -48,8 +48,7 @@ export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = { type: "glance", ...config };
this._config = config;
this._configEntities = processEditorEntities(config.entities);
}
@ -65,8 +64,8 @@ export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
return this._config!.theme || "Backend-selected";
}
get _columns(): string {
return this._config!.columns ? String(this._config!.columns) : "";
get _columns(): number {
return this._config!.columns || NaN;
}
protected render(): TemplateResult {
@ -79,7 +78,7 @@ export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
<div class="card-config">
<paper-input
label="Title"
value="${this._title}"
.value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
@ -93,7 +92,7 @@ export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
<paper-input
label="Columns"
type="number"
value="${this._columns}"
.value="${this._columns}"
.configValue="${"columns"}"
@value-changed="${this._valueChanged}"
></paper-input>
@ -127,22 +126,29 @@ export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
if (target.configValue && this[`_${target.configValue}`] === target.value) {
return;
}
if (ev.detail && ev.detail.entities) {
this._config.entities = ev.detail.entities;
this._configEntities = processEditorEntities(this._config.entities);
} else if (target.configValue) {
let value: any = target.value;
if (target.type === "number") {
value = Number(value);
if (
target.value === "" ||
(target.type === "number" && isNaN(Number(target.value)))
) {
delete this._config[target.configValue!];
} else {
let value: any = target.value;
if (target.type === "number") {
value = Number(value);
}
this._config = {
...this._config,
[target.configValue!]:
target.checked !== undefined ? target.checked : value,
};
}
this._config = {
...this._config,
[target.configValue!]:
target.checked !== undefined ? target.checked : value,
};
}
fireEvent(this, "config-changed", { config: this._config });
}

View File

@ -0,0 +1,111 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-iframe-card";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = struct({
type: "string",
title: "string?",
url: "string?",
aspect_ratio: "string?",
});
export class HuiIframeCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _title(): string {
return this._config!.title || "";
}
get _url(): string {
return this._config!.url || "";
}
get _aspect_ratio(): string {
return this._config!.aspect_ratio || "";
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${configElementStyle}
<div class="card-config">
<div class="side-by-side">
<paper-input
label="Title"
.value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="Aspect Ratio"
type="number"
.value="${Number(this._aspect_ratio.replace("%", ""))}"
.configValue="${"aspect_ratio"}"
@value-changed="${this._valueChanged}"
></paper-input>
</div>
<paper-input
label="Url"
.value="${this._url}"
.configValue="${"url"}"
@value-changed="${this._valueChanged}"
></paper-input>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
let value = target.value;
if (target.configValue! === "aspect_ratio" && target.value) {
value += "%";
}
if (this[`_${target.configValue}`] === value) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = { ...this._config, [target.configValue!]: value };
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-iframe-card-editor": HuiIframeCardEditor;
}
}
customElements.define("hui-iframe-card-editor", HuiIframeCardEditor);

View File

@ -0,0 +1,113 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-light-card";
import { configElementStyle } from "./config-elements-style";
import "../../components/hui-theme-select-editor";
import "../../components/hui-entity-editor";
const cardConfigStruct = struct({
type: "string",
name: "string?",
entity: "string?",
theme: "string?",
});
export class HuiLightCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {}, _configEntities: {} };
}
get _name(): string {
return this._config!.name || "";
}
get _theme(): string {
return this._config!.theme || "default";
}
get _entity(): string {
return this._config!.entity || "";
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${configElementStyle}
<div class="card-config">
<paper-input
label="Name"
.value="${this._name}"
.configValue="${"name"}"
@value-changed="${this._valueChanged}"
></paper-input>
<div class="side-by-side">
<ha-entity-picker
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="light"
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-light-card-editor": HuiLightCardEditor;
}
}
customElements.define("hui-light-card-editor", HuiLightCardEditor);

View File

@ -0,0 +1,143 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-alarm-panel-card";
import { configElementStyle } from "./config-elements-style";
import { processEditorEntities } from "../process-editor-entities";
import { EntityConfig } from "../../entity-rows/types";
import "../../components/hui-entity-editor";
const entitiesConfigStruct = struct.union([
{
entity: "entity-id",
name: "string?",
icon: "icon?",
},
"entity-id",
]);
const cardConfigStruct = struct({
type: "string",
title: "string?",
aspect_ratio: "string?",
default_zoom: "number?",
entities: [entitiesConfigStruct],
});
export class HuiMapCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
private _configEntities?: EntityConfig[];
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
this._configEntities = processEditorEntities(config.entities);
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {}, _configEntities: {} };
}
get _title(): string {
return this._config!.title || "";
}
get _aspect_ratio(): string {
return this._config!.aspect_ratio || "";
}
get _default_zoom(): number {
return this._config!.default_zoom || NaN;
}
get _entities(): string[] {
return this._config!.entities || [];
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${configElementStyle}
<div class="card-config">
<paper-input
label="Title"
.value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
<div class="side-by-side">
<paper-input
label="Aspect Ratio"
.value="${this._aspect_ratio}"
.configValue="${"aspect_ratio"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="Default Zoom"
type="number"
.value="${this._default_zoom}"
.configValue="${"default_zoom"}"
@value-changed="${this._valueChanged}"
></paper-input>
</div>
<hui-entity-editor
.hass="${this.hass}"
.entities="${this._configEntities}"
@entities-changed="${this._valueChanged}"
></hui-entity-editor>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (target.configValue && this[`_${target.configValue}`] === target.value) {
return;
}
if (ev.detail && ev.detail.entities) {
this._config.entities = ev.detail.entities;
this._configEntities = processEditorEntities(this._config.entities);
} else if (target.configValue) {
if (
target.value === "" ||
(target.type === "number" && isNaN(Number(target.value)))
) {
delete this._config[target.configValue!];
} else {
let value: any = target.value;
if (target.type === "number") {
value = Number(value);
}
this._config = {
...this._config,
[target.configValue!]: value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-map-card-editor": HuiMapCardEditor;
}
}
customElements.define("hui-map-card-editor", HuiMapCardEditor);

View File

@ -0,0 +1,99 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-glance-card";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = struct({
type: "string",
title: "string?",
content: "string",
});
export class HuiMarkdownCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _title(): string {
return this._config!.title || "";
}
get _content(): string {
return this._config!.content || "";
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${configElementStyle}
<div class="card-config">
<paper-input
label="Title"
.value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-textarea
label="Content"
.value="${this._content}"
.configValue="${"content"}"
@value-changed="${this._valueChanged}"
autocapitalize="none"
autocomplete="off"
spellcheck="false"
></paper-textarea>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-markdown-card-editor": HuiMarkdownCardEditor;
}
}
customElements.define("hui-markdown-card-editor", HuiMarkdownCardEditor);

View File

@ -0,0 +1,87 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-media-control-card";
import "../../../../components/entity/ha-entity-picker";
const cardConfigStruct = struct({
type: "string",
entity: "string?",
});
export class HuiMediaControlCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _entity(): string {
return this._config!.entity || "";
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<div class="card-config">
<ha-entity-picker
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="media_player"
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-media-control-card-editor": HuiMediaControlCardEditor;
}
}
customElements.define(
"hui-media-control-card-editor",
HuiMediaControlCardEditor
);

View File

@ -0,0 +1,124 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import { struct } from "../../common/structs/struct";
import {
EntitiesEditorEvent,
EditorTarget,
actionConfigStruct,
} from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-picture-card";
import { configElementStyle } from "./config-elements-style";
import { ActionConfig } from "../../../../data/lovelace";
import "../../components/hui-action-editor";
const cardConfigStruct = struct({
type: "string",
image: "string?",
tap_action: actionConfigStruct,
hold_action: actionConfigStruct,
});
export class HuiPictureCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _image(): string {
return this._config!.image || "";
}
get _tap_action(): ActionConfig {
return this._config!.tap_action || { action: "none" };
}
get _hold_action(): ActionConfig {
return this._config!.hold_action || { action: "none" };
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const actions = ["navigate", "call-service", "none"];
return html`
${configElementStyle}
<div class="card-config">
<paper-input
label="Image Url"
.value="${this._image}"
.configValue="${"image"}"
@value-changed="${this._valueChanged}"
></paper-input>
<div class="side-by-side">
<hui-action-editor
label="Tap Action"
.hass="${this.hass}"
.config="${this._tap_action}"
.actions="${actions}"
.configValue="${"tap_action"}"
@action-changed="${this._valueChanged}"
></hui-action-editor>
<hui-action-editor
label="Hold Action"
.hass="${this.hass}"
.config="${this._hold_action}"
.actions="${actions}"
.configValue="${"hold_action"}"
@action-changed="${this._valueChanged}"
></hui-action-editor>
</div>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (
this[`_${target.configValue}`] === target.value ||
this[`_${target.configValue}`] === target.config
) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value ? target.value : target.config,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-picture-card-editor": HuiPictureCardEditor;
}
}
customElements.define("hui-picture-card-editor", HuiPictureCardEditor);

View File

@ -0,0 +1,101 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-alarm-panel-card";
import { configElementStyle } from "./config-elements-style";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-icon";
const cardConfigStruct = struct({
type: "string",
entity: "string",
name: "string?",
});
export class HuiPlantStatusCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _entity(): string {
return this._config!.entity || "";
}
get _name(): string {
return this._config!.name || "";
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${configElementStyle}
<div class="card-config">
<div class="side-by-side">
<paper-input
label="Name"
.value="${this._name}"
.configValue="${"name"}"
@value-changed="${this._valueChanged}"
></paper-input>
<ha-entity-picker
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="plant"
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
</div>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-plant-status-card-editor": HuiPlantStatusCardEditor;
}
}
customElements.define("hui-plant-status-card-editor", HuiPlantStatusCardEditor);

View File

@ -0,0 +1,197 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-sensor-card";
import { configElementStyle } from "./config-elements-style";
import "../../components/hui-theme-select-editor";
import "../../../../components/entity/ha-entity-picker";
const cardConfigStruct = struct({
type: "string",
entity: "string?",
name: "string?",
icon: "string?",
graph: "string?",
unit: "string?",
detail: "number?",
theme: "string?",
hours_to_show: "number?",
});
export class HuiSensorCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _entity(): string {
return this._config!.entity || "";
}
get _name(): string {
return this._config!.name || "";
}
get _icon(): string {
return this._config!.icon || "";
}
get _graph(): string {
return this._config!.graph || "none";
}
get _unit(): string {
return this._config!.unit || "";
}
get _detail(): number | string {
return this._config!.number || "1";
}
get _theme(): string {
return this._config!.theme || "default";
}
get _hours_to_show(): number | string {
return this._config!.hours_to_show || "24";
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const graphs = ["line", "none"];
return html`
${configElementStyle}
<div class="card-config">
<div class="side-by-side">
<paper-input
label="Name"
.value="${this._name}"
.configValue="${"name"}"
@value-changed="${this._valueChanged}"
></paper-input>
<ha-entity-picker
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="sensor"
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
</div>
<div class="side-by-side">
<paper-input
label="Icon"
.value="${this._icon}"
.configValue="${"icon"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-dropdown-menu
label="Graph Type"
.configValue="${"graph"}"
@value-changed="${this._valueChanged}"
>
<paper-listbox
slot="dropdown-content"
.selected="${graphs.indexOf(this._graph)}"
>
${
graphs.map((graph) => {
return html`
<paper-item>${graph}</paper-item>
`;
})
}
</paper-listbox>
</paper-dropdown-menu>
</div>
<div class="side-by-side">
<paper-input
label="Units"
.value="${this._unit}"
.configValue="${"unit"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="Graph Detail"
type="number"
.value="${this._detail}"
.configValue="${"detail"}"
@value-changed="${this._valueChanged}"
></paper-input>
</div>
<div class="side-by-side">
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
<paper-input
label="Hours To Show"
type="number"
.value="${this._hours_to_show}"
.configValue="${"hours_to_show"}"
@value-changed="${this._valueChanged}"
></paper-input>
</div>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (
target.value === "" ||
(target.type === "number" && isNaN(Number(target.value)))
) {
delete this._config[target.configValue!];
} else {
let value: any = target.value;
if (target.type === "number") {
value = Number(value);
}
this._config = { ...this._config, [target.configValue!]: value };
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-sensor-card-editor": HuiSensorCardEditor;
}
}
customElements.define("hui-sensor-card-editor", HuiSensorCardEditor);

View File

@ -0,0 +1,82 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-shopping-list-card";
const cardConfigStruct = struct({
type: "string",
title: "string?",
});
export class HuiShoppingListEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _title(): string {
return this._config!.title || "";
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<div class="card-config">
<paper-input
label="Title"
.value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-shopping-list-card-editor": HuiShoppingListEditor;
}
}
customElements.define("hui-shopping-list-card-editor", HuiShoppingListEditor);

View File

@ -0,0 +1,110 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-thermostat-card";
import { configElementStyle } from "./config-elements-style";
import "../../components/hui-theme-select-editor";
import "../../../../components/entity/ha-entity-picker";
const cardConfigStruct = struct({
type: "string",
entity: "string",
name: "string?",
theme: "string?",
});
export class HuiThermostatCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _entity(): string {
return this._config!.entity || "";
}
get _name(): string {
return this._config!.name || "";
}
get _theme(): string {
return this._config!.theme || "default";
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${configElementStyle}
<div class="card-config">
<paper-input
label="Name"
.value="${this._name}"
.configValue="${"name"}"
@value-changed="${this._valueChanged}"
></paper-input>
<div class="side-by-side">
<ha-entity-picker
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="climate"
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = { ...this._config, [target.configValue!]: target.value };
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-thermostat-card-editor": HuiThermostatCardEditor;
}
}
customElements.define("hui-thermostat-card-editor", HuiThermostatCardEditor);

View File

@ -0,0 +1,103 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-weather-forecast-card";
import { configElementStyle } from "./config-elements-style";
import "../../../../components/entity/ha-entity-picker";
const cardConfigStruct = struct({
type: "string",
entity: "string?",
name: "string?",
});
export class HuiWeatherForecastCardEditor
extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _entity(): string {
return this._config!.entity || "";
}
get _name(): string {
return this._config!.name || "";
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${configElementStyle}
<div class="card-config">
<div class="side-by-side">
<paper-input
label="Name"
.value="${this._name}"
.configValue="${"name"}"
@value-changed="${this._valueChanged}"
></paper-input>
<ha-entity-picker
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="weather"
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
</div>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-weather-forecast-card-editor": HuiWeatherForecastCardEditor;
}
}
customElements.define(
"hui-weather-forecast-card-editor",
HuiWeatherForecastCardEditor
);

View File

@ -121,6 +121,44 @@ export const swapCard = (
};
};
export const moveCard = (
config: LovelaceConfig,
fromPath: [number, number],
toPath: [number]
): LovelaceConfig => {
if (fromPath[0] === toPath[0]) {
throw new Error("You can not move a card to the view it is in.");
}
const fromView = config.views[fromPath[0]];
const card = fromView.cards![fromPath[1]];
const newView1 = {
...fromView,
cards: (fromView.cards || []).filter(
(_origConf, ind) => ind !== fromPath[1]
),
};
const toView = config.views[toPath[0]];
const cards = toView.cards ? [...toView.cards, card] : [card];
const newView2 = {
...toView,
cards,
};
return {
...config,
views: config.views.map((origView, index) =>
index === toPath[0]
? newView2
: index === fromPath[0]
? newView1
: origView
),
};
};
export const addView = (
config: LovelaceConfig,
viewConfig: LovelaceViewConfig

View File

@ -14,7 +14,7 @@ import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { SaveDialogParams } from "./show-save-config-dialog";
export class HuiSaveConfig extends hassLocalizeLitMixin(LitElement) {
protected hass?: HomeAssistant;
public hass?: HomeAssistant;
private _params?: SaveDialogParams;
private _saving: boolean;

View File

@ -0,0 +1,152 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-spinner/paper-spinner";
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 "@polymer/paper-button/paper-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "./hui-lovelace-editor";
import { HomeAssistant } from "../../../../types";
import { LovelaceConfig } from "../../../../data/lovelace";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { Lovelace } from "../../types";
export class HuiDialogEditLovelace extends hassLocalizeLitMixin(LitElement) {
public hass?: HomeAssistant;
private _lovelace?: Lovelace;
private _config?: LovelaceConfig;
private _saving: boolean;
static get properties(): PropertyDeclarations {
return {
hass: {},
_lovelace: {},
};
}
protected constructor() {
super();
this._saving = false;
}
public async showDialog(lovelace: Lovelace): Promise<void> {
this._lovelace = lovelace;
if (this._dialog == null) {
await this.updateComplete;
}
const { views, ...lovelaceConfig } = this._lovelace!.config;
this._config = lovelaceConfig as LovelaceConfig;
this._dialog.open();
}
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
}
protected render(): TemplateResult {
return html`
${this.renderStyle()}
<paper-dialog with-backdrop>
<h2>Edit Lovelace</h2>
<paper-dialog-scrollable>
<hui-lovelace-editor
.hass="${this.hass}"
.config="${this._config}"
@lovelace-config-changed="${this._ConfigChanged}"
></hui-lovelace-editor
></paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<paper-button @click="${this._closeDialog}"
>${this.localize("ui.common.cancel")}</paper-button
>
<paper-button
?disabled="${!this._config || this._saving}"
@click="${this._save}"
>
<paper-spinner
?active="${this._saving}"
alt="Saving"
></paper-spinner>
${this.localize("ui.common.save")}</paper-button
>
</div>
</paper-dialog>
`;
}
private _closeDialog(): void {
this._config = undefined;
this._dialog.close();
}
private async _save(): Promise<void> {
if (!this._config) {
return;
}
if (!this._isConfigChanged()) {
this._closeDialog();
return;
}
this._saving = true;
const lovelace = this._lovelace!;
const config: LovelaceConfig = {
...lovelace.config,
...this._config,
};
try {
await lovelace.saveConfig(config);
this._closeDialog();
} catch (err) {
alert(`Saving failed: ${err.message}`);
} finally {
this._saving = false;
}
}
private _ConfigChanged(ev: CustomEvent): void {
if (ev.detail && ev.detail.config) {
this._config = ev.detail.config;
}
}
private _isConfigChanged(): boolean {
const { views, ...lovelaceConfig } = this._lovelace!.config;
return JSON.stringify(this._config) !== JSON.stringify(lovelaceConfig);
}
private renderStyle(): TemplateResult {
return html`
<style>
paper-dialog {
width: 650px;
}
paper-button paper-spinner {
width: 14px;
height: 14px;
margin-right: 20px;
}
paper-spinner {
display: none;
}
paper-spinner[active] {
display: block;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-dialog-edit-lovelace": HuiDialogEditLovelace;
}
}
customElements.define("hui-dialog-edit-lovelace", HuiDialogEditLovelace);

View File

@ -0,0 +1,80 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import { EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { configElementStyle } from "../config-elements/config-elements-style";
import { LovelaceConfig } from "../../../../data/lovelace";
declare global {
interface HASSDomEvents {
"lovelace-config-changed": {
config: LovelaceConfig;
};
}
}
export class HuiLovelaceEditor extends hassLocalizeLitMixin(LitElement) {
static get properties(): PropertyDeclarations {
return { hass: {}, config: {} };
}
public hass?: HomeAssistant;
public config?: LovelaceConfig;
get _title(): string {
if (!this.config) {
return "";
}
return this.config.title || "";
}
protected render(): TemplateResult {
return html`
${configElementStyle}
<div class="card-config">
<paper-input
label="Title"
.value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
</div>
`;
}
private _valueChanged(ev: Event): void {
if (!this.config) {
return;
}
const target = ev.currentTarget! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
let newConfig;
if (target.configValue) {
newConfig = {
...this.config,
[target.configValue]: target.value,
};
}
fireEvent(this, "lovelace-config-changed", { config: newConfig });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-lovelace-editor": HuiLovelaceEditor;
}
}
customElements.define("hui-lovelace-editor", HuiLovelaceEditor);

View File

@ -0,0 +1,32 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import { Lovelace } from "../../types";
declare global {
// for fire event
interface HASSDomEvents {
"show-edit-lovelace": Lovelace;
}
}
let registeredDialog = false;
const dialogShowEvent = "show-edit-lovelace";
const dialogTag = "hui-dialog-edit-lovelace";
const registerEditLovelaceDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () =>
import(/* webpackChunkName: "hui-dialog-edit-lovelace" */ "./hui-dialog-edit-lovelace"),
});
export const showEditLovelaceDialog = (
element: HTMLElement,
lovelace: Lovelace
) => {
if (!registeredDialog) {
registeredDialog = true;
registerEditLovelaceDialog(element);
}
fireEvent(element, dialogShowEvent, lovelace);
};

View File

@ -26,7 +26,8 @@ export const showSaveDialog = (
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () => import("./hui-dialog-save-config"),
dialogImport: () =>
import(/* webpackChunkName: "hui-dialog-save-config" */ "./hui-dialog-save-config"),
});
}
fireEvent(element, dialogShowEvent, saveDialogParams);

View File

@ -1,6 +1,11 @@
import { LovelaceCardConfig, LovelaceViewConfig } from "../../../data/lovelace";
import {
LovelaceCardConfig,
LovelaceViewConfig,
ActionConfig,
} from "../../../data/lovelace";
import { EntityConfig } from "../entity-rows/types";
import { InputType } from "zlib";
import { struct } from "../common/structs/struct";
export interface YamlChangedEvent extends Event {
detail: {
@ -37,8 +42,16 @@ export interface EditorTarget extends EventTarget {
checked?: boolean;
configValue?: string;
type?: InputType;
config: ActionConfig;
}
export interface CardPickTarget extends EventTarget {
type: string;
}
export const actionConfigStruct = struct({
action: "string",
navigation_path: "string?",
service: "string?",
service_data: "object?",
});

View File

@ -30,7 +30,7 @@ import { deleteView, addView, replaceView } from "../config-util";
export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
public lovelace?: Lovelace;
public viewIndex?: number;
protected hass?: HomeAssistant;
public hass?: HomeAssistant;
private _config?: LovelaceViewConfig;
private _badges?: EntityConfig[];
private _cards?: LovelaceCardConfig[];

View File

@ -69,19 +69,19 @@ export class HuiViewEditor extends hassLocalizeLitMixin(LitElement) {
<div class="card-config">
<paper-input
label="Title"
value="${this._title}"
.value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="Icon"
value="${this._icon}"
.value="${this._icon}"
.configValue="${"icon"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="URL Path"
value="${this._path}"
.value="${this._path}"
.configValue="${"path"}"
@value-changed="${this._valueChanged}"
></paper-input>

View File

@ -26,7 +26,8 @@ const registerEditViewDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () => import("./hui-dialog-edit-view"),
dialogImport: () =>
import(/* webpackChunkName: "hui-dialog-edit-view" */ "./hui-dialog-edit-view"),
});
export const showEditViewDialog = (

View File

@ -31,12 +31,22 @@ class HuiClimateEntityRow extends LitElement implements EntityRow {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
return html`
<hui-error-entity-row
.entity="${this._config.entity}"
></hui-error-entity-row>
`;
}
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]}"
.stateObj="${stateObj}"
></ha-climate-state>
</hui-generic-entity-row>
`;

View File

@ -29,7 +29,7 @@ export type EntityRowConfig =
| WeblinkConfig
| CallServiceConfig;
export interface EntityRow {
export interface EntityRow extends HTMLElement {
hass?: HomeAssistant;
setConfig(config: EntityRowConfig);
}

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