Normalize all line endings
This commit is contained in:
parent
727cfe92e3
commit
fbc1a722bd
Binary file not shown.
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
@ -1,24 +1,24 @@
|
|||
export default function parseAspectRatio(input) {
|
||||
// Handle 16x9, 16:9, 1.78x1, 1.78:1, 1.78
|
||||
// Ignore everything else
|
||||
function parseOrThrow(number) {
|
||||
const parsed = parseFloat(number);
|
||||
if (isNaN(parsed)) throw new Error(`${number} is not a number`);
|
||||
return parsed;
|
||||
}
|
||||
try {
|
||||
if (input) {
|
||||
const arr = input.replace(":", "x").split("x");
|
||||
if (arr.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return arr.length === 1
|
||||
? { w: parseOrThrow(arr[0]), h: 1 }
|
||||
: { w: parseOrThrow(arr[0]), h: parseOrThrow(arr[1]) };
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore the error
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export default function parseAspectRatio(input) {
|
||||
// Handle 16x9, 16:9, 1.78x1, 1.78:1, 1.78
|
||||
// Ignore everything else
|
||||
function parseOrThrow(number) {
|
||||
const parsed = parseFloat(number);
|
||||
if (isNaN(parsed)) throw new Error(`${number} is not a number`);
|
||||
return parsed;
|
||||
}
|
||||
try {
|
||||
if (input) {
|
||||
const arr = input.replace(":", "x").split("x");
|
||||
if (arr.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return arr.length === 1
|
||||
? { w: parseOrThrow(arr[0]), h: 1 }
|
||||
: { w: parseOrThrow(arr[0]), h: parseOrThrow(arr[1]) };
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore the error
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,59 +1,59 @@
|
|||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-dialog/paper-dialog";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../resources/ha-style";
|
||||
|
||||
import EventsMixin from "../../mixins/events-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaLoadedComponents extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style-dialog">
|
||||
paper-dialog {
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
<paper-dialog id="dialog" with-backdrop="" opened="{{_opened}}">
|
||||
<h2>Loaded Components</h2>
|
||||
<paper-dialog-scrollable id="scrollable">
|
||||
<p>The following components are currently loaded:</p>
|
||||
<ul>
|
||||
<template is='dom-repeat' items='[[_components]]'>
|
||||
<li>[[item]]</li>
|
||||
</template>
|
||||
</ul>
|
||||
</paper-dialog-scrollable>
|
||||
</paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_hass: Object,
|
||||
_components: Array,
|
||||
|
||||
_opened: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
}
|
||||
|
||||
showDialog({ hass }) {
|
||||
this.hass = hass;
|
||||
this._opened = true;
|
||||
this._components = this.hass.config.components.sort();
|
||||
setTimeout(() => this.$.dialog.center(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-loaded-components", HaLoadedComponents);
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-dialog/paper-dialog";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../resources/ha-style";
|
||||
|
||||
import EventsMixin from "../../mixins/events-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaLoadedComponents extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style-dialog">
|
||||
paper-dialog {
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
<paper-dialog id="dialog" with-backdrop="" opened="{{_opened}}">
|
||||
<h2>Loaded Components</h2>
|
||||
<paper-dialog-scrollable id="scrollable">
|
||||
<p>The following components are currently loaded:</p>
|
||||
<ul>
|
||||
<template is='dom-repeat' items='[[_components]]'>
|
||||
<li>[[item]]</li>
|
||||
</template>
|
||||
</ul>
|
||||
</paper-dialog-scrollable>
|
||||
</paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_hass: Object,
|
||||
_components: Array,
|
||||
|
||||
_opened: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
}
|
||||
|
||||
showDialog({ hass }) {
|
||||
this.hass = hass;
|
||||
this._opened = true;
|
||||
this._components = this.hass.config.components.sort();
|
||||
setTimeout(() => this.$.dialog.center(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-loaded-components", HaLoadedComponents);
|
||||
|
|
|
@ -1,258 +1,258 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import "../../../components/ha-label-badge";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
|
||||
const Icons = {
|
||||
armed_away: "hass:security-lock",
|
||||
armed_custom_bypass: "hass:security",
|
||||
armed_home: "hass:security-home",
|
||||
armed_night: "hass:security-home",
|
||||
disarmed: "hass:verified",
|
||||
pending: "hass:shield-outline",
|
||||
triggered: "hass:bell-ring",
|
||||
};
|
||||
|
||||
class HuiAlarmPanelCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
padding-bottom: 16px;
|
||||
position: relative;
|
||||
--alarm-color-disarmed: var(--label-badge-green);
|
||||
--alarm-color-pending: var(--label-badge-yellow);
|
||||
--alarm-color-triggered: var(--label-badge-red);
|
||||
--alarm-color-armed: var(--label-badge-red);
|
||||
--alarm-color-autoarm: rgba(0, 153, 255, .1);
|
||||
--alarm-state-color: var(--alarm-color-armed);
|
||||
--base-unit: 15px;
|
||||
font-size: calc(var(--base-unit));
|
||||
}
|
||||
ha-label-badge {
|
||||
--ha-label-badge-color: var(--alarm-state-color);
|
||||
--label-badge-text-color: var(--alarm-state-color);
|
||||
color: var(--alarm-state-color);
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
}
|
||||
.disarmed {
|
||||
--alarm-state-color: var(--alarm-color-disarmed);
|
||||
}
|
||||
.triggered {
|
||||
--alarm-state-color: var(--alarm-color-triggered);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
.arming {
|
||||
--alarm-state-color: var(--alarm-color-pending);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
.pending {
|
||||
--alarm-state-color: var(--alarm-color-pending);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
--ha-label-badge-color: var(--alarm-state-color);
|
||||
}
|
||||
100% {
|
||||
--ha-label-badge-color: rgba(255, 153, 0, 0.3);
|
||||
}
|
||||
}
|
||||
paper-input {
|
||||
margin: auto;
|
||||
max-width: 200px;
|
||||
font-size: calc(var(--base-unit));
|
||||
}
|
||||
.state {
|
||||
margin-left: 16px;
|
||||
font-size: calc(var(--base-unit) * 0.9);
|
||||
position: relative;
|
||||
bottom: 16px;
|
||||
color: var(--alarm-state-color);
|
||||
animation: none;
|
||||
}
|
||||
#keypad {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
#keypad div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#keypad paper-button {
|
||||
margin-bottom: 10%;
|
||||
position: relative;
|
||||
padding: calc(var(--base-unit));
|
||||
font-size: calc(var(--base-unit) * 1.1);
|
||||
}
|
||||
.actions {
|
||||
margin: 0 8px;
|
||||
padding-top: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
font-size: calc(var(--base-unit) * 1);
|
||||
}
|
||||
.actions paper-button {
|
||||
min-width: calc(var(--base-unit) * 9);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
paper-button#disarm {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
.not-found {
|
||||
flex: 1;
|
||||
background-color: yellow;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
<ha-card
|
||||
header$="[[_computeHeader(localize, _stateObj)]]"
|
||||
class$="[[_computeClassName(_stateObj)]]"
|
||||
>
|
||||
<template is="dom-if" if="[[_stateObj]]">
|
||||
<ha-label-badge
|
||||
class$="[[_stateObj.state]]"
|
||||
icon="[[_computeIcon(_stateObj)]]"
|
||||
label="[[_stateIconLabel(_stateObj.state)]]"
|
||||
></ha-label-badge>
|
||||
<template is="dom-if" if="[[_showActionToggle(_stateObj.state)]]">
|
||||
<div id="armActions" class="actions">
|
||||
<template is="dom-repeat" items="[[_config.states]]">
|
||||
<paper-button noink raised id="[[item]]" on-click="_handleActionClick">[[_label(localize, item)]]</paper-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_showActionToggle(_stateObj.state)]]">
|
||||
<div id="disarmActions" class="actions">
|
||||
<paper-button noink raised id="disarm" on-click="_handleActionClick">[[_label(localize, "disarm")]]</paper-button>
|
||||
</div>
|
||||
</template>
|
||||
<paper-input label="Alarm Code" type="password" value="[[_value]]"></paper-input>
|
||||
<div id="keypad">
|
||||
<div>
|
||||
<paper-button noink raised value="1" on-click="_handlePadClick">1</paper-button>
|
||||
<paper-button noink raised value="4" on-click="_handlePadClick">4</paper-button>
|
||||
<paper-button noink raised value="7" on-click="_handlePadClick">7</paper-button>
|
||||
</div>
|
||||
<div>
|
||||
<paper-button noink raised value="2" on-click="_handlePadClick">2</paper-button>
|
||||
<paper-button noink raised value="5" on-click="_handlePadClick">5</paper-button>
|
||||
<paper-button noink raised value="8" on-click="_handlePadClick">8</paper-button>
|
||||
<paper-button noink raised value="0" on-click="_handlePadClick">0</paper-button>
|
||||
</div>
|
||||
<div>
|
||||
<paper-button noink raised value="3" on-click="_handlePadClick">3</paper-button>
|
||||
<paper-button noink raised value="6" on-click="_handlePadClick">6</paper-button>
|
||||
<paper-button noink raised value="9" on-click="_handlePadClick">9</paper-button>
|
||||
<paper-button noink raised value="clear" on-click="_handlePadClick">[[_label(localize, "clear_code")]]</paper-button>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_stateObj]]">
|
||||
<div>Entity not available: [[_config.entity]]</div>
|
||||
</template>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
_value: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (
|
||||
!config ||
|
||||
!config.entity ||
|
||||
config.entity.split(".")[0] !== "alarm_control_panel"
|
||||
) {
|
||||
throw new Error("Invalid card configuration");
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
states: ["arm_away", "arm_home"],
|
||||
};
|
||||
|
||||
this._config = { ...defaults, ...config };
|
||||
this._icons = Icons;
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
_computeHeader(localize, stateObj) {
|
||||
if (!stateObj) return "";
|
||||
return this._config.title
|
||||
? this._config.title
|
||||
: this._label(localize, stateObj.state);
|
||||
}
|
||||
|
||||
_computeIcon(stateObj) {
|
||||
return this._icons[stateObj.state] || "hass:shield-outline";
|
||||
}
|
||||
|
||||
_label(localize, state) {
|
||||
return (
|
||||
localize(`state.alarm_control_panel.${state}`) ||
|
||||
localize(`ui.card.alarm_control_panel.${state}`)
|
||||
);
|
||||
}
|
||||
|
||||
_stateIconLabel(state) {
|
||||
const stateLabel = state.split("_").pop();
|
||||
return stateLabel === "disarmed" || stateLabel === "triggered"
|
||||
? ""
|
||||
: stateLabel;
|
||||
}
|
||||
|
||||
_showActionToggle(state) {
|
||||
return state === "disarmed";
|
||||
}
|
||||
|
||||
_computeClassName(stateObj) {
|
||||
if (!stateObj) return "not-found";
|
||||
return "";
|
||||
}
|
||||
|
||||
_handlePadClick(e) {
|
||||
const val = e.target.getAttribute("value");
|
||||
this._value = val === "clear" ? "" : this._value + val;
|
||||
}
|
||||
|
||||
_handleActionClick(e) {
|
||||
this.hass.callService("alarm_control_panel", "alarm_" + e.target.id, {
|
||||
entity_id: this._stateObj.entity_id,
|
||||
code: this._value,
|
||||
});
|
||||
this._value = "";
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-alarm-panel-card", HuiAlarmPanelCard);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import "../../../components/ha-label-badge";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
|
||||
const Icons = {
|
||||
armed_away: "hass:security-lock",
|
||||
armed_custom_bypass: "hass:security",
|
||||
armed_home: "hass:security-home",
|
||||
armed_night: "hass:security-home",
|
||||
disarmed: "hass:verified",
|
||||
pending: "hass:shield-outline",
|
||||
triggered: "hass:bell-ring",
|
||||
};
|
||||
|
||||
class HuiAlarmPanelCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
padding-bottom: 16px;
|
||||
position: relative;
|
||||
--alarm-color-disarmed: var(--label-badge-green);
|
||||
--alarm-color-pending: var(--label-badge-yellow);
|
||||
--alarm-color-triggered: var(--label-badge-red);
|
||||
--alarm-color-armed: var(--label-badge-red);
|
||||
--alarm-color-autoarm: rgba(0, 153, 255, .1);
|
||||
--alarm-state-color: var(--alarm-color-armed);
|
||||
--base-unit: 15px;
|
||||
font-size: calc(var(--base-unit));
|
||||
}
|
||||
ha-label-badge {
|
||||
--ha-label-badge-color: var(--alarm-state-color);
|
||||
--label-badge-text-color: var(--alarm-state-color);
|
||||
color: var(--alarm-state-color);
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
}
|
||||
.disarmed {
|
||||
--alarm-state-color: var(--alarm-color-disarmed);
|
||||
}
|
||||
.triggered {
|
||||
--alarm-state-color: var(--alarm-color-triggered);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
.arming {
|
||||
--alarm-state-color: var(--alarm-color-pending);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
.pending {
|
||||
--alarm-state-color: var(--alarm-color-pending);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
--ha-label-badge-color: var(--alarm-state-color);
|
||||
}
|
||||
100% {
|
||||
--ha-label-badge-color: rgba(255, 153, 0, 0.3);
|
||||
}
|
||||
}
|
||||
paper-input {
|
||||
margin: auto;
|
||||
max-width: 200px;
|
||||
font-size: calc(var(--base-unit));
|
||||
}
|
||||
.state {
|
||||
margin-left: 16px;
|
||||
font-size: calc(var(--base-unit) * 0.9);
|
||||
position: relative;
|
||||
bottom: 16px;
|
||||
color: var(--alarm-state-color);
|
||||
animation: none;
|
||||
}
|
||||
#keypad {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
#keypad div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#keypad paper-button {
|
||||
margin-bottom: 10%;
|
||||
position: relative;
|
||||
padding: calc(var(--base-unit));
|
||||
font-size: calc(var(--base-unit) * 1.1);
|
||||
}
|
||||
.actions {
|
||||
margin: 0 8px;
|
||||
padding-top: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
font-size: calc(var(--base-unit) * 1);
|
||||
}
|
||||
.actions paper-button {
|
||||
min-width: calc(var(--base-unit) * 9);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
paper-button#disarm {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
.not-found {
|
||||
flex: 1;
|
||||
background-color: yellow;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
<ha-card
|
||||
header$="[[_computeHeader(localize, _stateObj)]]"
|
||||
class$="[[_computeClassName(_stateObj)]]"
|
||||
>
|
||||
<template is="dom-if" if="[[_stateObj]]">
|
||||
<ha-label-badge
|
||||
class$="[[_stateObj.state]]"
|
||||
icon="[[_computeIcon(_stateObj)]]"
|
||||
label="[[_stateIconLabel(_stateObj.state)]]"
|
||||
></ha-label-badge>
|
||||
<template is="dom-if" if="[[_showActionToggle(_stateObj.state)]]">
|
||||
<div id="armActions" class="actions">
|
||||
<template is="dom-repeat" items="[[_config.states]]">
|
||||
<paper-button noink raised id="[[item]]" on-click="_handleActionClick">[[_label(localize, item)]]</paper-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_showActionToggle(_stateObj.state)]]">
|
||||
<div id="disarmActions" class="actions">
|
||||
<paper-button noink raised id="disarm" on-click="_handleActionClick">[[_label(localize, "disarm")]]</paper-button>
|
||||
</div>
|
||||
</template>
|
||||
<paper-input label="Alarm Code" type="password" value="[[_value]]"></paper-input>
|
||||
<div id="keypad">
|
||||
<div>
|
||||
<paper-button noink raised value="1" on-click="_handlePadClick">1</paper-button>
|
||||
<paper-button noink raised value="4" on-click="_handlePadClick">4</paper-button>
|
||||
<paper-button noink raised value="7" on-click="_handlePadClick">7</paper-button>
|
||||
</div>
|
||||
<div>
|
||||
<paper-button noink raised value="2" on-click="_handlePadClick">2</paper-button>
|
||||
<paper-button noink raised value="5" on-click="_handlePadClick">5</paper-button>
|
||||
<paper-button noink raised value="8" on-click="_handlePadClick">8</paper-button>
|
||||
<paper-button noink raised value="0" on-click="_handlePadClick">0</paper-button>
|
||||
</div>
|
||||
<div>
|
||||
<paper-button noink raised value="3" on-click="_handlePadClick">3</paper-button>
|
||||
<paper-button noink raised value="6" on-click="_handlePadClick">6</paper-button>
|
||||
<paper-button noink raised value="9" on-click="_handlePadClick">9</paper-button>
|
||||
<paper-button noink raised value="clear" on-click="_handlePadClick">[[_label(localize, "clear_code")]]</paper-button>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_stateObj]]">
|
||||
<div>Entity not available: [[_config.entity]]</div>
|
||||
</template>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
_value: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (
|
||||
!config ||
|
||||
!config.entity ||
|
||||
config.entity.split(".")[0] !== "alarm_control_panel"
|
||||
) {
|
||||
throw new Error("Invalid card configuration");
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
states: ["arm_away", "arm_home"],
|
||||
};
|
||||
|
||||
this._config = { ...defaults, ...config };
|
||||
this._icons = Icons;
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
_computeHeader(localize, stateObj) {
|
||||
if (!stateObj) return "";
|
||||
return this._config.title
|
||||
? this._config.title
|
||||
: this._label(localize, stateObj.state);
|
||||
}
|
||||
|
||||
_computeIcon(stateObj) {
|
||||
return this._icons[stateObj.state] || "hass:shield-outline";
|
||||
}
|
||||
|
||||
_label(localize, state) {
|
||||
return (
|
||||
localize(`state.alarm_control_panel.${state}`) ||
|
||||
localize(`ui.card.alarm_control_panel.${state}`)
|
||||
);
|
||||
}
|
||||
|
||||
_stateIconLabel(state) {
|
||||
const stateLabel = state.split("_").pop();
|
||||
return stateLabel === "disarmed" || stateLabel === "triggered"
|
||||
? ""
|
||||
: stateLabel;
|
||||
}
|
||||
|
||||
_showActionToggle(state) {
|
||||
return state === "disarmed";
|
||||
}
|
||||
|
||||
_computeClassName(stateObj) {
|
||||
if (!stateObj) return "not-found";
|
||||
return "";
|
||||
}
|
||||
|
||||
_handlePadClick(e) {
|
||||
const val = e.target.getAttribute("value");
|
||||
this._value = val === "clear" ? "" : this._value + val;
|
||||
}
|
||||
|
||||
_handleActionClick(e) {
|
||||
this.hass.callService("alarm_control_panel", "alarm_" + e.target.id, {
|
||||
entity_id: this._stateObj.entity_id,
|
||||
code: this._value,
|
||||
});
|
||||
this._value = "";
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-alarm-panel-card", HuiAlarmPanelCard);
|
||||
|
|
|
@ -1,83 +1,83 @@
|
|||
import computeCardSize from "../common/compute-card-size";
|
||||
import createCardElement from "../common/create-card-element";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
|
||||
interface Condition {
|
||||
entity: string;
|
||||
state?: string;
|
||||
state_not?: string;
|
||||
}
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
card: LovelaceConfig;
|
||||
conditions: Condition[];
|
||||
}
|
||||
|
||||
class HuiConditionalCard extends HTMLElement implements LovelaceCard {
|
||||
private _hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
private _card?: LovelaceCard;
|
||||
|
||||
public setConfig(config) {
|
||||
if (
|
||||
!config.card ||
|
||||
!config.conditions ||
|
||||
!Array.isArray(config.conditions) ||
|
||||
!config.conditions.every((c) => c.entity && (c.state || c.state_not))
|
||||
) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
if (this._card && this._card.parentElement) {
|
||||
this.removeChild(this._card);
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
this._card = createCardElement(config.card);
|
||||
if (this._hass) {
|
||||
this.hass = this._hass;
|
||||
}
|
||||
}
|
||||
|
||||
set hass(hass: HomeAssistant) {
|
||||
this._hass = hass;
|
||||
|
||||
if (!this._card) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visible =
|
||||
this._config &&
|
||||
this._config.conditions.every((c) => {
|
||||
if (!(c.entity in hass.states)) {
|
||||
return false;
|
||||
}
|
||||
if (c.state) {
|
||||
return hass.states[c.entity].state === c.state;
|
||||
}
|
||||
return hass.states[c.entity].state !== c.state_not;
|
||||
});
|
||||
|
||||
if (visible) {
|
||||
this._card.hass = hass;
|
||||
if (!this._card.parentElement) {
|
||||
this.appendChild(this._card);
|
||||
}
|
||||
} else if (this._card.parentElement) {
|
||||
this.removeChild(this._card);
|
||||
}
|
||||
}
|
||||
|
||||
public getCardSize() {
|
||||
return computeCardSize(this._card);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-conditional-card": HuiConditionalCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-conditional-card", HuiConditionalCard);
|
||||
import computeCardSize from "../common/compute-card-size";
|
||||
import createCardElement from "../common/create-card-element";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
|
||||
interface Condition {
|
||||
entity: string;
|
||||
state?: string;
|
||||
state_not?: string;
|
||||
}
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
card: LovelaceConfig;
|
||||
conditions: Condition[];
|
||||
}
|
||||
|
||||
class HuiConditionalCard extends HTMLElement implements LovelaceCard {
|
||||
private _hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
private _card?: LovelaceCard;
|
||||
|
||||
public setConfig(config) {
|
||||
if (
|
||||
!config.card ||
|
||||
!config.conditions ||
|
||||
!Array.isArray(config.conditions) ||
|
||||
!config.conditions.every((c) => c.entity && (c.state || c.state_not))
|
||||
) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
if (this._card && this._card.parentElement) {
|
||||
this.removeChild(this._card);
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
this._card = createCardElement(config.card);
|
||||
if (this._hass) {
|
||||
this.hass = this._hass;
|
||||
}
|
||||
}
|
||||
|
||||
set hass(hass: HomeAssistant) {
|
||||
this._hass = hass;
|
||||
|
||||
if (!this._card) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visible =
|
||||
this._config &&
|
||||
this._config.conditions.every((c) => {
|
||||
if (!(c.entity in hass.states)) {
|
||||
return false;
|
||||
}
|
||||
if (c.state) {
|
||||
return hass.states[c.entity].state === c.state;
|
||||
}
|
||||
return hass.states[c.entity].state !== c.state_not;
|
||||
});
|
||||
|
||||
if (visible) {
|
||||
this._card.hass = hass;
|
||||
if (!this._card.parentElement) {
|
||||
this.appendChild(this._card);
|
||||
}
|
||||
} else if (this._card.parentElement) {
|
||||
this.removeChild(this._card);
|
||||
}
|
||||
}
|
||||
|
||||
public getCardSize() {
|
||||
return computeCardSize(this._card);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-conditional-card": HuiConditionalCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-conditional-card", HuiConditionalCard);
|
||||
|
|
|
@ -1,187 +1,187 @@
|
|||
import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
} from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../components/hui-entities-toggle";
|
||||
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { EntityConfig, EntityRow } from "../entity-rows/types";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
import createRowElement from "../common/create-row-element";
|
||||
import computeDomain from "../../../common/entity/compute_domain";
|
||||
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
|
||||
|
||||
interface ConfigEntity extends EntityConfig {
|
||||
type?: string;
|
||||
secondary_info: "entity-id" | "last-changed";
|
||||
action_name?: string;
|
||||
service?: string;
|
||||
service_data?: object;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
show_header_toggle?: boolean;
|
||||
title?: string;
|
||||
entities: ConfigEntity[];
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
class HuiEntitiesCard extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceCard {
|
||||
protected _hass?: HomeAssistant;
|
||||
protected _config?: Config;
|
||||
protected _configEntities?: ConfigEntity[];
|
||||
|
||||
set hass(hass: HomeAssistant) {
|
||||
this._hass = hass;
|
||||
this.shadowRoot!.querySelectorAll("#states > div > *").forEach(
|
||||
(element: unknown) => {
|
||||
(element as EntityRow).hass = hass;
|
||||
}
|
||||
);
|
||||
const entitiesToggle = this.shadowRoot!.querySelector(
|
||||
"hui-entities-toggle"
|
||||
);
|
||||
if (entitiesToggle) {
|
||||
(entitiesToggle as any).hass = hass;
|
||||
}
|
||||
}
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
if (!this._config) {
|
||||
return 0;
|
||||
}
|
||||
// +1 for the header
|
||||
return (this._config.title ? 1 : 0) + this._config.entities.length;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
const entities = processConfigEntities(config.entities);
|
||||
|
||||
this._config = { theme: "default", ...config };
|
||||
this._configEntities = entities;
|
||||
}
|
||||
|
||||
protected updated(_changedProperties: PropertyValues): void {
|
||||
if (this._hass && this._config) {
|
||||
applyThemesOnElement(this, this._hass.themes, this._config.theme);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config || !this._hass) {
|
||||
return html``;
|
||||
}
|
||||
const { show_header_toggle, title } = this._config;
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card>
|
||||
${
|
||||
!title && !show_header_toggle
|
||||
? html``
|
||||
: html`
|
||||
<div class='header'>
|
||||
<div class="name">${title}</div>
|
||||
${
|
||||
show_header_toggle === false
|
||||
? html``
|
||||
: html`
|
||||
<hui-entities-toggle
|
||||
.hass="${this._hass}"
|
||||
.entities="${this._configEntities!.map(
|
||||
(conf) => conf.entity
|
||||
)}"
|
||||
></hui-entities-toggle>`
|
||||
}
|
||||
</div>`
|
||||
}
|
||||
<div id="states">
|
||||
${this._configEntities!.map((entityConf) =>
|
||||
this.renderEntity(entityConf)
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
padding: 16px;
|
||||
}
|
||||
#states {
|
||||
margin: -4px 0;
|
||||
}
|
||||
#states > * {
|
||||
margin: 8px 0;
|
||||
}
|
||||
#states > div > * {
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
@apply --paper-font-headline;
|
||||
/* overwriting line-height +8 because entity-toggle can be 40px height,
|
||||
compensating this with reduced padding */
|
||||
line-height: 40px;
|
||||
color: var(--primary-text-color);
|
||||
padding: 4px 0 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.header .name {
|
||||
@apply --paper-font-common-nowrap;
|
||||
}
|
||||
.state-card-dialog {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEntity(entityConf: ConfigEntity): TemplateResult {
|
||||
const element = createRowElement(entityConf);
|
||||
if (this._hass) {
|
||||
element.hass = this._hass;
|
||||
}
|
||||
if (
|
||||
entityConf.entity &&
|
||||
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(entityConf.entity))
|
||||
) {
|
||||
element.classList.add("state-card-dialog");
|
||||
element.addEventListener("click", () => this._handleClick(entityConf));
|
||||
}
|
||||
|
||||
return html`<div>${element}</div>`;
|
||||
}
|
||||
|
||||
private _handleClick(entityConf: ConfigEntity): void {
|
||||
const entityId = entityConf.entity;
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-entities-card": HuiEntitiesCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-entities-card", HuiEntitiesCard);
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
} from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../components/hui-entities-toggle";
|
||||
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { EntityConfig, EntityRow } from "../entity-rows/types";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
import createRowElement from "../common/create-row-element";
|
||||
import computeDomain from "../../../common/entity/compute_domain";
|
||||
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
|
||||
|
||||
interface ConfigEntity extends EntityConfig {
|
||||
type?: string;
|
||||
secondary_info: "entity-id" | "last-changed";
|
||||
action_name?: string;
|
||||
service?: string;
|
||||
service_data?: object;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
show_header_toggle?: boolean;
|
||||
title?: string;
|
||||
entities: ConfigEntity[];
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
class HuiEntitiesCard extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceCard {
|
||||
protected _hass?: HomeAssistant;
|
||||
protected _config?: Config;
|
||||
protected _configEntities?: ConfigEntity[];
|
||||
|
||||
set hass(hass: HomeAssistant) {
|
||||
this._hass = hass;
|
||||
this.shadowRoot!.querySelectorAll("#states > div > *").forEach(
|
||||
(element: unknown) => {
|
||||
(element as EntityRow).hass = hass;
|
||||
}
|
||||
);
|
||||
const entitiesToggle = this.shadowRoot!.querySelector(
|
||||
"hui-entities-toggle"
|
||||
);
|
||||
if (entitiesToggle) {
|
||||
(entitiesToggle as any).hass = hass;
|
||||
}
|
||||
}
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
if (!this._config) {
|
||||
return 0;
|
||||
}
|
||||
// +1 for the header
|
||||
return (this._config.title ? 1 : 0) + this._config.entities.length;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
const entities = processConfigEntities(config.entities);
|
||||
|
||||
this._config = { theme: "default", ...config };
|
||||
this._configEntities = entities;
|
||||
}
|
||||
|
||||
protected updated(_changedProperties: PropertyValues): void {
|
||||
if (this._hass && this._config) {
|
||||
applyThemesOnElement(this, this._hass.themes, this._config.theme);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config || !this._hass) {
|
||||
return html``;
|
||||
}
|
||||
const { show_header_toggle, title } = this._config;
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card>
|
||||
${
|
||||
!title && !show_header_toggle
|
||||
? html``
|
||||
: html`
|
||||
<div class='header'>
|
||||
<div class="name">${title}</div>
|
||||
${
|
||||
show_header_toggle === false
|
||||
? html``
|
||||
: html`
|
||||
<hui-entities-toggle
|
||||
.hass="${this._hass}"
|
||||
.entities="${this._configEntities!.map(
|
||||
(conf) => conf.entity
|
||||
)}"
|
||||
></hui-entities-toggle>`
|
||||
}
|
||||
</div>`
|
||||
}
|
||||
<div id="states">
|
||||
${this._configEntities!.map((entityConf) =>
|
||||
this.renderEntity(entityConf)
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
padding: 16px;
|
||||
}
|
||||
#states {
|
||||
margin: -4px 0;
|
||||
}
|
||||
#states > * {
|
||||
margin: 8px 0;
|
||||
}
|
||||
#states > div > * {
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
@apply --paper-font-headline;
|
||||
/* overwriting line-height +8 because entity-toggle can be 40px height,
|
||||
compensating this with reduced padding */
|
||||
line-height: 40px;
|
||||
color: var(--primary-text-color);
|
||||
padding: 4px 0 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.header .name {
|
||||
@apply --paper-font-common-nowrap;
|
||||
}
|
||||
.state-card-dialog {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEntity(entityConf: ConfigEntity): TemplateResult {
|
||||
const element = createRowElement(entityConf);
|
||||
if (this._hass) {
|
||||
element.hass = this._hass;
|
||||
}
|
||||
if (
|
||||
entityConf.entity &&
|
||||
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(entityConf.entity))
|
||||
) {
|
||||
element.classList.add("state-card-dialog");
|
||||
element.addEventListener("click", () => this._handleClick(entityConf));
|
||||
}
|
||||
|
||||
return html`<div>${element}</div>`;
|
||||
}
|
||||
|
||||
private _handleClick(entityConf: ConfigEntity): void {
|
||||
const entityId = entityConf.entity;
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-entities-card": HuiEntitiesCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-entities-card", HuiEntitiesCard);
|
||||
|
|
|
@ -1,77 +1,77 @@
|
|||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import createCardElement from "../common/create-card-element";
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
|
||||
function getEntities(hass, filterState, entities) {
|
||||
return entities.filter((entityConf) => {
|
||||
const stateObj = hass.states[entityConf.entity];
|
||||
return stateObj && filterState.includes(stateObj.state);
|
||||
});
|
||||
}
|
||||
|
||||
class HuiEntitiesCard extends PolymerElement {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_hassChanged",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return this.lastChild ? this.lastChild.getCardSize() : 1;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config.state_filter || !Array.isArray(config.state_filter)) {
|
||||
throw new Error("Incorrect filter config.");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
this._configEntities = processConfigEntities(config.entities);
|
||||
|
||||
if (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
this._element = null;
|
||||
}
|
||||
|
||||
const card = "card" in config ? { ...config.card } : {};
|
||||
if (!card.type) card.type = "entities";
|
||||
card.entities = [];
|
||||
|
||||
const element = createCardElement(card);
|
||||
element._filterRawConfig = card;
|
||||
this._updateCardConfig(element);
|
||||
|
||||
this._element = element;
|
||||
}
|
||||
|
||||
_hassChanged() {
|
||||
this._updateCardConfig(this._element);
|
||||
}
|
||||
|
||||
_updateCardConfig(element) {
|
||||
if (!element || element.tagName === "HUI-ERROR-CARD" || !this.hass) return;
|
||||
const entitiesList = getEntities(
|
||||
this.hass,
|
||||
this._config.state_filter,
|
||||
this._configEntities
|
||||
);
|
||||
|
||||
if (entitiesList.length === 0 && this._config.show_empty === false) {
|
||||
this.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
this.style.display = "block";
|
||||
element.setConfig({ ...element._filterRawConfig, entities: entitiesList });
|
||||
element.isPanel = this.isPanel;
|
||||
element.hass = this.hass;
|
||||
|
||||
// Attach element if it has never been attached.
|
||||
if (!this.lastChild) this.appendChild(element);
|
||||
}
|
||||
}
|
||||
customElements.define("hui-entity-filter-card", HuiEntitiesCard);
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import createCardElement from "../common/create-card-element";
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
|
||||
function getEntities(hass, filterState, entities) {
|
||||
return entities.filter((entityConf) => {
|
||||
const stateObj = hass.states[entityConf.entity];
|
||||
return stateObj && filterState.includes(stateObj.state);
|
||||
});
|
||||
}
|
||||
|
||||
class HuiEntitiesCard extends PolymerElement {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_hassChanged",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return this.lastChild ? this.lastChild.getCardSize() : 1;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config.state_filter || !Array.isArray(config.state_filter)) {
|
||||
throw new Error("Incorrect filter config.");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
this._configEntities = processConfigEntities(config.entities);
|
||||
|
||||
if (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
this._element = null;
|
||||
}
|
||||
|
||||
const card = "card" in config ? { ...config.card } : {};
|
||||
if (!card.type) card.type = "entities";
|
||||
card.entities = [];
|
||||
|
||||
const element = createCardElement(card);
|
||||
element._filterRawConfig = card;
|
||||
this._updateCardConfig(element);
|
||||
|
||||
this._element = element;
|
||||
}
|
||||
|
||||
_hassChanged() {
|
||||
this._updateCardConfig(this._element);
|
||||
}
|
||||
|
||||
_updateCardConfig(element) {
|
||||
if (!element || element.tagName === "HUI-ERROR-CARD" || !this.hass) return;
|
||||
const entitiesList = getEntities(
|
||||
this.hass,
|
||||
this._config.state_filter,
|
||||
this._configEntities
|
||||
);
|
||||
|
||||
if (entitiesList.length === 0 && this._config.show_empty === false) {
|
||||
this.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
this.style.display = "block";
|
||||
element.setConfig({ ...element._filterRawConfig, entities: entitiesList });
|
||||
element.isPanel = this.isPanel;
|
||||
element.hass = this.hass;
|
||||
|
||||
// Attach element if it has never been attached.
|
||||
if (!this.lastChild) this.appendChild(element);
|
||||
}
|
||||
}
|
||||
customElements.define("hui-entity-filter-card", HuiEntitiesCard);
|
||||
|
|
|
@ -1,273 +1,273 @@
|
|||
import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
} from "@polymer/lit-element";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { TemplateResult } from "lit-html";
|
||||
import isValidEntityId from "../../../common/entity/valid_entity_id";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
entity: string;
|
||||
title?: string;
|
||||
unit_of_measurement?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
severity?: object;
|
||||
}
|
||||
|
||||
const severityMap = {
|
||||
red: "var(--label-badge-red)",
|
||||
green: "var(--label-badge-green)",
|
||||
yellow: "var(--label-badge-yellow)",
|
||||
normal: "var(--label-badge-blue)",
|
||||
};
|
||||
|
||||
class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 2;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Invalid card configuration");
|
||||
}
|
||||
if (!isValidEntityId(config.entity)) {
|
||||
throw new Error("Invalid Entity");
|
||||
}
|
||||
this._config = { min: 0, max: 100, ...config };
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config || !this.hass) {
|
||||
return html``;
|
||||
}
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
let error;
|
||||
if (!stateObj) {
|
||||
error = "Entity not available: " + this._config.entity;
|
||||
} else if (isNaN(Number(stateObj.state))) {
|
||||
error = "Entity is non-numeric: " + this._config.entity;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card @click="${this._handleClick}">
|
||||
${
|
||||
error
|
||||
? html`<div class="not-found">${error}</div>`
|
||||
: html`
|
||||
<div class='container'>
|
||||
<div class='gauge-a'></div>
|
||||
<div class='gauge-b'></div>
|
||||
<div class='gauge-c' id='gauge'></div>
|
||||
<div class='gauge-data'>
|
||||
<div id='percent'>${stateObj.state}
|
||||
${this._config.unit_of_measurement ||
|
||||
stateObj.attributes.unit_of_measurement ||
|
||||
""}
|
||||
</div>
|
||||
<div id='title'>${this._config.title}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.get("hass")) {
|
||||
return (
|
||||
(changedProps.get("hass") as any).states[this._config!.entity] !==
|
||||
this.hass!.states[this._config!.entity]
|
||||
);
|
||||
}
|
||||
if (changedProps.get("_config")) {
|
||||
return changedProps.get("_config") !== this._config;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected updated(): void {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.shadowRoot!.getElementById("gauge")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
if (isNaN(Number(stateObj.state))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const turn = this._translateTurn(Number(stateObj.state), this._config);
|
||||
|
||||
this.shadowRoot!.getElementById(
|
||||
"gauge"
|
||||
)!.style.cssText = `transform: rotate(${turn}turn); background-color: ${this._computeSeverity(
|
||||
stateObj.state,
|
||||
this._config.severity!
|
||||
)}`;
|
||||
|
||||
(this.shadowRoot!.querySelector(
|
||||
"ha-card"
|
||||
)! as HTMLElement).style.setProperty(
|
||||
"--base-unit",
|
||||
this._computeBaseUnit()
|
||||
);
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
--base-unit: 50px;
|
||||
height: calc(var(--base-unit)*3);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.container{
|
||||
width: calc(var(--base-unit) * 4);
|
||||
height: calc(var(--base-unit) * 2);
|
||||
position: absolute;
|
||||
top: calc(var(--base-unit)*1.5);
|
||||
left: 50%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.gauge-a{
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
background-color: var(--primary-background-color);
|
||||
width: calc(var(--base-unit) * 4);
|
||||
height: calc(var(--base-unit) * 2);
|
||||
top: 0%;
|
||||
border-radius:calc(var(--base-unit) * 2.5) calc(var(--base-unit) * 2.5) 0px 0px ;
|
||||
}
|
||||
.gauge-b{
|
||||
z-index: 3;
|
||||
position: absolute;
|
||||
background-color: var(--paper-card-background-color);
|
||||
width: calc(var(--base-unit) * 2.5);
|
||||
height: calc(var(--base-unit) * 1.25);
|
||||
top: calc(var(--base-unit) * 0.75);
|
||||
margin-left: calc(var(--base-unit) * 0.75);
|
||||
margin-right: auto;
|
||||
border-radius: calc(var(--base-unit) * 2.5) calc(var(--base-unit) * 2.5) 0px 0px ;
|
||||
}
|
||||
.gauge-c{
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
background-color: var(--label-badge-blue);
|
||||
width: calc(var(--base-unit) * 4);
|
||||
height: calc(var(--base-unit) * 2);
|
||||
top: calc(var(--base-unit) * 2);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-radius: 0px 0px calc(var(--base-unit) * 2) calc(var(--base-unit) * 2) ;
|
||||
transform-origin: center top;
|
||||
transition: all 1.3s ease-in-out;
|
||||
}
|
||||
.gauge-data{
|
||||
z-index: 4;
|
||||
color: var(--primary-text-color);
|
||||
line-height: calc(var(--base-unit) * 0.3);
|
||||
position: absolute;
|
||||
width: calc(var(--base-unit) * 4);
|
||||
height: calc(var(--base-unit) * 2.1);
|
||||
top: calc(var(--base-unit) * 1.2);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
transition: all 1s ease-out;
|
||||
}
|
||||
.gauge-data #percent{
|
||||
font-size: calc(var(--base-unit) * 0.55);
|
||||
}
|
||||
.gauge-data #title{
|
||||
padding-top: calc(var(--base-unit) * 0.15);
|
||||
font-size: calc(var(--base-unit) * 0.30);
|
||||
}
|
||||
.not-found {
|
||||
flex: 1;
|
||||
background-color: yellow;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeSeverity(stateValue: string, sections: object): string {
|
||||
const numberValue = Number(stateValue);
|
||||
|
||||
if (!sections) {
|
||||
return severityMap.normal;
|
||||
}
|
||||
|
||||
const sectionsArray = Object.keys(sections);
|
||||
const sortable = sectionsArray.map((severity) => [
|
||||
severity,
|
||||
sections[severity],
|
||||
]);
|
||||
|
||||
for (const severity of sortable) {
|
||||
if (severityMap[severity[0]] == null || isNaN(severity[1])) {
|
||||
return severityMap.normal;
|
||||
}
|
||||
}
|
||||
sortable.sort((a, b) => a[1] - b[1]);
|
||||
|
||||
if (numberValue >= sortable[0][1] && numberValue < sortable[1][1]) {
|
||||
return severityMap[sortable[0][0]];
|
||||
}
|
||||
if (numberValue >= sortable[1][1] && numberValue < sortable[2][1]) {
|
||||
return severityMap[sortable[1][0]];
|
||||
}
|
||||
if (numberValue >= sortable[2][1]) {
|
||||
return severityMap[sortable[2][0]];
|
||||
}
|
||||
return severityMap.normal;
|
||||
}
|
||||
|
||||
private _translateTurn(value: number, config: Config): number {
|
||||
const maxTurnValue = Math.min(Math.max(value, config.min!), config.max!);
|
||||
return (
|
||||
(5 * (maxTurnValue - config.min!)) / (config.max! - config.min!) / 10
|
||||
);
|
||||
}
|
||||
|
||||
private _computeBaseUnit(): string {
|
||||
return this.clientWidth < 200 ? this.clientWidth / 5 + "px" : "50px";
|
||||
}
|
||||
|
||||
private _handleClick(): void {
|
||||
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-gauge-card": HuiGaugeCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-gauge-card", HuiGaugeCard);
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
} from "@polymer/lit-element";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { TemplateResult } from "lit-html";
|
||||
import isValidEntityId from "../../../common/entity/valid_entity_id";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
entity: string;
|
||||
title?: string;
|
||||
unit_of_measurement?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
severity?: object;
|
||||
}
|
||||
|
||||
const severityMap = {
|
||||
red: "var(--label-badge-red)",
|
||||
green: "var(--label-badge-green)",
|
||||
yellow: "var(--label-badge-yellow)",
|
||||
normal: "var(--label-badge-blue)",
|
||||
};
|
||||
|
||||
class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 2;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Invalid card configuration");
|
||||
}
|
||||
if (!isValidEntityId(config.entity)) {
|
||||
throw new Error("Invalid Entity");
|
||||
}
|
||||
this._config = { min: 0, max: 100, ...config };
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config || !this.hass) {
|
||||
return html``;
|
||||
}
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
let error;
|
||||
if (!stateObj) {
|
||||
error = "Entity not available: " + this._config.entity;
|
||||
} else if (isNaN(Number(stateObj.state))) {
|
||||
error = "Entity is non-numeric: " + this._config.entity;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card @click="${this._handleClick}">
|
||||
${
|
||||
error
|
||||
? html`<div class="not-found">${error}</div>`
|
||||
: html`
|
||||
<div class='container'>
|
||||
<div class='gauge-a'></div>
|
||||
<div class='gauge-b'></div>
|
||||
<div class='gauge-c' id='gauge'></div>
|
||||
<div class='gauge-data'>
|
||||
<div id='percent'>${stateObj.state}
|
||||
${this._config.unit_of_measurement ||
|
||||
stateObj.attributes.unit_of_measurement ||
|
||||
""}
|
||||
</div>
|
||||
<div id='title'>${this._config.title}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.get("hass")) {
|
||||
return (
|
||||
(changedProps.get("hass") as any).states[this._config!.entity] !==
|
||||
this.hass!.states[this._config!.entity]
|
||||
);
|
||||
}
|
||||
if (changedProps.get("_config")) {
|
||||
return changedProps.get("_config") !== this._config;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected updated(): void {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.shadowRoot!.getElementById("gauge")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
if (isNaN(Number(stateObj.state))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const turn = this._translateTurn(Number(stateObj.state), this._config);
|
||||
|
||||
this.shadowRoot!.getElementById(
|
||||
"gauge"
|
||||
)!.style.cssText = `transform: rotate(${turn}turn); background-color: ${this._computeSeverity(
|
||||
stateObj.state,
|
||||
this._config.severity!
|
||||
)}`;
|
||||
|
||||
(this.shadowRoot!.querySelector(
|
||||
"ha-card"
|
||||
)! as HTMLElement).style.setProperty(
|
||||
"--base-unit",
|
||||
this._computeBaseUnit()
|
||||
);
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
--base-unit: 50px;
|
||||
height: calc(var(--base-unit)*3);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.container{
|
||||
width: calc(var(--base-unit) * 4);
|
||||
height: calc(var(--base-unit) * 2);
|
||||
position: absolute;
|
||||
top: calc(var(--base-unit)*1.5);
|
||||
left: 50%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.gauge-a{
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
background-color: var(--primary-background-color);
|
||||
width: calc(var(--base-unit) * 4);
|
||||
height: calc(var(--base-unit) * 2);
|
||||
top: 0%;
|
||||
border-radius:calc(var(--base-unit) * 2.5) calc(var(--base-unit) * 2.5) 0px 0px ;
|
||||
}
|
||||
.gauge-b{
|
||||
z-index: 3;
|
||||
position: absolute;
|
||||
background-color: var(--paper-card-background-color);
|
||||
width: calc(var(--base-unit) * 2.5);
|
||||
height: calc(var(--base-unit) * 1.25);
|
||||
top: calc(var(--base-unit) * 0.75);
|
||||
margin-left: calc(var(--base-unit) * 0.75);
|
||||
margin-right: auto;
|
||||
border-radius: calc(var(--base-unit) * 2.5) calc(var(--base-unit) * 2.5) 0px 0px ;
|
||||
}
|
||||
.gauge-c{
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
background-color: var(--label-badge-blue);
|
||||
width: calc(var(--base-unit) * 4);
|
||||
height: calc(var(--base-unit) * 2);
|
||||
top: calc(var(--base-unit) * 2);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-radius: 0px 0px calc(var(--base-unit) * 2) calc(var(--base-unit) * 2) ;
|
||||
transform-origin: center top;
|
||||
transition: all 1.3s ease-in-out;
|
||||
}
|
||||
.gauge-data{
|
||||
z-index: 4;
|
||||
color: var(--primary-text-color);
|
||||
line-height: calc(var(--base-unit) * 0.3);
|
||||
position: absolute;
|
||||
width: calc(var(--base-unit) * 4);
|
||||
height: calc(var(--base-unit) * 2.1);
|
||||
top: calc(var(--base-unit) * 1.2);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
transition: all 1s ease-out;
|
||||
}
|
||||
.gauge-data #percent{
|
||||
font-size: calc(var(--base-unit) * 0.55);
|
||||
}
|
||||
.gauge-data #title{
|
||||
padding-top: calc(var(--base-unit) * 0.15);
|
||||
font-size: calc(var(--base-unit) * 0.30);
|
||||
}
|
||||
.not-found {
|
||||
flex: 1;
|
||||
background-color: yellow;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeSeverity(stateValue: string, sections: object): string {
|
||||
const numberValue = Number(stateValue);
|
||||
|
||||
if (!sections) {
|
||||
return severityMap.normal;
|
||||
}
|
||||
|
||||
const sectionsArray = Object.keys(sections);
|
||||
const sortable = sectionsArray.map((severity) => [
|
||||
severity,
|
||||
sections[severity],
|
||||
]);
|
||||
|
||||
for (const severity of sortable) {
|
||||
if (severityMap[severity[0]] == null || isNaN(severity[1])) {
|
||||
return severityMap.normal;
|
||||
}
|
||||
}
|
||||
sortable.sort((a, b) => a[1] - b[1]);
|
||||
|
||||
if (numberValue >= sortable[0][1] && numberValue < sortable[1][1]) {
|
||||
return severityMap[sortable[0][0]];
|
||||
}
|
||||
if (numberValue >= sortable[1][1] && numberValue < sortable[2][1]) {
|
||||
return severityMap[sortable[1][0]];
|
||||
}
|
||||
if (numberValue >= sortable[2][1]) {
|
||||
return severityMap[sortable[2][0]];
|
||||
}
|
||||
return severityMap.normal;
|
||||
}
|
||||
|
||||
private _translateTurn(value: number, config: Config): number {
|
||||
const maxTurnValue = Math.min(Math.max(value, config.min!), config.max!);
|
||||
return (
|
||||
(5 * (maxTurnValue - config.min!)) / (config.max! - config.min!) / 10
|
||||
);
|
||||
}
|
||||
|
||||
private _computeBaseUnit(): string {
|
||||
return this.clientWidth < 200 ? this.clientWidth / 5 + "px" : "50px";
|
||||
}
|
||||
|
||||
private _handleClick(): void {
|
||||
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-gauge-card": HuiGaugeCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-gauge-card", HuiGaugeCard);
|
||||
|
|
|
@ -1,86 +1,86 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/state-history-charts";
|
||||
import "../../../data/ha-state-history-data";
|
||||
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
|
||||
class HuiHistoryGraphCard extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
padding: 16px;
|
||||
}
|
||||
ha-card[header] {
|
||||
padding-top: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card header$='[[_config.title]]'>
|
||||
<ha-state-history-data
|
||||
hass="[[hass]]"
|
||||
filter-type="recent-entity"
|
||||
entity-id="[[_entities]]"
|
||||
data="{{_stateHistory}}"
|
||||
is-loading="{{_stateHistoryLoading}}"
|
||||
cache-config="[[_cacheConfig]]"
|
||||
></ha-state-history-data>
|
||||
<state-history-charts
|
||||
hass="[[hass]]"
|
||||
history-data="[[_stateHistory]]"
|
||||
is-loading-data="[[_stateHistoryLoading]]"
|
||||
names="[[_names]]"
|
||||
up-to-now
|
||||
no-single
|
||||
></state-history-charts>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_names: Object,
|
||||
_entities: Array,
|
||||
|
||||
_stateHistory: Object,
|
||||
_stateHistoryLoading: Boolean,
|
||||
_cacheConfig: Object,
|
||||
};
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
const entities = processConfigEntities(config.entities);
|
||||
|
||||
this._config = config;
|
||||
|
||||
const _entities = [];
|
||||
const _names = {};
|
||||
for (const entity of entities) {
|
||||
_entities.push(entity.entity);
|
||||
if (entity.name) {
|
||||
_names[entity.entity] = entity.name;
|
||||
}
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
_cacheConfig: {
|
||||
cacheKey: _entities.sort().join(),
|
||||
hoursToShow: config.hours_to_show || 24,
|
||||
refresh: config.refresh_interval || 0,
|
||||
},
|
||||
_entities,
|
||||
_names,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-history-graph-card", HuiHistoryGraphCard);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/state-history-charts";
|
||||
import "../../../data/ha-state-history-data";
|
||||
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
|
||||
class HuiHistoryGraphCard extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
padding: 16px;
|
||||
}
|
||||
ha-card[header] {
|
||||
padding-top: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card header$='[[_config.title]]'>
|
||||
<ha-state-history-data
|
||||
hass="[[hass]]"
|
||||
filter-type="recent-entity"
|
||||
entity-id="[[_entities]]"
|
||||
data="{{_stateHistory}}"
|
||||
is-loading="{{_stateHistoryLoading}}"
|
||||
cache-config="[[_cacheConfig]]"
|
||||
></ha-state-history-data>
|
||||
<state-history-charts
|
||||
hass="[[hass]]"
|
||||
history-data="[[_stateHistory]]"
|
||||
is-loading-data="[[_stateHistoryLoading]]"
|
||||
names="[[_names]]"
|
||||
up-to-now
|
||||
no-single
|
||||
></state-history-charts>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_names: Object,
|
||||
_entities: Array,
|
||||
|
||||
_stateHistory: Object,
|
||||
_stateHistoryLoading: Boolean,
|
||||
_cacheConfig: Object,
|
||||
};
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
const entities = processConfigEntities(config.entities);
|
||||
|
||||
this._config = config;
|
||||
|
||||
const _entities = [];
|
||||
const _names = {};
|
||||
for (const entity of entities) {
|
||||
_entities.push(entity.entity);
|
||||
if (entity.name) {
|
||||
_names[entity.entity] = entity.name;
|
||||
}
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
_cacheConfig: {
|
||||
cacheKey: _entities.sort().join(),
|
||||
hoursToShow: config.hours_to_show || 24,
|
||||
refresh: config.refresh_interval || 0,
|
||||
},
|
||||
_entities,
|
||||
_names,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-history-graph-card", HuiHistoryGraphCard);
|
||||
|
|
|
@ -1,50 +1,50 @@
|
|||
import { html } from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import computeCardSize from "../common/compute-card-size";
|
||||
|
||||
import { HuiStackCard } from "./hui-stack-card";
|
||||
|
||||
class HuiHorizontalStackCard extends HuiStackCard {
|
||||
public getCardSize(): number {
|
||||
let totalSize = 0;
|
||||
|
||||
if (this._cards) {
|
||||
for (const element of this._cards) {
|
||||
const elementSize = computeCardSize(element);
|
||||
totalSize = elementSize > totalSize ? elementSize : totalSize;
|
||||
}
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
protected renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
#root {
|
||||
display: flex;
|
||||
}
|
||||
#root > * {
|
||||
flex: 1 1 0;
|
||||
margin: 0 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
#root > *:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
#root > *:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-horitzontal-stack-card": HuiHorizontalStackCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-horizontal-stack-card", HuiHorizontalStackCard);
|
||||
import { html } from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import computeCardSize from "../common/compute-card-size";
|
||||
|
||||
import { HuiStackCard } from "./hui-stack-card";
|
||||
|
||||
class HuiHorizontalStackCard extends HuiStackCard {
|
||||
public getCardSize(): number {
|
||||
let totalSize = 0;
|
||||
|
||||
if (this._cards) {
|
||||
for (const element of this._cards) {
|
||||
const elementSize = computeCardSize(element);
|
||||
totalSize = elementSize > totalSize ? elementSize : totalSize;
|
||||
}
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
protected renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
#root {
|
||||
display: flex;
|
||||
}
|
||||
#root > * {
|
||||
flex: 1 1 0;
|
||||
margin: 0 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
#root > *:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
#root > *:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-horitzontal-stack-card": HuiHorizontalStackCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-horizontal-stack-card", HuiHorizontalStackCard);
|
||||
|
|
|
@ -1,80 +1,80 @@
|
|||
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
aspect_ratio?: string;
|
||||
title?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
protected _config?: Config;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 1 + this.offsetHeight / 50;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config.url) {
|
||||
throw new Error("URL required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card .header="${this._config.title}">
|
||||
<div id="root">
|
||||
<iframe src="${this._config.url}"></iframe>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
#root {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-top: ${this._config!.aspect_ratio || "50%"};
|
||||
}
|
||||
iframe {
|
||||
position: absolute;
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-iframe-card": HuiIframeCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-iframe-card", HuiIframeCard);
|
||||
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
aspect_ratio?: string;
|
||||
title?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
protected _config?: Config;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 1 + this.offsetHeight / 50;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config.url) {
|
||||
throw new Error("URL required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card .header="${this._config.title}">
|
||||
<div id="root">
|
||||
<iframe src="${this._config.url}"></iframe>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
#root {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-top: ${this._config!.aspect_ratio || "50%"};
|
||||
}
|
||||
iframe {
|
||||
position: absolute;
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-iframe-card": HuiIframeCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-iframe-card", HuiIframeCard);
|
||||
|
|
|
@ -1,57 +1,57 @@
|
|||
import createErrorCardConfig from "../common/create-error-card-config";
|
||||
import computeDomain from "../../../common/entity/compute_domain";
|
||||
|
||||
export default class LegacyWrapperCard extends HTMLElement {
|
||||
constructor(tag, domain) {
|
||||
super();
|
||||
this._tag = tag.toUpperCase();
|
||||
this._domain = domain;
|
||||
this._element = null;
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config.entity) {
|
||||
throw new Error("No entity specified");
|
||||
}
|
||||
|
||||
if (computeDomain(config.entity) !== this._domain) {
|
||||
throw new Error(
|
||||
`Specified entity needs to be of domain ${this._domain}.`
|
||||
);
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
set hass(hass) {
|
||||
const entityId = this._config.entity;
|
||||
|
||||
if (entityId in hass.states) {
|
||||
this._ensureElement(this._tag);
|
||||
this.lastChild.hass = hass;
|
||||
this.lastChild.stateObj = hass.states[entityId];
|
||||
} else {
|
||||
this._ensureElement("HUI-ERROR-CARD");
|
||||
this.lastChild.setConfig(
|
||||
createErrorCardConfig(
|
||||
`No state available for ${entityId}`,
|
||||
this._config
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_ensureElement(tag) {
|
||||
if (this.lastChild && this.lastChild.tagName === tag) return;
|
||||
|
||||
if (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
|
||||
this.appendChild(document.createElement(tag));
|
||||
}
|
||||
}
|
||||
import createErrorCardConfig from "../common/create-error-card-config";
|
||||
import computeDomain from "../../../common/entity/compute_domain";
|
||||
|
||||
export default class LegacyWrapperCard extends HTMLElement {
|
||||
constructor(tag, domain) {
|
||||
super();
|
||||
this._tag = tag.toUpperCase();
|
||||
this._domain = domain;
|
||||
this._element = null;
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config.entity) {
|
||||
throw new Error("No entity specified");
|
||||
}
|
||||
|
||||
if (computeDomain(config.entity) !== this._domain) {
|
||||
throw new Error(
|
||||
`Specified entity needs to be of domain ${this._domain}.`
|
||||
);
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
set hass(hass) {
|
||||
const entityId = this._config.entity;
|
||||
|
||||
if (entityId in hass.states) {
|
||||
this._ensureElement(this._tag);
|
||||
this.lastChild.hass = hass;
|
||||
this.lastChild.stateObj = hass.states[entityId];
|
||||
} else {
|
||||
this._ensureElement("HUI-ERROR-CARD");
|
||||
this.lastChild.setConfig(
|
||||
createErrorCardConfig(
|
||||
`No state available for ${entityId}`,
|
||||
this._config
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_ensureElement(tag) {
|
||||
if (this.lastChild && this.lastChild.tagName === tag) return;
|
||||
|
||||
if (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
|
||||
this.appendChild(document.createElement(tag));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,327 +1,327 @@
|
|||
import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
PropertyDeclarations,
|
||||
} from "@polymer/lit-element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { styleMap } from "lit-html/directives/styleMap";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import stateIcon from "../../../common/entity/state_icon";
|
||||
import { jQuery } from "../../../resources/jquery";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
|
||||
|
||||
import { HomeAssistant, LightEntity } from "../../../types";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
const lightConfig = {
|
||||
radius: 80,
|
||||
step: 1,
|
||||
circleShape: "pie",
|
||||
startAngle: 315,
|
||||
width: 5,
|
||||
min: 1,
|
||||
max: 100,
|
||||
sliderType: "min-range",
|
||||
lineCap: "round",
|
||||
handleSize: "+12",
|
||||
showTooltip: false,
|
||||
};
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
entity: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceCard {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
private _brightnessTimout?: number;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 2;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config.entity || config.entity.split(".")[0] !== "light") {
|
||||
throw new Error("Specify an entity from within the light domain.");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this._config!.entity] as LightEntity;
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card>
|
||||
${
|
||||
!stateObj
|
||||
? html`
|
||||
<div class="not-found">Entity not available: ${
|
||||
this._config.entity
|
||||
}</div>`
|
||||
: html`
|
||||
<div id="light"></div>
|
||||
<div id="tooltip">
|
||||
<div class="icon-state">
|
||||
<ha-icon
|
||||
data-state="${stateObj.state}"
|
||||
.icon="${stateIcon(stateObj)}"
|
||||
style="${styleMap({
|
||||
filter: this._computeBrightness(stateObj),
|
||||
color: this._computeColor(stateObj),
|
||||
})}"
|
||||
@ha-click="${() => this._handleClick(false)}"
|
||||
@ha-hold="${() => this._handleClick(true)}"
|
||||
.longPress="${longPress()}"
|
||||
></ha-icon>
|
||||
<div
|
||||
class="brightness"
|
||||
@ha-click="${() => this._handleClick(false)}"
|
||||
@ha-hold="${() => this._handleClick(true)}"
|
||||
.longPress="${longPress()}"
|
||||
></div>
|
||||
<div class="name">${this._config.name ||
|
||||
computeStateName(stateObj)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.get("hass")) {
|
||||
return (
|
||||
(changedProps.get("hass") as any).states[this._config!.entity] !==
|
||||
this.hass!.states[this._config!.entity]
|
||||
);
|
||||
}
|
||||
return (changedProps as unknown) as boolean;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
const brightness = this.hass!.states[this._config!.entity].attributes
|
||||
.brightness;
|
||||
jQuery("#light", this.shadowRoot).roundSlider({
|
||||
...lightConfig,
|
||||
change: (value) => this._setBrightness(value),
|
||||
drag: (value) => this._dragEvent(value),
|
||||
start: () => this._showBrightness(),
|
||||
stop: () => this._hideBrightness(),
|
||||
});
|
||||
this.shadowRoot!.querySelector(".brightness")!.innerHTML =
|
||||
(Math.round((brightness / 254) * 100) || 0) + "%";
|
||||
}
|
||||
|
||||
protected updated(): void {
|
||||
const attrs = this.hass!.states[this._config!.entity].attributes;
|
||||
|
||||
jQuery("#light", this.shadowRoot).roundSlider({
|
||||
value: Math.round((attrs.brightness / 254) * 100) || 0,
|
||||
});
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
${roundSliderStyle}
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
--brightness-font-color: white;
|
||||
--brightness-font-text-shadow:
|
||||
-1px -1px 0 #000,
|
||||
1px -1px 0 #000,
|
||||
-1px 1px 0 #000,
|
||||
1px 1px 0 #000;
|
||||
--name-font-size: 1.2rem;
|
||||
--brightness-font-size: 1.2rem;
|
||||
--rail-border-color: transparent;
|
||||
}
|
||||
#tooltip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
z-index: 15;
|
||||
}
|
||||
.icon-state {
|
||||
display: block;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translate(0,25%);
|
||||
}
|
||||
#light {
|
||||
margin: 0 auto;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
#light .rs-bar.rs-transition.rs-first, .rs-bar.rs-transition.rs-second{
|
||||
z-index: 20 !important;
|
||||
}
|
||||
#light .rs-range-color {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
#light .rs-path-color {
|
||||
background-color: var(--disabled-text-color);
|
||||
}
|
||||
#light .rs-handle {
|
||||
background-color: var(--paper-card-background-color, white);
|
||||
padding: 7px;
|
||||
border: 2px solid var(--disabled-text-color);
|
||||
}
|
||||
#light .rs-handle.rs-focus {
|
||||
border-color:var(--primary-color);
|
||||
}
|
||||
#light .rs-handle:after {
|
||||
border-color: var(--primary-color);
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
#light .rs-border {
|
||||
border-color: var(--rail-border-color);
|
||||
}
|
||||
#light .rs-inner.rs-bg-color.rs-border,
|
||||
#light .rs-overlay.rs-transition.rs-bg-color {
|
||||
background-color: var(--paper-card-background-color, white);
|
||||
}
|
||||
ha-icon {
|
||||
margin: auto;
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
color: var(--paper-item-icon-color, #44739e);
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-icon[data-state=on] {
|
||||
color: var(--paper-item-icon-active-color, #FDD835);
|
||||
}
|
||||
ha-icon[data-state=unavailable] {
|
||||
color: var(--state-icon-unavailable-color);
|
||||
}
|
||||
.name {
|
||||
padding-top: 40px;
|
||||
font-size: var(--name-font-size);
|
||||
}
|
||||
.brightness {
|
||||
font-size: var(--brightness-font-size);
|
||||
position: absolute;
|
||||
margin: 0 auto;
|
||||
left: 50%;
|
||||
top: 10%;
|
||||
transform: translate(-50%);
|
||||
opacity: 0;
|
||||
transition: opacity .5s ease-in-out;
|
||||
-moz-transition: opacity .5s ease-in-out;
|
||||
-webkit-transition: opacity .5s ease-in-out;
|
||||
cursor: pointer;
|
||||
color: var(--brightness-font-color);
|
||||
text-shadow: var(--brightness-font-text-shadow)
|
||||
}
|
||||
.show_brightness {
|
||||
opacity: 1;
|
||||
}
|
||||
.not-found {
|
||||
flex: 1;
|
||||
background-color: yellow;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _dragEvent(e: any): void {
|
||||
this.shadowRoot!.querySelector(".brightness")!.innerHTML = e.value + "%";
|
||||
}
|
||||
|
||||
private _showBrightness(): void {
|
||||
clearTimeout(this._brightnessTimout);
|
||||
this.shadowRoot!.querySelector(".brightness")!.classList.add(
|
||||
"show_brightness"
|
||||
);
|
||||
}
|
||||
|
||||
private _hideBrightness(): void {
|
||||
this._brightnessTimout = window.setTimeout(() => {
|
||||
this.shadowRoot!.querySelector(".brightness")!.classList.remove(
|
||||
"show_brightness"
|
||||
);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private _setBrightness(e: any): void {
|
||||
this.hass!.callService("light", "turn_on", {
|
||||
entity_id: this._config!.entity,
|
||||
brightness_pct: e.value,
|
||||
});
|
||||
}
|
||||
|
||||
private _computeBrightness(stateObj: LightEntity): string {
|
||||
if (!stateObj.attributes.brightness) {
|
||||
return "";
|
||||
}
|
||||
const brightness = stateObj.attributes.brightness;
|
||||
return `brightness(${(brightness + 245) / 5}%)`;
|
||||
}
|
||||
|
||||
private _computeColor(stateObj: LightEntity): string {
|
||||
if (!stateObj.attributes.hs_color) {
|
||||
return "";
|
||||
}
|
||||
const [hue, sat] = stateObj.attributes.hs_color;
|
||||
if (sat <= 10) {
|
||||
return "";
|
||||
}
|
||||
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
|
||||
}
|
||||
|
||||
private _handleClick(hold: boolean): void {
|
||||
const entityId = this._config!.entity;
|
||||
|
||||
if (hold) {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass!.callService("light", "toggle", {
|
||||
entity_id: entityId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-light-card": HuiLightCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-light-card", HuiLightCard);
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
PropertyDeclarations,
|
||||
} from "@polymer/lit-element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { styleMap } from "lit-html/directives/styleMap";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import stateIcon from "../../../common/entity/state_icon";
|
||||
import { jQuery } from "../../../resources/jquery";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
|
||||
|
||||
import { HomeAssistant, LightEntity } from "../../../types";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
const lightConfig = {
|
||||
radius: 80,
|
||||
step: 1,
|
||||
circleShape: "pie",
|
||||
startAngle: 315,
|
||||
width: 5,
|
||||
min: 1,
|
||||
max: 100,
|
||||
sliderType: "min-range",
|
||||
lineCap: "round",
|
||||
handleSize: "+12",
|
||||
showTooltip: false,
|
||||
};
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
entity: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceCard {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
private _brightnessTimout?: number;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 2;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config.entity || config.entity.split(".")[0] !== "light") {
|
||||
throw new Error("Specify an entity from within the light domain.");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this._config!.entity] as LightEntity;
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card>
|
||||
${
|
||||
!stateObj
|
||||
? html`
|
||||
<div class="not-found">Entity not available: ${
|
||||
this._config.entity
|
||||
}</div>`
|
||||
: html`
|
||||
<div id="light"></div>
|
||||
<div id="tooltip">
|
||||
<div class="icon-state">
|
||||
<ha-icon
|
||||
data-state="${stateObj.state}"
|
||||
.icon="${stateIcon(stateObj)}"
|
||||
style="${styleMap({
|
||||
filter: this._computeBrightness(stateObj),
|
||||
color: this._computeColor(stateObj),
|
||||
})}"
|
||||
@ha-click="${() => this._handleClick(false)}"
|
||||
@ha-hold="${() => this._handleClick(true)}"
|
||||
.longPress="${longPress()}"
|
||||
></ha-icon>
|
||||
<div
|
||||
class="brightness"
|
||||
@ha-click="${() => this._handleClick(false)}"
|
||||
@ha-hold="${() => this._handleClick(true)}"
|
||||
.longPress="${longPress()}"
|
||||
></div>
|
||||
<div class="name">${this._config.name ||
|
||||
computeStateName(stateObj)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.get("hass")) {
|
||||
return (
|
||||
(changedProps.get("hass") as any).states[this._config!.entity] !==
|
||||
this.hass!.states[this._config!.entity]
|
||||
);
|
||||
}
|
||||
return (changedProps as unknown) as boolean;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
const brightness = this.hass!.states[this._config!.entity].attributes
|
||||
.brightness;
|
||||
jQuery("#light", this.shadowRoot).roundSlider({
|
||||
...lightConfig,
|
||||
change: (value) => this._setBrightness(value),
|
||||
drag: (value) => this._dragEvent(value),
|
||||
start: () => this._showBrightness(),
|
||||
stop: () => this._hideBrightness(),
|
||||
});
|
||||
this.shadowRoot!.querySelector(".brightness")!.innerHTML =
|
||||
(Math.round((brightness / 254) * 100) || 0) + "%";
|
||||
}
|
||||
|
||||
protected updated(): void {
|
||||
const attrs = this.hass!.states[this._config!.entity].attributes;
|
||||
|
||||
jQuery("#light", this.shadowRoot).roundSlider({
|
||||
value: Math.round((attrs.brightness / 254) * 100) || 0,
|
||||
});
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
${roundSliderStyle}
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
--brightness-font-color: white;
|
||||
--brightness-font-text-shadow:
|
||||
-1px -1px 0 #000,
|
||||
1px -1px 0 #000,
|
||||
-1px 1px 0 #000,
|
||||
1px 1px 0 #000;
|
||||
--name-font-size: 1.2rem;
|
||||
--brightness-font-size: 1.2rem;
|
||||
--rail-border-color: transparent;
|
||||
}
|
||||
#tooltip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
z-index: 15;
|
||||
}
|
||||
.icon-state {
|
||||
display: block;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translate(0,25%);
|
||||
}
|
||||
#light {
|
||||
margin: 0 auto;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
#light .rs-bar.rs-transition.rs-first, .rs-bar.rs-transition.rs-second{
|
||||
z-index: 20 !important;
|
||||
}
|
||||
#light .rs-range-color {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
#light .rs-path-color {
|
||||
background-color: var(--disabled-text-color);
|
||||
}
|
||||
#light .rs-handle {
|
||||
background-color: var(--paper-card-background-color, white);
|
||||
padding: 7px;
|
||||
border: 2px solid var(--disabled-text-color);
|
||||
}
|
||||
#light .rs-handle.rs-focus {
|
||||
border-color:var(--primary-color);
|
||||
}
|
||||
#light .rs-handle:after {
|
||||
border-color: var(--primary-color);
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
#light .rs-border {
|
||||
border-color: var(--rail-border-color);
|
||||
}
|
||||
#light .rs-inner.rs-bg-color.rs-border,
|
||||
#light .rs-overlay.rs-transition.rs-bg-color {
|
||||
background-color: var(--paper-card-background-color, white);
|
||||
}
|
||||
ha-icon {
|
||||
margin: auto;
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
color: var(--paper-item-icon-color, #44739e);
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-icon[data-state=on] {
|
||||
color: var(--paper-item-icon-active-color, #FDD835);
|
||||
}
|
||||
ha-icon[data-state=unavailable] {
|
||||
color: var(--state-icon-unavailable-color);
|
||||
}
|
||||
.name {
|
||||
padding-top: 40px;
|
||||
font-size: var(--name-font-size);
|
||||
}
|
||||
.brightness {
|
||||
font-size: var(--brightness-font-size);
|
||||
position: absolute;
|
||||
margin: 0 auto;
|
||||
left: 50%;
|
||||
top: 10%;
|
||||
transform: translate(-50%);
|
||||
opacity: 0;
|
||||
transition: opacity .5s ease-in-out;
|
||||
-moz-transition: opacity .5s ease-in-out;
|
||||
-webkit-transition: opacity .5s ease-in-out;
|
||||
cursor: pointer;
|
||||
color: var(--brightness-font-color);
|
||||
text-shadow: var(--brightness-font-text-shadow)
|
||||
}
|
||||
.show_brightness {
|
||||
opacity: 1;
|
||||
}
|
||||
.not-found {
|
||||
flex: 1;
|
||||
background-color: yellow;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _dragEvent(e: any): void {
|
||||
this.shadowRoot!.querySelector(".brightness")!.innerHTML = e.value + "%";
|
||||
}
|
||||
|
||||
private _showBrightness(): void {
|
||||
clearTimeout(this._brightnessTimout);
|
||||
this.shadowRoot!.querySelector(".brightness")!.classList.add(
|
||||
"show_brightness"
|
||||
);
|
||||
}
|
||||
|
||||
private _hideBrightness(): void {
|
||||
this._brightnessTimout = window.setTimeout(() => {
|
||||
this.shadowRoot!.querySelector(".brightness")!.classList.remove(
|
||||
"show_brightness"
|
||||
);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private _setBrightness(e: any): void {
|
||||
this.hass!.callService("light", "turn_on", {
|
||||
entity_id: this._config!.entity,
|
||||
brightness_pct: e.value,
|
||||
});
|
||||
}
|
||||
|
||||
private _computeBrightness(stateObj: LightEntity): string {
|
||||
if (!stateObj.attributes.brightness) {
|
||||
return "";
|
||||
}
|
||||
const brightness = stateObj.attributes.brightness;
|
||||
return `brightness(${(brightness + 245) / 5}%)`;
|
||||
}
|
||||
|
||||
private _computeColor(stateObj: LightEntity): string {
|
||||
if (!stateObj.attributes.hs_color) {
|
||||
return "";
|
||||
}
|
||||
const [hue, sat] = stateObj.attributes.hs_color;
|
||||
if (sat <= 10) {
|
||||
return "";
|
||||
}
|
||||
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
|
||||
}
|
||||
|
||||
private _handleClick(hold: boolean): void {
|
||||
const entityId = this._config!.entity;
|
||||
|
||||
if (hold) {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass!.callService("light", "toggle", {
|
||||
entity_id: entityId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-light-card": HuiLightCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-light-card", HuiLightCard);
|
||||
|
|
|
@ -1,305 +1,305 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import Leaflet from "leaflet";
|
||||
|
||||
import "../../map/ha-entity-marker";
|
||||
|
||||
import setupLeafletMap from "../../../common/dom/setup-leaflet-map";
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
import computeStateDomain from "../../../common/entity/compute_state_domain";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import debounce from "../../../common/util/debounce";
|
||||
|
||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
|
||||
|
||||
class HuiMapCard extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host([is-panel]) ha-card {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
/**
|
||||
* In panel mode we want a full height map. Since parent #view
|
||||
* only sets min-height, we need absolute positioning here
|
||||
*/
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#map {
|
||||
z-index: 0;
|
||||
border: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
paper-icon-button {
|
||||
position: absolute;
|
||||
top: 75px;
|
||||
left: 7px;
|
||||
}
|
||||
|
||||
#root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([is-panel]) #root {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card id="card" header="[[_config.title]]">
|
||||
<div id="root">
|
||||
<div id="map"></div>
|
||||
<paper-icon-button
|
||||
on-click="_fitMap"
|
||||
icon="hass:image-filter-center-focus"
|
||||
title="Reset focus"
|
||||
></paper-icon-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_drawEntities",
|
||||
},
|
||||
_config: Object,
|
||||
isPanel: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._debouncedResizeListener = debounce(this._resetMap.bind(this), 100);
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
|
||||
if (!this._config || this.isPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.root.style.paddingTop = this._config.aspect_ratio || "100%";
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
this._configEntities = processConfigEntities(config.entities);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
let ar = this._config.aspect_ratio || "100%";
|
||||
ar = ar.substr(0, ar.length - 1);
|
||||
return 1 + Math.floor(ar / 25) || 3;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// Observe changes to map size and invalidate to prevent broken rendering
|
||||
// Uses ResizeObserver in Chrome, otherwise window resize event
|
||||
if (typeof ResizeObserver === "function") {
|
||||
this._resizeObserver = new ResizeObserver(() =>
|
||||
this._debouncedResizeListener()
|
||||
);
|
||||
this._resizeObserver.observe(this.$.map);
|
||||
} else {
|
||||
window.addEventListener("resize", this._debouncedResizeListener);
|
||||
}
|
||||
|
||||
this._map = setupLeafletMap(this.$.map);
|
||||
this._drawEntities(this.hass);
|
||||
|
||||
setTimeout(() => {
|
||||
this._resetMap();
|
||||
this._fitMap();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
if (this._map) {
|
||||
this._map.remove();
|
||||
}
|
||||
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.unobserve(this.$.map);
|
||||
} else {
|
||||
window.removeEventListener("resize", this._debouncedResizeListener);
|
||||
}
|
||||
}
|
||||
|
||||
_resetMap() {
|
||||
if (!this._map) {
|
||||
return;
|
||||
}
|
||||
this._map.invalidateSize();
|
||||
}
|
||||
|
||||
_fitMap() {
|
||||
const zoom = this._config.default_zoom;
|
||||
if (this._mapItems.length === 0) {
|
||||
this._map.setView(
|
||||
new Leaflet.LatLng(
|
||||
this.hass.config.latitude,
|
||||
this.hass.config.longitude
|
||||
),
|
||||
zoom || 14
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = new Leaflet.latLngBounds(
|
||||
this._mapItems.map((item) => item.getLatLng())
|
||||
);
|
||||
this._map.fitBounds(bounds.pad(0.5));
|
||||
|
||||
if (zoom && this._map.getZoom() > zoom) {
|
||||
this._map.setZoom(zoom);
|
||||
}
|
||||
}
|
||||
|
||||
_drawEntities(hass) {
|
||||
const map = this._map;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._mapItems) {
|
||||
this._mapItems.forEach((marker) => marker.remove());
|
||||
}
|
||||
const mapItems = (this._mapItems = []);
|
||||
|
||||
this._configEntities.forEach((entity) => {
|
||||
const entityId = entity.entity;
|
||||
if (!(entityId in hass.states)) {
|
||||
return;
|
||||
}
|
||||
const stateObj = hass.states[entityId];
|
||||
const title = computeStateName(stateObj);
|
||||
const {
|
||||
latitude,
|
||||
longitude,
|
||||
passive,
|
||||
icon,
|
||||
radius,
|
||||
entity_picture: entityPicture,
|
||||
gps_accuracy: gpsAccuracy,
|
||||
} = stateObj.attributes;
|
||||
|
||||
if (!(latitude && longitude)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let markerIcon;
|
||||
let iconHTML;
|
||||
let el;
|
||||
|
||||
if (computeStateDomain(stateObj) === "zone") {
|
||||
// DRAW ZONE
|
||||
if (passive) return;
|
||||
|
||||
// create icon
|
||||
if (icon) {
|
||||
el = document.createElement("ha-icon");
|
||||
el.setAttribute("icon", icon);
|
||||
iconHTML = el.outerHTML;
|
||||
} else {
|
||||
iconHTML = title;
|
||||
}
|
||||
|
||||
markerIcon = Leaflet.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className: "",
|
||||
});
|
||||
|
||||
// create market with the icon
|
||||
mapItems.push(
|
||||
Leaflet.marker([latitude, longitude], {
|
||||
icon: markerIcon,
|
||||
interactive: false,
|
||||
title: title,
|
||||
}).addTo(map)
|
||||
);
|
||||
|
||||
// create circle around it
|
||||
mapItems.push(
|
||||
Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: "#FF9800",
|
||||
radius: radius,
|
||||
}).addTo(map)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// DRAW ENTITY
|
||||
// create icon
|
||||
const entityName = title
|
||||
.split(" ")
|
||||
.map((part) => part[0])
|
||||
.join("")
|
||||
.substr(0, 3);
|
||||
|
||||
el = document.createElement("ha-entity-marker");
|
||||
el.setAttribute("entity-id", entityId);
|
||||
el.setAttribute("entity-name", entityName);
|
||||
el.setAttribute("entity-picture", entityPicture || "");
|
||||
|
||||
/* Leaflet clones this element before adding it to the map. This messes up
|
||||
our Polymer object and we can't pass data through. Thus we hack like this. */
|
||||
markerIcon = Leaflet.divIcon({
|
||||
html: el.outerHTML,
|
||||
iconSize: [48, 48],
|
||||
className: "",
|
||||
});
|
||||
|
||||
// create market with the icon
|
||||
mapItems.push(
|
||||
Leaflet.marker([latitude, longitude], {
|
||||
icon: markerIcon,
|
||||
title: computeStateName(stateObj),
|
||||
}).addTo(map)
|
||||
);
|
||||
|
||||
// create circle around if entity has accuracy
|
||||
if (gpsAccuracy) {
|
||||
mapItems.push(
|
||||
Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: "#0288D1",
|
||||
radius: gpsAccuracy,
|
||||
}).addTo(map)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-map-card", HuiMapCard);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import Leaflet from "leaflet";
|
||||
|
||||
import "../../map/ha-entity-marker";
|
||||
|
||||
import setupLeafletMap from "../../../common/dom/setup-leaflet-map";
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
import computeStateDomain from "../../../common/entity/compute_state_domain";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import debounce from "../../../common/util/debounce";
|
||||
|
||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
|
||||
|
||||
class HuiMapCard extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host([is-panel]) ha-card {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
/**
|
||||
* In panel mode we want a full height map. Since parent #view
|
||||
* only sets min-height, we need absolute positioning here
|
||||
*/
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#map {
|
||||
z-index: 0;
|
||||
border: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
paper-icon-button {
|
||||
position: absolute;
|
||||
top: 75px;
|
||||
left: 7px;
|
||||
}
|
||||
|
||||
#root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([is-panel]) #root {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card id="card" header="[[_config.title]]">
|
||||
<div id="root">
|
||||
<div id="map"></div>
|
||||
<paper-icon-button
|
||||
on-click="_fitMap"
|
||||
icon="hass:image-filter-center-focus"
|
||||
title="Reset focus"
|
||||
></paper-icon-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_drawEntities",
|
||||
},
|
||||
_config: Object,
|
||||
isPanel: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._debouncedResizeListener = debounce(this._resetMap.bind(this), 100);
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
|
||||
if (!this._config || this.isPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.root.style.paddingTop = this._config.aspect_ratio || "100%";
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
this._configEntities = processConfigEntities(config.entities);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
let ar = this._config.aspect_ratio || "100%";
|
||||
ar = ar.substr(0, ar.length - 1);
|
||||
return 1 + Math.floor(ar / 25) || 3;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// Observe changes to map size and invalidate to prevent broken rendering
|
||||
// Uses ResizeObserver in Chrome, otherwise window resize event
|
||||
if (typeof ResizeObserver === "function") {
|
||||
this._resizeObserver = new ResizeObserver(() =>
|
||||
this._debouncedResizeListener()
|
||||
);
|
||||
this._resizeObserver.observe(this.$.map);
|
||||
} else {
|
||||
window.addEventListener("resize", this._debouncedResizeListener);
|
||||
}
|
||||
|
||||
this._map = setupLeafletMap(this.$.map);
|
||||
this._drawEntities(this.hass);
|
||||
|
||||
setTimeout(() => {
|
||||
this._resetMap();
|
||||
this._fitMap();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
if (this._map) {
|
||||
this._map.remove();
|
||||
}
|
||||
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.unobserve(this.$.map);
|
||||
} else {
|
||||
window.removeEventListener("resize", this._debouncedResizeListener);
|
||||
}
|
||||
}
|
||||
|
||||
_resetMap() {
|
||||
if (!this._map) {
|
||||
return;
|
||||
}
|
||||
this._map.invalidateSize();
|
||||
}
|
||||
|
||||
_fitMap() {
|
||||
const zoom = this._config.default_zoom;
|
||||
if (this._mapItems.length === 0) {
|
||||
this._map.setView(
|
||||
new Leaflet.LatLng(
|
||||
this.hass.config.latitude,
|
||||
this.hass.config.longitude
|
||||
),
|
||||
zoom || 14
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = new Leaflet.latLngBounds(
|
||||
this._mapItems.map((item) => item.getLatLng())
|
||||
);
|
||||
this._map.fitBounds(bounds.pad(0.5));
|
||||
|
||||
if (zoom && this._map.getZoom() > zoom) {
|
||||
this._map.setZoom(zoom);
|
||||
}
|
||||
}
|
||||
|
||||
_drawEntities(hass) {
|
||||
const map = this._map;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._mapItems) {
|
||||
this._mapItems.forEach((marker) => marker.remove());
|
||||
}
|
||||
const mapItems = (this._mapItems = []);
|
||||
|
||||
this._configEntities.forEach((entity) => {
|
||||
const entityId = entity.entity;
|
||||
if (!(entityId in hass.states)) {
|
||||
return;
|
||||
}
|
||||
const stateObj = hass.states[entityId];
|
||||
const title = computeStateName(stateObj);
|
||||
const {
|
||||
latitude,
|
||||
longitude,
|
||||
passive,
|
||||
icon,
|
||||
radius,
|
||||
entity_picture: entityPicture,
|
||||
gps_accuracy: gpsAccuracy,
|
||||
} = stateObj.attributes;
|
||||
|
||||
if (!(latitude && longitude)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let markerIcon;
|
||||
let iconHTML;
|
||||
let el;
|
||||
|
||||
if (computeStateDomain(stateObj) === "zone") {
|
||||
// DRAW ZONE
|
||||
if (passive) return;
|
||||
|
||||
// create icon
|
||||
if (icon) {
|
||||
el = document.createElement("ha-icon");
|
||||
el.setAttribute("icon", icon);
|
||||
iconHTML = el.outerHTML;
|
||||
} else {
|
||||
iconHTML = title;
|
||||
}
|
||||
|
||||
markerIcon = Leaflet.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className: "",
|
||||
});
|
||||
|
||||
// create market with the icon
|
||||
mapItems.push(
|
||||
Leaflet.marker([latitude, longitude], {
|
||||
icon: markerIcon,
|
||||
interactive: false,
|
||||
title: title,
|
||||
}).addTo(map)
|
||||
);
|
||||
|
||||
// create circle around it
|
||||
mapItems.push(
|
||||
Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: "#FF9800",
|
||||
radius: radius,
|
||||
}).addTo(map)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// DRAW ENTITY
|
||||
// create icon
|
||||
const entityName = title
|
||||
.split(" ")
|
||||
.map((part) => part[0])
|
||||
.join("")
|
||||
.substr(0, 3);
|
||||
|
||||
el = document.createElement("ha-entity-marker");
|
||||
el.setAttribute("entity-id", entityId);
|
||||
el.setAttribute("entity-name", entityName);
|
||||
el.setAttribute("entity-picture", entityPicture || "");
|
||||
|
||||
/* Leaflet clones this element before adding it to the map. This messes up
|
||||
our Polymer object and we can't pass data through. Thus we hack like this. */
|
||||
markerIcon = Leaflet.divIcon({
|
||||
html: el.outerHTML,
|
||||
iconSize: [48, 48],
|
||||
className: "",
|
||||
});
|
||||
|
||||
// create market with the icon
|
||||
mapItems.push(
|
||||
Leaflet.marker([latitude, longitude], {
|
||||
icon: markerIcon,
|
||||
title: computeStateName(stateObj),
|
||||
}).addTo(map)
|
||||
);
|
||||
|
||||
// create circle around if entity has accuracy
|
||||
if (gpsAccuracy) {
|
||||
mapItems.push(
|
||||
Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: "#0288D1",
|
||||
radius: gpsAccuracy,
|
||||
}).addTo(map)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-map-card", HuiMapCard);
|
||||
|
|
|
@ -1,93 +1,93 @@
|
|||
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
import { classMap } from "lit-html/directives/classMap";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-markdown";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
content: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
private _config?: Config;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return this._config!.content.split("\n").length;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config.content) {
|
||||
throw new Error("Invalid Configuration: Content Required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card .header="${this._config.title}">
|
||||
<ha-markdown
|
||||
class="markdown ${classMap({
|
||||
"no-header": !this._config.title,
|
||||
})}"
|
||||
.content="${this._config.content}"
|
||||
></ha-markdown>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
@apply --paper-font-body1;
|
||||
}
|
||||
ha-markdown {
|
||||
display: block;
|
||||
padding: 0 16px 16px;
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
.markdown.no-header {
|
||||
padding-top: 16px;
|
||||
}
|
||||
ha-markdown > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
ha-markdown > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-markdown a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-markdown img {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-markdown-card": HuiMarkdownCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-markdown-card", HuiMarkdownCard);
|
||||
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
import { classMap } from "lit-html/directives/classMap";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-markdown";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
content: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
private _config?: Config;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return this._config!.content.split("\n").length;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config.content) {
|
||||
throw new Error("Invalid Configuration: Content Required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card .header="${this._config.title}">
|
||||
<ha-markdown
|
||||
class="markdown ${classMap({
|
||||
"no-header": !this._config.title,
|
||||
})}"
|
||||
.content="${this._config.content}"
|
||||
></ha-markdown>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
@apply --paper-font-body1;
|
||||
}
|
||||
ha-markdown {
|
||||
display: block;
|
||||
padding: 0 16px 16px;
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
.markdown.no-header {
|
||||
padding-top: 16px;
|
||||
}
|
||||
ha-markdown > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
ha-markdown > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-markdown a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-markdown img {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-markdown-card": HuiMarkdownCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-markdown-card", HuiMarkdownCard);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import "../../../cards/ha-media_player-card";
|
||||
|
||||
import LegacyWrapperCard from "./hui-legacy-wrapper-card";
|
||||
|
||||
class HuiMediaControlCard extends LegacyWrapperCard {
|
||||
constructor() {
|
||||
super("ha-media_player-card", "media_player");
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-media-control-card", HuiMediaControlCard);
|
||||
import "../../../cards/ha-media_player-card";
|
||||
|
||||
import LegacyWrapperCard from "./hui-legacy-wrapper-card";
|
||||
|
||||
class HuiMediaControlCard extends LegacyWrapperCard {
|
||||
constructor() {
|
||||
super("ha-media_player-card", "media_player");
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-media-control-card", HuiMediaControlCard);
|
||||
|
|
|
@ -1,67 +1,67 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
|
||||
import NavigateMixin from "../../../mixins/navigate-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin NavigateMixin
|
||||
*/
|
||||
class HuiPictureCard extends NavigateMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
ha-card[clickable] {
|
||||
cursor: pointer;
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card on-click="_cardClicked" clickable$='[[_computeClickable(_config)]]'>
|
||||
<img src='[[_config.image]]' />
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
};
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.image) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_computeClickable(config) {
|
||||
return config.navigation_path || config.service;
|
||||
}
|
||||
|
||||
_cardClicked() {
|
||||
if (this._config.navigation_path) {
|
||||
this.navigate(this._config.navigation_path);
|
||||
}
|
||||
if (this._config.service) {
|
||||
const [domain, service] = this._config.service.split(".", 2);
|
||||
this.hass.callService(domain, service, this._config.service_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-picture-card", HuiPictureCard);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
|
||||
import NavigateMixin from "../../../mixins/navigate-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin NavigateMixin
|
||||
*/
|
||||
class HuiPictureCard extends NavigateMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
ha-card[clickable] {
|
||||
cursor: pointer;
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card on-click="_cardClicked" clickable$='[[_computeClickable(_config)]]'>
|
||||
<img src='[[_config.image]]' />
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
};
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.image) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_computeClickable(config) {
|
||||
return config.navigation_path || config.service;
|
||||
}
|
||||
|
||||
_cardClicked() {
|
||||
if (this._config.navigation_path) {
|
||||
this.navigate(this._config.navigation_path);
|
||||
}
|
||||
if (this._config.service) {
|
||||
const [domain, service] = this._config.service.split(".", 2);
|
||||
this.hass.callService(domain, service, this._config.service_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-picture-card", HuiPictureCard);
|
||||
|
|
|
@ -1,111 +1,111 @@
|
|||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import createHuiElement from "../common/create-hui-element";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
title?: string;
|
||||
image: string;
|
||||
elements: LovelaceElementConfig[];
|
||||
}
|
||||
|
||||
class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
||||
private _config?: Config;
|
||||
private _hass?: HomeAssistant;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
set hass(hass: HomeAssistant) {
|
||||
this._hass = hass;
|
||||
for (const el of this.shadowRoot!.querySelectorAll("#root > *")) {
|
||||
const element = el as LovelaceElement;
|
||||
element.hass = this._hass;
|
||||
}
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 4;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid Configuration");
|
||||
} else if (!config.image) {
|
||||
throw new Error("Invalid Configuration: image required");
|
||||
} else if (!Array.isArray(config.elements)) {
|
||||
throw new Error("Invalid Configuration: elements required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card .header="${this._config.title}">
|
||||
<div id="root">
|
||||
<img src="${this._config.image}">
|
||||
${this._config.elements.map((elementConfig: LovelaceElementConfig) =>
|
||||
this._createHuiElement(elementConfig)
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#root img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.element {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _createHuiElement(
|
||||
elementConfig: LovelaceElementConfig
|
||||
): LovelaceElement {
|
||||
const element = createHuiElement(elementConfig) as LovelaceElement;
|
||||
element.hass = this._hass;
|
||||
element.classList.add("element");
|
||||
|
||||
Object.keys(elementConfig.style).forEach((prop) => {
|
||||
element.style.setProperty(prop, elementConfig.style[prop]);
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-picture-elements-card": HuiPictureElementsCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-picture-elements-card", HuiPictureElementsCard);
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import createHuiElement from "../common/create-hui-element";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
title?: string;
|
||||
image: string;
|
||||
elements: LovelaceElementConfig[];
|
||||
}
|
||||
|
||||
class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
||||
private _config?: Config;
|
||||
private _hass?: HomeAssistant;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
set hass(hass: HomeAssistant) {
|
||||
this._hass = hass;
|
||||
for (const el of this.shadowRoot!.querySelectorAll("#root > *")) {
|
||||
const element = el as LovelaceElement;
|
||||
element.hass = this._hass;
|
||||
}
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 4;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid Configuration");
|
||||
} else if (!config.image) {
|
||||
throw new Error("Invalid Configuration: image required");
|
||||
} else if (!Array.isArray(config.elements)) {
|
||||
throw new Error("Invalid Configuration: elements required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card .header="${this._config.title}">
|
||||
<div id="root">
|
||||
<img src="${this._config.image}">
|
||||
${this._config.elements.map((elementConfig: LovelaceElementConfig) =>
|
||||
this._createHuiElement(elementConfig)
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#root img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.element {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _createHuiElement(
|
||||
elementConfig: LovelaceElementConfig
|
||||
): LovelaceElement {
|
||||
const element = createHuiElement(elementConfig) as LovelaceElement;
|
||||
element.hass = this._hass;
|
||||
element.classList.add("element");
|
||||
|
||||
Object.keys(elementConfig.style).forEach((prop) => {
|
||||
element.style.setProperty(prop, elementConfig.style[prop]);
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-picture-elements-card": HuiPictureElementsCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-picture-elements-card", HuiPictureElementsCard);
|
||||
|
|
|
@ -1,201 +1,201 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../components/hui-image";
|
||||
|
||||
import computeDomain from "../../../common/entity/compute_domain";
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import toggleEntity from "../common/entity/toggle-entity";
|
||||
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import { longPressBind } from "../common/directives/long-press-directive";
|
||||
|
||||
const UNAVAILABLE = "Unavailable";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HuiPictureEntityCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
min-height: 75px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
ha-card.canInteract {
|
||||
cursor: pointer;
|
||||
}
|
||||
.footer {
|
||||
@apply --paper-font-common-nowrap;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
color: white;
|
||||
}
|
||||
.both {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.state {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card id='card'>
|
||||
<hui-image
|
||||
hass="[[hass]]"
|
||||
image="[[_config.image]]"
|
||||
state-image="[[_config.state_image]]"
|
||||
camera-image="[[_getCameraImage(_config)]]"
|
||||
entity="[[_config.entity]]"
|
||||
aspect-ratio="[[_config.aspect_ratio]]"
|
||||
></hui-image>
|
||||
<template is="dom-if" if="[[_showNameAndState(_config)]]">
|
||||
<div class="footer both">
|
||||
<div>[[_name]]</div>
|
||||
<div>[[_state]]</div>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showName(_config)]]">
|
||||
<div class="footer">
|
||||
[[_name]]
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showState(_config)]]">
|
||||
<div class="footer state">
|
||||
[[_state]]
|
||||
</div>
|
||||
</template>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_hassChanged",
|
||||
},
|
||||
_config: Object,
|
||||
_name: String,
|
||||
_state: String,
|
||||
};
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
this._entityDomain = computeDomain(config.entity);
|
||||
if (
|
||||
this._entityDomain !== "camera" &&
|
||||
(!config.image && !config.state_image && !config.camera_image)
|
||||
) {
|
||||
throw new Error("No image source configured.");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
const card = this.shadowRoot.querySelector("#card");
|
||||
longPressBind(card);
|
||||
card.addEventListener("ha-click", () => this._cardClicked(false));
|
||||
card.addEventListener("ha-hold", () => this._cardClicked(true));
|
||||
}
|
||||
|
||||
_hassChanged(hass) {
|
||||
const config = this._config;
|
||||
const entityId = config.entity;
|
||||
const stateObj = hass.states[entityId];
|
||||
|
||||
// Nothing changed
|
||||
if (
|
||||
(!stateObj && this._oldState === UNAVAILABLE) ||
|
||||
(stateObj && stateObj.state === this._oldState)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name;
|
||||
let state;
|
||||
let stateLabel;
|
||||
let available;
|
||||
|
||||
if (stateObj) {
|
||||
name = config.name || computeStateName(stateObj);
|
||||
state = stateObj.state;
|
||||
stateLabel = computeStateDisplay(this.localize, stateObj);
|
||||
available = true;
|
||||
} else {
|
||||
name = config.name || entityId;
|
||||
state = UNAVAILABLE;
|
||||
stateLabel = this.localize("state.default.unavailable");
|
||||
available = false;
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
_name: name,
|
||||
_state: stateLabel,
|
||||
_oldState: state,
|
||||
});
|
||||
|
||||
this.$.card.classList.toggle("canInteract", available);
|
||||
}
|
||||
|
||||
_showNameAndState(config) {
|
||||
return config.show_name !== false && config.show_state !== false;
|
||||
}
|
||||
|
||||
_showName(config) {
|
||||
return config.show_name !== false && config.show_state === false;
|
||||
}
|
||||
|
||||
_showState(config) {
|
||||
return config.show_name === false && config.show_state !== false;
|
||||
}
|
||||
|
||||
_cardClicked(hold) {
|
||||
const config = this._config;
|
||||
const entityId = config.entity;
|
||||
|
||||
if (!(entityId in this.hass.states)) return;
|
||||
|
||||
const action = hold ? config.hold_action : config.tap_action || "more-info";
|
||||
|
||||
switch (action) {
|
||||
case "toggle":
|
||||
toggleEntity(this.hass, entityId);
|
||||
break;
|
||||
case "more-info":
|
||||
this.fire("hass-more-info", { entityId });
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
_getCameraImage(config) {
|
||||
return this._entityDomain === "camera"
|
||||
? config.entity
|
||||
: config.camera_image;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-picture-entity-card", HuiPictureEntityCard);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../components/hui-image";
|
||||
|
||||
import computeDomain from "../../../common/entity/compute_domain";
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import toggleEntity from "../common/entity/toggle-entity";
|
||||
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import { longPressBind } from "../common/directives/long-press-directive";
|
||||
|
||||
const UNAVAILABLE = "Unavailable";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HuiPictureEntityCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
min-height: 75px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
ha-card.canInteract {
|
||||
cursor: pointer;
|
||||
}
|
||||
.footer {
|
||||
@apply --paper-font-common-nowrap;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
color: white;
|
||||
}
|
||||
.both {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.state {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card id='card'>
|
||||
<hui-image
|
||||
hass="[[hass]]"
|
||||
image="[[_config.image]]"
|
||||
state-image="[[_config.state_image]]"
|
||||
camera-image="[[_getCameraImage(_config)]]"
|
||||
entity="[[_config.entity]]"
|
||||
aspect-ratio="[[_config.aspect_ratio]]"
|
||||
></hui-image>
|
||||
<template is="dom-if" if="[[_showNameAndState(_config)]]">
|
||||
<div class="footer both">
|
||||
<div>[[_name]]</div>
|
||||
<div>[[_state]]</div>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showName(_config)]]">
|
||||
<div class="footer">
|
||||
[[_name]]
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showState(_config)]]">
|
||||
<div class="footer state">
|
||||
[[_state]]
|
||||
</div>
|
||||
</template>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_hassChanged",
|
||||
},
|
||||
_config: Object,
|
||||
_name: String,
|
||||
_state: String,
|
||||
};
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
this._entityDomain = computeDomain(config.entity);
|
||||
if (
|
||||
this._entityDomain !== "camera" &&
|
||||
(!config.image && !config.state_image && !config.camera_image)
|
||||
) {
|
||||
throw new Error("No image source configured.");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
const card = this.shadowRoot.querySelector("#card");
|
||||
longPressBind(card);
|
||||
card.addEventListener("ha-click", () => this._cardClicked(false));
|
||||
card.addEventListener("ha-hold", () => this._cardClicked(true));
|
||||
}
|
||||
|
||||
_hassChanged(hass) {
|
||||
const config = this._config;
|
||||
const entityId = config.entity;
|
||||
const stateObj = hass.states[entityId];
|
||||
|
||||
// Nothing changed
|
||||
if (
|
||||
(!stateObj && this._oldState === UNAVAILABLE) ||
|
||||
(stateObj && stateObj.state === this._oldState)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name;
|
||||
let state;
|
||||
let stateLabel;
|
||||
let available;
|
||||
|
||||
if (stateObj) {
|
||||
name = config.name || computeStateName(stateObj);
|
||||
state = stateObj.state;
|
||||
stateLabel = computeStateDisplay(this.localize, stateObj);
|
||||
available = true;
|
||||
} else {
|
||||
name = config.name || entityId;
|
||||
state = UNAVAILABLE;
|
||||
stateLabel = this.localize("state.default.unavailable");
|
||||
available = false;
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
_name: name,
|
||||
_state: stateLabel,
|
||||
_oldState: state,
|
||||
});
|
||||
|
||||
this.$.card.classList.toggle("canInteract", available);
|
||||
}
|
||||
|
||||
_showNameAndState(config) {
|
||||
return config.show_name !== false && config.show_state !== false;
|
||||
}
|
||||
|
||||
_showName(config) {
|
||||
return config.show_name !== false && config.show_state === false;
|
||||
}
|
||||
|
||||
_showState(config) {
|
||||
return config.show_name === false && config.show_state !== false;
|
||||
}
|
||||
|
||||
_cardClicked(hold) {
|
||||
const config = this._config;
|
||||
const entityId = config.entity;
|
||||
|
||||
if (!(entityId in this.hass.states)) return;
|
||||
|
||||
const action = hold ? config.hold_action : config.tap_action || "more-info";
|
||||
|
||||
switch (action) {
|
||||
case "toggle":
|
||||
toggleEntity(this.hass, entityId);
|
||||
break;
|
||||
case "more-info":
|
||||
this.fire("hass-more-info", { entityId });
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
_getCameraImage(config) {
|
||||
return this._entityDomain === "camera"
|
||||
? config.entity
|
||||
: config.camera_image;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-picture-entity-card", HuiPictureEntityCard);
|
||||
|
|
|
@ -1,195 +1,195 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import "../components/hui-image";
|
||||
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||
import stateIcon from "../../../common/entity/state_icon";
|
||||
import toggleEntity from "../common/entity/toggle-entity";
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import NavigateMixin from "../../../mixins/navigate-mixin";
|
||||
import computeDomain from "../../../common/entity/compute_domain";
|
||||
|
||||
const STATES_OFF = new Set(["closed", "locked", "not_home", "off"]);
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin NavigateMixin
|
||||
*/
|
||||
class HuiPictureGlanceCard extends NavigateMixin(
|
||||
LocalizeMixin(EventsMixin(PolymerElement))
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
position: relative;
|
||||
min-height: 48px;
|
||||
overflow: hidden;
|
||||
}
|
||||
hui-image.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.box {
|
||||
@apply --paper-font-common-nowrap;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 4px 8px;
|
||||
font-size: 16px;
|
||||
line-height: 40px;
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.box .title {
|
||||
font-weight: 500;
|
||||
margin-left: 8px;
|
||||
}
|
||||
ha-icon {
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
color: #A9A9A9;
|
||||
}
|
||||
ha-icon.state-on {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card>
|
||||
<hui-image
|
||||
class$='[[_computeImageClass(_config)]]'
|
||||
on-click='_handleImageClick'
|
||||
hass="[[hass]]"
|
||||
image="[[_config.image]]"
|
||||
state-image="[[_config.state_image]]"
|
||||
camera-image="[[_config.camera_image]]"
|
||||
entity="[[_config.entity]]"
|
||||
aspect-ratio="[[_config.aspect_ratio]]"
|
||||
></hui-image>
|
||||
<div class="box">
|
||||
<template is="dom-if" if="[[_config.title]]">
|
||||
<div class="title">[[_config.title]]</div>
|
||||
</template>
|
||||
<div>
|
||||
<template is="dom-repeat" items="[[_computeVisible(_entitiesDialog, hass.states)]]">
|
||||
<ha-icon
|
||||
on-click="_openDialog"
|
||||
class$="[[_computeButtonClass(item.entity, hass.states)]]"
|
||||
icon="[[_computeIcon(item, hass.states)]]"
|
||||
title="[[_computeTooltip(item.entity, hass.states)]]"
|
||||
></ha-icon>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<template is="dom-repeat" items="[[_computeVisible(_entitiesToggle, hass.states)]]">
|
||||
<ha-icon
|
||||
on-click="_callService"
|
||||
class$="[[_computeButtonClass(item.entity, hass.states)]]"
|
||||
icon="[[_computeIcon(item, hass.states)]]"
|
||||
title="[[_computeTooltip(item.entity, hass.states)]]"
|
||||
></ha-icon>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_entitiesDialog: Array,
|
||||
_entitiesToggle: Array,
|
||||
};
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (
|
||||
!config ||
|
||||
!config.entities ||
|
||||
!Array.isArray(config.entities) ||
|
||||
!(config.image || config.camera_image || config.state_image) ||
|
||||
(config.state_image && !config.entity)
|
||||
) {
|
||||
throw new Error("Invalid card configuration");
|
||||
}
|
||||
const entities = processConfigEntities(config.entities);
|
||||
const dialog = [];
|
||||
const toggle = [];
|
||||
|
||||
entities.forEach((item) => {
|
||||
if (
|
||||
config.force_dialog ||
|
||||
!DOMAINS_TOGGLE.has(computeDomain(item.entity))
|
||||
) {
|
||||
dialog.push(item);
|
||||
} else {
|
||||
toggle.push(item);
|
||||
}
|
||||
});
|
||||
this.setProperties({
|
||||
_config: config,
|
||||
_entitiesDialog: dialog,
|
||||
_entitiesToggle: toggle,
|
||||
});
|
||||
}
|
||||
|
||||
_computeVisible(collection, states) {
|
||||
return collection.filter((el) => el.entity in states);
|
||||
}
|
||||
|
||||
_computeIcon(item, states) {
|
||||
return item.icon || stateIcon(states[item.entity]);
|
||||
}
|
||||
|
||||
_computeButtonClass(entityId, states) {
|
||||
return STATES_OFF.has(states[entityId].state) ? "" : "state-on";
|
||||
}
|
||||
|
||||
_computeTooltip(entityId, states) {
|
||||
return `${computeStateName(states[entityId])}: ${computeStateDisplay(
|
||||
this.localize,
|
||||
states[entityId]
|
||||
)}`;
|
||||
}
|
||||
|
||||
_computeImageClass(config) {
|
||||
return config.navigation_path || config.camera_image ? "clickable" : "";
|
||||
}
|
||||
|
||||
_openDialog(ev) {
|
||||
this.fire("hass-more-info", { entityId: ev.model.item.entity });
|
||||
}
|
||||
|
||||
_callService(ev) {
|
||||
toggleEntity(this.hass, ev.model.item.entity);
|
||||
}
|
||||
|
||||
_handleImageClick() {
|
||||
if (this._config.navigation_path) {
|
||||
this.navigate(this._config.navigation_path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._config.camera_image) {
|
||||
this.fire("hass-more-info", { entityId: this._config.camera_image });
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("hui-picture-glance-card", HuiPictureGlanceCard);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import "../components/hui-image";
|
||||
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||
import stateIcon from "../../../common/entity/state_icon";
|
||||
import toggleEntity from "../common/entity/toggle-entity";
|
||||
import processConfigEntities from "../common/process-config-entities";
|
||||
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import NavigateMixin from "../../../mixins/navigate-mixin";
|
||||
import computeDomain from "../../../common/entity/compute_domain";
|
||||
|
||||
const STATES_OFF = new Set(["closed", "locked", "not_home", "off"]);
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin NavigateMixin
|
||||
*/
|
||||
class HuiPictureGlanceCard extends NavigateMixin(
|
||||
LocalizeMixin(EventsMixin(PolymerElement))
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-card {
|
||||
position: relative;
|
||||
min-height: 48px;
|
||||
overflow: hidden;
|
||||
}
|
||||
hui-image.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.box {
|
||||
@apply --paper-font-common-nowrap;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 4px 8px;
|
||||
font-size: 16px;
|
||||
line-height: 40px;
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.box .title {
|
||||
font-weight: 500;
|
||||
margin-left: 8px;
|
||||
}
|
||||
ha-icon {
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
color: #A9A9A9;
|
||||
}
|
||||
ha-icon.state-on {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card>
|
||||
<hui-image
|
||||
class$='[[_computeImageClass(_config)]]'
|
||||
on-click='_handleImageClick'
|
||||
hass="[[hass]]"
|
||||
image="[[_config.image]]"
|
||||
state-image="[[_config.state_image]]"
|
||||
camera-image="[[_config.camera_image]]"
|
||||
entity="[[_config.entity]]"
|
||||
aspect-ratio="[[_config.aspect_ratio]]"
|
||||
></hui-image>
|
||||
<div class="box">
|
||||
<template is="dom-if" if="[[_config.title]]">
|
||||
<div class="title">[[_config.title]]</div>
|
||||
</template>
|
||||
<div>
|
||||
<template is="dom-repeat" items="[[_computeVisible(_entitiesDialog, hass.states)]]">
|
||||
<ha-icon
|
||||
on-click="_openDialog"
|
||||
class$="[[_computeButtonClass(item.entity, hass.states)]]"
|
||||
icon="[[_computeIcon(item, hass.states)]]"
|
||||
title="[[_computeTooltip(item.entity, hass.states)]]"
|
||||
></ha-icon>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<template is="dom-repeat" items="[[_computeVisible(_entitiesToggle, hass.states)]]">
|
||||
<ha-icon
|
||||
on-click="_callService"
|
||||
class$="[[_computeButtonClass(item.entity, hass.states)]]"
|
||||
icon="[[_computeIcon(item, hass.states)]]"
|
||||
title="[[_computeTooltip(item.entity, hass.states)]]"
|
||||
></ha-icon>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_entitiesDialog: Array,
|
||||
_entitiesToggle: Array,
|
||||
};
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (
|
||||
!config ||
|
||||
!config.entities ||
|
||||
!Array.isArray(config.entities) ||
|
||||
!(config.image || config.camera_image || config.state_image) ||
|
||||
(config.state_image && !config.entity)
|
||||
) {
|
||||
throw new Error("Invalid card configuration");
|
||||
}
|
||||
const entities = processConfigEntities(config.entities);
|
||||
const dialog = [];
|
||||
const toggle = [];
|
||||
|
||||
entities.forEach((item) => {
|
||||
if (
|
||||
config.force_dialog ||
|
||||
!DOMAINS_TOGGLE.has(computeDomain(item.entity))
|
||||
) {
|
||||
dialog.push(item);
|
||||
} else {
|
||||
toggle.push(item);
|
||||
}
|
||||
});
|
||||
this.setProperties({
|
||||
_config: config,
|
||||
_entitiesDialog: dialog,
|
||||
_entitiesToggle: toggle,
|
||||
});
|
||||
}
|
||||
|
||||
_computeVisible(collection, states) {
|
||||
return collection.filter((el) => el.entity in states);
|
||||
}
|
||||
|
||||
_computeIcon(item, states) {
|
||||
return item.icon || stateIcon(states[item.entity]);
|
||||
}
|
||||
|
||||
_computeButtonClass(entityId, states) {
|
||||
return STATES_OFF.has(states[entityId].state) ? "" : "state-on";
|
||||
}
|
||||
|
||||
_computeTooltip(entityId, states) {
|
||||
return `${computeStateName(states[entityId])}: ${computeStateDisplay(
|
||||
this.localize,
|
||||
states[entityId]
|
||||
)}`;
|
||||
}
|
||||
|
||||
_computeImageClass(config) {
|
||||
return config.navigation_path || config.camera_image ? "clickable" : "";
|
||||
}
|
||||
|
||||
_openDialog(ev) {
|
||||
this.fire("hass-more-info", { entityId: ev.model.item.entity });
|
||||
}
|
||||
|
||||
_callService(ev) {
|
||||
toggleEntity(this.hass, ev.model.item.entity);
|
||||
}
|
||||
|
||||
_handleImageClick() {
|
||||
if (this._config.navigation_path) {
|
||||
this.navigate(this._config.navigation_path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._config.camera_image) {
|
||||
this.fire("hass-more-info", { entityId: this._config.camera_image });
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("hui-picture-glance-card", HuiPictureGlanceCard);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import "../../../cards/ha-plant-card";
|
||||
|
||||
import LegacyWrapperCard from "./hui-legacy-wrapper-card";
|
||||
|
||||
class HuiPlantStatusCard extends LegacyWrapperCard {
|
||||
constructor() {
|
||||
super("ha-plant-card", "plant");
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-plant-status-card", HuiPlantStatusCard);
|
||||
import "../../../cards/ha-plant-card";
|
||||
|
||||
import LegacyWrapperCard from "./hui-legacy-wrapper-card";
|
||||
|
||||
class HuiPlantStatusCard extends LegacyWrapperCard {
|
||||
constructor() {
|
||||
super("ha-plant-card", "plant");
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-plant-status-card", HuiPlantStatusCard);
|
||||
|
|
|
@ -1,292 +1,292 @@
|
|||
import { LitElement, html, svg } from "@polymer/lit-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import stateIcon from "../../../common/entity/state_icon";
|
||||
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
|
||||
class HuiSensorCard extends EventsMixin(LitElement) {
|
||||
set hass(hass) {
|
||||
this._hass = hass;
|
||||
const entity = hass.states[this._config.entity];
|
||||
if (entity && this._entity !== entity) {
|
||||
this._entity = entity;
|
||||
if (
|
||||
this._config.graph !== "none" &&
|
||||
entity.attributes.unit_of_measurement
|
||||
) {
|
||||
this._getHistory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_hass: {},
|
||||
_config: {},
|
||||
_entity: {},
|
||||
_line: String,
|
||||
};
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config.entity || config.entity.split(".")[0] !== "sensor") {
|
||||
throw new Error("Specify an entity from within the sensor domain.");
|
||||
}
|
||||
|
||||
const cardConfig = {
|
||||
icon: false,
|
||||
hours_to_show: 24,
|
||||
accuracy: 10,
|
||||
height: 100,
|
||||
line_width: 5,
|
||||
line_color: "var(--accent-color)",
|
||||
...config,
|
||||
};
|
||||
cardConfig.hours_to_show = Number(cardConfig.hours_to_show);
|
||||
cardConfig.accuracy = Number(cardConfig.accuracy);
|
||||
cardConfig.height = Number(cardConfig.height);
|
||||
cardConfig.line_width = Number(cardConfig.line_width);
|
||||
|
||||
this._config = cardConfig;
|
||||
}
|
||||
|
||||
shouldUpdate(changedProps) {
|
||||
const change = changedProps.has("_entity") || changedProps.has("_line");
|
||||
return change;
|
||||
}
|
||||
|
||||
render({ _config, _entity, _line } = this) {
|
||||
return html`
|
||||
${this._style()}
|
||||
<ha-card @click=${this._handleClick}>
|
||||
<div class='flex'>
|
||||
<div class='icon'>
|
||||
<ha-icon .icon=${this._computeIcon(_entity)}></ha-icon>
|
||||
</div>
|
||||
<div class='header'>
|
||||
<span class='name'>${this._computeName(_entity)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex info'>
|
||||
<span id='value'>${_entity.state}</span>
|
||||
<span id='measurement'>${this._computeUom(_entity)}</span>
|
||||
</div>
|
||||
<div class='graph'>
|
||||
<div>
|
||||
${
|
||||
_line
|
||||
? svg`
|
||||
<svg width='100%' height='100%' viewBox='0 0 500 ${_config.height}'>
|
||||
<path d=${_line} fill='none' stroke=${_config.line_color}
|
||||
stroke-width=${_config.line_width}
|
||||
stroke-linecap='round' stroke-linejoin='round' />
|
||||
</svg>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>`;
|
||||
}
|
||||
|
||||
_handleClick() {
|
||||
this.fire("hass-more-info", { entityId: this._config.entity });
|
||||
}
|
||||
|
||||
_computeIcon(item) {
|
||||
return this._config.icon || stateIcon(item);
|
||||
}
|
||||
|
||||
_computeName(item) {
|
||||
return this._config.name || computeStateName(item);
|
||||
}
|
||||
|
||||
_computeUom(item) {
|
||||
return this._config.unit || item.attributes.unit_of_measurement;
|
||||
}
|
||||
|
||||
_getGraph(items, width, height) {
|
||||
const values = this._getValueArr(items);
|
||||
const coords = this._calcCoordinates(values, width, height);
|
||||
return this._getPath(coords);
|
||||
}
|
||||
|
||||
_getValueArr(items) {
|
||||
return items.map((item) => Number(item.state) || 0);
|
||||
}
|
||||
|
||||
_calcCoordinates(values, width, height) {
|
||||
const margin = this._config.line_width;
|
||||
width -= margin * 2;
|
||||
height -= margin * 2;
|
||||
const min = Math.floor(Math.min.apply(null, values) * 0.95);
|
||||
const max = Math.ceil(Math.max.apply(null, values) * 1.05);
|
||||
|
||||
if (values.length === 1) values.push(values[0]);
|
||||
|
||||
const yRatio = (max - min) / height;
|
||||
const xRatio = width / (values.length - 1);
|
||||
|
||||
return values.map((value, i) => {
|
||||
const y = height - (value - min) / yRatio || 0;
|
||||
const x = xRatio * i + margin;
|
||||
return [x, y];
|
||||
});
|
||||
}
|
||||
|
||||
_getPath(points) {
|
||||
const SPACE = " ";
|
||||
let next;
|
||||
let Z;
|
||||
const X = 0;
|
||||
const Y = 1;
|
||||
let path = "";
|
||||
let point = points[0];
|
||||
|
||||
path += "M" + point[X] + "," + point[Y];
|
||||
const first = point;
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
next = points[i];
|
||||
Z = this._midPoint(point[X], point[Y], next[X], next[Y]);
|
||||
path += SPACE + Z[X] + "," + Z[Y];
|
||||
path += "Q" + Math.floor(next[X]) + "," + next[Y];
|
||||
point = next;
|
||||
}
|
||||
|
||||
const second = points[1];
|
||||
Z = this._midPoint(first[X], first[Y], second[X], second[Y]);
|
||||
path += SPACE + Math.floor(next[X]) + "." + points[points.length - 1];
|
||||
return path;
|
||||
}
|
||||
|
||||
_midPoint(Ax, Ay, Bx, By) {
|
||||
const Zx = (Ax - Bx) / 2 + Bx;
|
||||
const Zy = (Ay - By) / 2 + By;
|
||||
return [Zx, Zy];
|
||||
}
|
||||
|
||||
async _getHistory() {
|
||||
const endTime = new Date();
|
||||
const startTime = new Date();
|
||||
startTime.setHours(endTime.getHours() - this._config.hours_to_show);
|
||||
const stateHistory = await this._fetchRecent(
|
||||
this._config.entity,
|
||||
startTime,
|
||||
endTime
|
||||
);
|
||||
const history = stateHistory[0];
|
||||
const valArray = [history[history.length - 1]];
|
||||
|
||||
let pos = history.length - 1;
|
||||
const accuracy = this._config.accuracy <= pos ? this._config.accuracy : pos;
|
||||
let increment = Math.ceil(history.length / accuracy);
|
||||
increment = increment <= 0 ? 1 : increment;
|
||||
for (let i = accuracy; i >= 2; i--) {
|
||||
pos -= increment;
|
||||
valArray.unshift(pos >= 0 ? history[pos] : history[0]);
|
||||
}
|
||||
this._line = this._getGraph(valArray, 500, this._config.height);
|
||||
}
|
||||
|
||||
async _fetchRecent(entityId, startTime, endTime) {
|
||||
let url = "history/period";
|
||||
if (startTime) url += "/" + startTime.toISOString();
|
||||
url += "?filter_entity_id=" + entityId;
|
||||
if (endTime) url += "&end_time=" + endTime.toISOString();
|
||||
|
||||
return await this._hass.callApi("GET", url);
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
_style() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
opacity: .8;
|
||||
position: relative;
|
||||
}
|
||||
.name {
|
||||
display: block;
|
||||
display: -webkit-box;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
max-height: 1.4rem;
|
||||
margin-top: 2px;
|
||||
opacity: .8;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
.icon {
|
||||
color: var(--paper-item-icon-color, #44739e);
|
||||
display: inline-block;
|
||||
flex: 0 0 40px;
|
||||
line-height: 40px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
}
|
||||
.info {
|
||||
flex-wrap: wrap;
|
||||
margin: 16px 0 16px 8px;
|
||||
}
|
||||
#value {
|
||||
display: inline-block;
|
||||
font-size: 2rem;
|
||||
font-weight: 400;
|
||||
line-height: 1em;
|
||||
margin-right: 4px;
|
||||
}
|
||||
#measurement {
|
||||
align-self: flex-end;
|
||||
display: inline-block;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.2em;
|
||||
margin-top: .1em;
|
||||
opacity: .6;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.graph {
|
||||
align-self: flex-end;
|
||||
margin: auto;
|
||||
margin-bottom: 0px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.graph > div {
|
||||
align-self: flex-end;
|
||||
margin: auto 8px;
|
||||
}
|
||||
</style>`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-sensor-card", HuiSensorCard);
|
||||
import { LitElement, html, svg } from "@polymer/lit-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import stateIcon from "../../../common/entity/state_icon";
|
||||
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
|
||||
class HuiSensorCard extends EventsMixin(LitElement) {
|
||||
set hass(hass) {
|
||||
this._hass = hass;
|
||||
const entity = hass.states[this._config.entity];
|
||||
if (entity && this._entity !== entity) {
|
||||
this._entity = entity;
|
||||
if (
|
||||
this._config.graph !== "none" &&
|
||||
entity.attributes.unit_of_measurement
|
||||
) {
|
||||
this._getHistory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_hass: {},
|
||||
_config: {},
|
||||
_entity: {},
|
||||
_line: String,
|
||||
};
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config.entity || config.entity.split(".")[0] !== "sensor") {
|
||||
throw new Error("Specify an entity from within the sensor domain.");
|
||||
}
|
||||
|
||||
const cardConfig = {
|
||||
icon: false,
|
||||
hours_to_show: 24,
|
||||
accuracy: 10,
|
||||
height: 100,
|
||||
line_width: 5,
|
||||
line_color: "var(--accent-color)",
|
||||
...config,
|
||||
};
|
||||
cardConfig.hours_to_show = Number(cardConfig.hours_to_show);
|
||||
cardConfig.accuracy = Number(cardConfig.accuracy);
|
||||
cardConfig.height = Number(cardConfig.height);
|
||||
cardConfig.line_width = Number(cardConfig.line_width);
|
||||
|
||||
this._config = cardConfig;
|
||||
}
|
||||
|
||||
shouldUpdate(changedProps) {
|
||||
const change = changedProps.has("_entity") || changedProps.has("_line");
|
||||
return change;
|
||||
}
|
||||
|
||||
render({ _config, _entity, _line } = this) {
|
||||
return html`
|
||||
${this._style()}
|
||||
<ha-card @click=${this._handleClick}>
|
||||
<div class='flex'>
|
||||
<div class='icon'>
|
||||
<ha-icon .icon=${this._computeIcon(_entity)}></ha-icon>
|
||||
</div>
|
||||
<div class='header'>
|
||||
<span class='name'>${this._computeName(_entity)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex info'>
|
||||
<span id='value'>${_entity.state}</span>
|
||||
<span id='measurement'>${this._computeUom(_entity)}</span>
|
||||
</div>
|
||||
<div class='graph'>
|
||||
<div>
|
||||
${
|
||||
_line
|
||||
? svg`
|
||||
<svg width='100%' height='100%' viewBox='0 0 500 ${_config.height}'>
|
||||
<path d=${_line} fill='none' stroke=${_config.line_color}
|
||||
stroke-width=${_config.line_width}
|
||||
stroke-linecap='round' stroke-linejoin='round' />
|
||||
</svg>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>`;
|
||||
}
|
||||
|
||||
_handleClick() {
|
||||
this.fire("hass-more-info", { entityId: this._config.entity });
|
||||
}
|
||||
|
||||
_computeIcon(item) {
|
||||
return this._config.icon || stateIcon(item);
|
||||
}
|
||||
|
||||
_computeName(item) {
|
||||
return this._config.name || computeStateName(item);
|
||||
}
|
||||
|
||||
_computeUom(item) {
|
||||
return this._config.unit || item.attributes.unit_of_measurement;
|
||||
}
|
||||
|
||||
_getGraph(items, width, height) {
|
||||
const values = this._getValueArr(items);
|
||||
const coords = this._calcCoordinates(values, width, height);
|
||||
return this._getPath(coords);
|
||||
}
|
||||
|
||||
_getValueArr(items) {
|
||||
return items.map((item) => Number(item.state) || 0);
|
||||
}
|
||||
|
||||
_calcCoordinates(values, width, height) {
|
||||
const margin = this._config.line_width;
|
||||
width -= margin * 2;
|
||||
height -= margin * 2;
|
||||
const min = Math.floor(Math.min.apply(null, values) * 0.95);
|
||||
const max = Math.ceil(Math.max.apply(null, values) * 1.05);
|
||||
|
||||
if (values.length === 1) values.push(values[0]);
|
||||
|
||||
const yRatio = (max - min) / height;
|
||||
const xRatio = width / (values.length - 1);
|
||||
|
||||
return values.map((value, i) => {
|
||||
const y = height - (value - min) / yRatio || 0;
|
||||
const x = xRatio * i + margin;
|
||||
return [x, y];
|
||||
});
|
||||
}
|
||||
|
||||
_getPath(points) {
|
||||
const SPACE = " ";
|
||||
let next;
|
||||
let Z;
|
||||
const X = 0;
|
||||
const Y = 1;
|
||||
let path = "";
|
||||
let point = points[0];
|
||||
|
||||
path += "M" + point[X] + "," + point[Y];
|
||||
const first = point;
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
next = points[i];
|
||||
Z = this._midPoint(point[X], point[Y], next[X], next[Y]);
|
||||
path += SPACE + Z[X] + "," + Z[Y];
|
||||
path += "Q" + Math.floor(next[X]) + "," + next[Y];
|
||||
point = next;
|
||||
}
|
||||
|
||||
const second = points[1];
|
||||
Z = this._midPoint(first[X], first[Y], second[X], second[Y]);
|
||||
path += SPACE + Math.floor(next[X]) + "." + points[points.length - 1];
|
||||
return path;
|
||||
}
|
||||
|
||||
_midPoint(Ax, Ay, Bx, By) {
|
||||
const Zx = (Ax - Bx) / 2 + Bx;
|
||||
const Zy = (Ay - By) / 2 + By;
|
||||
return [Zx, Zy];
|
||||
}
|
||||
|
||||
async _getHistory() {
|
||||
const endTime = new Date();
|
||||
const startTime = new Date();
|
||||
startTime.setHours(endTime.getHours() - this._config.hours_to_show);
|
||||
const stateHistory = await this._fetchRecent(
|
||||
this._config.entity,
|
||||
startTime,
|
||||
endTime
|
||||
);
|
||||
const history = stateHistory[0];
|
||||
const valArray = [history[history.length - 1]];
|
||||
|
||||
let pos = history.length - 1;
|
||||
const accuracy = this._config.accuracy <= pos ? this._config.accuracy : pos;
|
||||
let increment = Math.ceil(history.length / accuracy);
|
||||
increment = increment <= 0 ? 1 : increment;
|
||||
for (let i = accuracy; i >= 2; i--) {
|
||||
pos -= increment;
|
||||
valArray.unshift(pos >= 0 ? history[pos] : history[0]);
|
||||
}
|
||||
this._line = this._getGraph(valArray, 500, this._config.height);
|
||||
}
|
||||
|
||||
async _fetchRecent(entityId, startTime, endTime) {
|
||||
let url = "history/period";
|
||||
if (startTime) url += "/" + startTime.toISOString();
|
||||
url += "?filter_entity_id=" + entityId;
|
||||
if (endTime) url += "&end_time=" + endTime.toISOString();
|
||||
|
||||
return await this._hass.callApi("GET", url);
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
_style() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
opacity: .8;
|
||||
position: relative;
|
||||
}
|
||||
.name {
|
||||
display: block;
|
||||
display: -webkit-box;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
max-height: 1.4rem;
|
||||
margin-top: 2px;
|
||||
opacity: .8;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
.icon {
|
||||
color: var(--paper-item-icon-color, #44739e);
|
||||
display: inline-block;
|
||||
flex: 0 0 40px;
|
||||
line-height: 40px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
}
|
||||
.info {
|
||||
flex-wrap: wrap;
|
||||
margin: 16px 0 16px 8px;
|
||||
}
|
||||
#value {
|
||||
display: inline-block;
|
||||
font-size: 2rem;
|
||||
font-weight: 400;
|
||||
line-height: 1em;
|
||||
margin-right: 4px;
|
||||
}
|
||||
#measurement {
|
||||
align-self: flex-end;
|
||||
display: inline-block;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.2em;
|
||||
margin-top: .1em;
|
||||
opacity: .6;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.graph {
|
||||
align-self: flex-end;
|
||||
margin: auto;
|
||||
margin-bottom: 0px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.graph > div {
|
||||
align-self: flex-end;
|
||||
margin: auto 8px;
|
||||
}
|
||||
</style>`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-sensor-card", HuiSensorCard);
|
||||
|
|
|
@ -1,66 +1,66 @@
|
|||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import createCardElement from "../common/create-card-element";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
cards: LovelaceConfig[];
|
||||
}
|
||||
|
||||
export abstract class HuiStackCard extends LitElement implements LovelaceCard {
|
||||
protected _cards?: LovelaceCard[];
|
||||
private _config?: Config;
|
||||
private _hass?: HomeAssistant;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
set hass(hass: HomeAssistant) {
|
||||
this._hass = hass;
|
||||
|
||||
if (!this._cards) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const element of this._cards) {
|
||||
element.hass = this._hass;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract getCardSize(): number;
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config || !config.cards || !Array.isArray(config.cards)) {
|
||||
throw new Error("Card config incorrect");
|
||||
}
|
||||
this._config = config;
|
||||
this._cards = config.cards.map((card) => {
|
||||
const element = createCardElement(card) as LovelaceCard;
|
||||
if (this._hass) {
|
||||
element.hass = this._hass;
|
||||
}
|
||||
return element;
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<div id="root">
|
||||
${this._cards}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected abstract renderStyle(): TemplateResult;
|
||||
}
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import createCardElement from "../common/create-card-element";
|
||||
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
cards: LovelaceConfig[];
|
||||
}
|
||||
|
||||
export abstract class HuiStackCard extends LitElement implements LovelaceCard {
|
||||
protected _cards?: LovelaceCard[];
|
||||
private _config?: Config;
|
||||
private _hass?: HomeAssistant;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
set hass(hass: HomeAssistant) {
|
||||
this._hass = hass;
|
||||
|
||||
if (!this._cards) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const element of this._cards) {
|
||||
element.hass = this._hass;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract getCardSize(): number;
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config || !config.cards || !Array.isArray(config.cards)) {
|
||||
throw new Error("Card config incorrect");
|
||||
}
|
||||
this._config = config;
|
||||
this._cards = config.cards.map((card) => {
|
||||
const element = createCardElement(card) as LovelaceCard;
|
||||
if (this._hass) {
|
||||
element.hass = this._hass;
|
||||
}
|
||||
return element;
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<div id="root">
|
||||
${this._cards}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected abstract renderStyle(): TemplateResult;
|
||||
}
|
||||
|
|
|
@ -1,390 +1,390 @@
|
|||
import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
} from "@polymer/lit-element";
|
||||
import { classMap } from "lit-html/directives/classMap";
|
||||
import { jQuery } from "../../../resources/jquery";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
|
||||
|
||||
import { HomeAssistant, ClimateEntity } from "../../../types";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
const thermostatConfig = {
|
||||
radius: 150,
|
||||
step: 1,
|
||||
circleShape: "pie",
|
||||
startAngle: 315,
|
||||
width: 5,
|
||||
lineCap: "round",
|
||||
handleSize: "+10",
|
||||
showTooltip: false,
|
||||
};
|
||||
|
||||
const modeIcons = {
|
||||
auto: "hass:autorenew",
|
||||
heat: "hass:fire",
|
||||
cool: "hass:snowflake",
|
||||
off: "hass:power",
|
||||
};
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
entity: string;
|
||||
}
|
||||
|
||||
function formatTemp(temps: string[]): string {
|
||||
return temps.filter(Boolean).join("-");
|
||||
}
|
||||
|
||||
export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceCard {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 4;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config.entity || config.entity.split(".")[0] !== "climate") {
|
||||
throw new Error("Specify an entity from within the climate domain.");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
const stateObj = this.hass.states[this._config.entity] as ClimateEntity;
|
||||
const broadCard = this.clientWidth > 390;
|
||||
const mode = modeIcons[stateObj.attributes.operation_mode || ""]
|
||||
? stateObj.attributes.operation_mode!
|
||||
: "unknown-mode";
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card
|
||||
class="${classMap({
|
||||
[mode]: true,
|
||||
large: broadCard,
|
||||
small: !broadCard,
|
||||
})}">
|
||||
<div id="root">
|
||||
<div id="thermostat"></div>
|
||||
<div id="tooltip">
|
||||
<div class="title">${computeStateName(stateObj)}</div>
|
||||
<div class="current-temperature">
|
||||
<span class="current-temperature-text">${
|
||||
stateObj.attributes.current_temperature
|
||||
}
|
||||
<span class="uom">${
|
||||
this.hass.config.unit_system.temperature
|
||||
}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="climate-info">
|
||||
<div id="set-temperature"></div>
|
||||
<div class="current-mode">${this.localize(
|
||||
`state.climate.${stateObj.state}`
|
||||
)}</div>
|
||||
<div class="modes">
|
||||
${(stateObj.attributes.operation_list || []).map((modeItem) =>
|
||||
this._renderIcon(modeItem, mode)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.get("hass")) {
|
||||
return (
|
||||
(changedProps.get("hass") as any).states[this._config!.entity] !==
|
||||
this.hass!.states[this._config!.entity]
|
||||
);
|
||||
}
|
||||
if (changedProps.has("_config")) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
|
||||
|
||||
const _sliderType =
|
||||
stateObj.attributes.target_temp_low &&
|
||||
stateObj.attributes.target_temp_high
|
||||
? "range"
|
||||
: "min-range";
|
||||
|
||||
jQuery("#thermostat", this.shadowRoot).roundSlider({
|
||||
...thermostatConfig,
|
||||
radius: this.clientWidth / 3,
|
||||
min: stateObj.attributes.min_temp,
|
||||
max: stateObj.attributes.max_temp,
|
||||
sliderType: _sliderType,
|
||||
change: (value) => this._setTemperature(value),
|
||||
drag: (value) => this._dragEvent(value),
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(): void {
|
||||
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
|
||||
|
||||
let sliderValue;
|
||||
let uiValue;
|
||||
|
||||
if (
|
||||
stateObj.attributes.target_temp_low &&
|
||||
stateObj.attributes.target_temp_high
|
||||
) {
|
||||
sliderValue = `${stateObj.attributes.target_temp_low}, ${
|
||||
stateObj.attributes.target_temp_high
|
||||
}`;
|
||||
uiValue = formatTemp([
|
||||
String(stateObj.attributes.target_temp_low),
|
||||
String(stateObj.attributes.target_temp_high),
|
||||
]);
|
||||
} else {
|
||||
sliderValue = uiValue = stateObj.attributes.temperature;
|
||||
}
|
||||
|
||||
jQuery("#thermostat", this.shadowRoot).roundSlider({
|
||||
value: sliderValue,
|
||||
});
|
||||
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = uiValue;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
${roundSliderStyle}
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
--rail-border-color: transparent;
|
||||
--auto-color: green;
|
||||
--cool-color: #2b9af9;
|
||||
--heat-color: #FF8100;
|
||||
--off-color: #8a8a8a;
|
||||
--unknown-color: #bac;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.auto {
|
||||
--mode-color: var(--auto-color);
|
||||
}
|
||||
.cool {
|
||||
--mode-color: var(--cool-color);
|
||||
}
|
||||
.heat {
|
||||
--mode-color: var(--heat-color);
|
||||
}
|
||||
.off {
|
||||
--mode-color: var(--off-color);
|
||||
}
|
||||
.unknown-mode {
|
||||
--mode-color: var(--unknown-color);
|
||||
}
|
||||
.no-title {
|
||||
--title-margin-top: 33% !important;
|
||||
}
|
||||
.large {
|
||||
--thermostat-padding-top: 25px;
|
||||
--thermostat-margin-bottom: 25px;
|
||||
--title-font-size: 28px;
|
||||
--title-margin-top: 20%;
|
||||
--climate-info-margin-top: 17%;
|
||||
--modes-margin-top: 2%;
|
||||
--set-temperature-font-size: 25px;
|
||||
--current-temperature-font-size: 71px;
|
||||
--current-temperature-margin-top: 10%;
|
||||
--current-temperature-text-padding-left: 15px;
|
||||
--uom-font-size: 20px;
|
||||
--uom-margin-left: -18px;
|
||||
--current-mode-font-size: 18px;
|
||||
--set-temperature-padding-bottom: 5px;
|
||||
}
|
||||
.small {
|
||||
--thermostat-padding-top: 15px;
|
||||
--thermostat-margin-bottom: 15px;
|
||||
--title-font-size: 18px;
|
||||
--title-margin-top: 20%;
|
||||
--climate-info-margin-top: 7.5%;
|
||||
--modes-margin-top: 1%;
|
||||
--set-temperature-font-size: 16px;
|
||||
--current-temperature-font-size: 25px;
|
||||
--current-temperature-margin-top: 5%;
|
||||
--current-temperature-text-padding-left: 7px;
|
||||
--uom-font-size: 12px;
|
||||
--uom-margin-left: -5px;
|
||||
--current-mode-font-size: 14px;
|
||||
--set-temperature-padding-bottom: 0px;
|
||||
}
|
||||
#thermostat {
|
||||
margin: 0 auto var(--thermostat-margin-bottom);
|
||||
padding-top: var(--thermostat-padding-top);
|
||||
}
|
||||
#thermostat .rs-range-color {
|
||||
background-color: var(--mode-color, var(--disabled-text-color));
|
||||
}
|
||||
#thermostat .rs-path-color {
|
||||
background-color: var(--disabled-text-color);
|
||||
}
|
||||
#thermostat .rs-handle {
|
||||
background-color: var(--paper-card-background-color, white);
|
||||
padding: 7px;
|
||||
border: 2px solid var(--disabled-text-color);
|
||||
}
|
||||
#thermostat .rs-handle.rs-focus {
|
||||
border-color: var(--mode-color, var(--disabled-text-color));
|
||||
}
|
||||
#thermostat .rs-handle:after {
|
||||
border-color: var(--mode-color, var(--disabled-text-color));
|
||||
background-color: var(--mode-color, var(--disabled-text-color));
|
||||
}
|
||||
#thermostat .rs-border {
|
||||
border-color: var(--rail-border-color);
|
||||
}
|
||||
#thermostat .rs-bar.rs-transition.rs-first, .rs-bar.rs-transition.rs-second{
|
||||
z-index: 20 !important;
|
||||
}
|
||||
#thermostat .rs-inner.rs-bg-color.rs-border,
|
||||
#thermostat .rs-overlay.rs-transition.rs-bg-color {
|
||||
background-color: var(--paper-card-background-color, white);
|
||||
}
|
||||
#tooltip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
z-index: 15;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
#set-temperature {
|
||||
font-size: var(--set-temperature-font-size);
|
||||
padding-bottom: var(--set-temperature-padding-bottom);
|
||||
}
|
||||
.title {
|
||||
font-size: var(--title-font-size);
|
||||
margin-top: var(--title-margin-top);
|
||||
}
|
||||
.climate-info {
|
||||
margin-top: var(--climate-info-margin-top);
|
||||
}
|
||||
.current-mode {
|
||||
font-size: var(--current-mode-font-size);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.modes {
|
||||
margin-top: var(--modes-margin-top);
|
||||
}
|
||||
.modes ha-icon {
|
||||
color: var(--disabled-text-color);
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
.modes ha-icon.selected-icon {
|
||||
color: var(--mode-color);
|
||||
}
|
||||
.current-temperature {
|
||||
margin-top: var(--current-temperature-margin-top);
|
||||
font-size: var(--current-temperature-font-size);
|
||||
}
|
||||
.current-temperature-text {
|
||||
padding-left: var(--current-temperature-text-padding-left);
|
||||
}
|
||||
.uom {
|
||||
font-size: var(--uom-font-size);
|
||||
vertical-align: top;
|
||||
margin-left: var(--uom-margin-left);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _dragEvent(e): void {
|
||||
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = formatTemp(
|
||||
String(e.value).split(",")
|
||||
);
|
||||
}
|
||||
|
||||
private _setTemperature(e): void {
|
||||
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
|
||||
if (
|
||||
stateObj.attributes.target_temp_low &&
|
||||
stateObj.attributes.target_temp_high
|
||||
) {
|
||||
if (e.handle.index === 1) {
|
||||
this.hass!.callService("climate", "set_temperature", {
|
||||
entity_id: this._config!.entity,
|
||||
target_temp_low: e.handle.value,
|
||||
target_temp_high: stateObj.attributes.target_temp_high,
|
||||
});
|
||||
} else {
|
||||
this.hass!.callService("climate", "set_temperature", {
|
||||
entity_id: this._config!.entity,
|
||||
target_temp_low: stateObj.attributes.target_temp_low,
|
||||
target_temp_high: e.handle.value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.hass!.callService("climate", "set_temperature", {
|
||||
entity_id: this._config!.entity,
|
||||
temperature: e.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _renderIcon(mode: string, currentMode: string): TemplateResult {
|
||||
if (!modeIcons[mode]) {
|
||||
return html``;
|
||||
}
|
||||
return html`<ha-icon
|
||||
class="${classMap({ "selected-icon": currentMode === mode })}"
|
||||
.mode="${mode}"
|
||||
.icon="${modeIcons[mode]}"
|
||||
@click="${this._handleModeClick}"
|
||||
></ha-icon>`;
|
||||
}
|
||||
|
||||
private _handleModeClick(e: MouseEvent): void {
|
||||
this.hass!.callService("climate", "set_operation_mode", {
|
||||
entity_id: this._config!.entity,
|
||||
operation_mode: (e.currentTarget as any).mode,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-thermostat-card": HuiThermostatCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-thermostat-card", HuiThermostatCard);
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
} from "@polymer/lit-element";
|
||||
import { classMap } from "lit-html/directives/classMap";
|
||||
import { jQuery } from "../../../resources/jquery";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
|
||||
|
||||
import { HomeAssistant, ClimateEntity } from "../../../types";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
const thermostatConfig = {
|
||||
radius: 150,
|
||||
step: 1,
|
||||
circleShape: "pie",
|
||||
startAngle: 315,
|
||||
width: 5,
|
||||
lineCap: "round",
|
||||
handleSize: "+10",
|
||||
showTooltip: false,
|
||||
};
|
||||
|
||||
const modeIcons = {
|
||||
auto: "hass:autorenew",
|
||||
heat: "hass:fire",
|
||||
cool: "hass:snowflake",
|
||||
off: "hass:power",
|
||||
};
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
entity: string;
|
||||
}
|
||||
|
||||
function formatTemp(temps: string[]): string {
|
||||
return temps.filter(Boolean).join("-");
|
||||
}
|
||||
|
||||
export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceCard {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 4;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config.entity || config.entity.split(".")[0] !== "climate") {
|
||||
throw new Error("Specify an entity from within the climate domain.");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
const stateObj = this.hass.states[this._config.entity] as ClimateEntity;
|
||||
const broadCard = this.clientWidth > 390;
|
||||
const mode = modeIcons[stateObj.attributes.operation_mode || ""]
|
||||
? stateObj.attributes.operation_mode!
|
||||
: "unknown-mode";
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card
|
||||
class="${classMap({
|
||||
[mode]: true,
|
||||
large: broadCard,
|
||||
small: !broadCard,
|
||||
})}">
|
||||
<div id="root">
|
||||
<div id="thermostat"></div>
|
||||
<div id="tooltip">
|
||||
<div class="title">${computeStateName(stateObj)}</div>
|
||||
<div class="current-temperature">
|
||||
<span class="current-temperature-text">${
|
||||
stateObj.attributes.current_temperature
|
||||
}
|
||||
<span class="uom">${
|
||||
this.hass.config.unit_system.temperature
|
||||
}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="climate-info">
|
||||
<div id="set-temperature"></div>
|
||||
<div class="current-mode">${this.localize(
|
||||
`state.climate.${stateObj.state}`
|
||||
)}</div>
|
||||
<div class="modes">
|
||||
${(stateObj.attributes.operation_list || []).map((modeItem) =>
|
||||
this._renderIcon(modeItem, mode)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.get("hass")) {
|
||||
return (
|
||||
(changedProps.get("hass") as any).states[this._config!.entity] !==
|
||||
this.hass!.states[this._config!.entity]
|
||||
);
|
||||
}
|
||||
if (changedProps.has("_config")) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
|
||||
|
||||
const _sliderType =
|
||||
stateObj.attributes.target_temp_low &&
|
||||
stateObj.attributes.target_temp_high
|
||||
? "range"
|
||||
: "min-range";
|
||||
|
||||
jQuery("#thermostat", this.shadowRoot).roundSlider({
|
||||
...thermostatConfig,
|
||||
radius: this.clientWidth / 3,
|
||||
min: stateObj.attributes.min_temp,
|
||||
max: stateObj.attributes.max_temp,
|
||||
sliderType: _sliderType,
|
||||
change: (value) => this._setTemperature(value),
|
||||
drag: (value) => this._dragEvent(value),
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(): void {
|
||||
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
|
||||
|
||||
let sliderValue;
|
||||
let uiValue;
|
||||
|
||||
if (
|
||||
stateObj.attributes.target_temp_low &&
|
||||
stateObj.attributes.target_temp_high
|
||||
) {
|
||||
sliderValue = `${stateObj.attributes.target_temp_low}, ${
|
||||
stateObj.attributes.target_temp_high
|
||||
}`;
|
||||
uiValue = formatTemp([
|
||||
String(stateObj.attributes.target_temp_low),
|
||||
String(stateObj.attributes.target_temp_high),
|
||||
]);
|
||||
} else {
|
||||
sliderValue = uiValue = stateObj.attributes.temperature;
|
||||
}
|
||||
|
||||
jQuery("#thermostat", this.shadowRoot).roundSlider({
|
||||
value: sliderValue,
|
||||
});
|
||||
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = uiValue;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
${roundSliderStyle}
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
--rail-border-color: transparent;
|
||||
--auto-color: green;
|
||||
--cool-color: #2b9af9;
|
||||
--heat-color: #FF8100;
|
||||
--off-color: #8a8a8a;
|
||||
--unknown-color: #bac;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.auto {
|
||||
--mode-color: var(--auto-color);
|
||||
}
|
||||
.cool {
|
||||
--mode-color: var(--cool-color);
|
||||
}
|
||||
.heat {
|
||||
--mode-color: var(--heat-color);
|
||||
}
|
||||
.off {
|
||||
--mode-color: var(--off-color);
|
||||
}
|
||||
.unknown-mode {
|
||||
--mode-color: var(--unknown-color);
|
||||
}
|
||||
.no-title {
|
||||
--title-margin-top: 33% !important;
|
||||
}
|
||||
.large {
|
||||
--thermostat-padding-top: 25px;
|
||||
--thermostat-margin-bottom: 25px;
|
||||
--title-font-size: 28px;
|
||||
--title-margin-top: 20%;
|
||||
--climate-info-margin-top: 17%;
|
||||
--modes-margin-top: 2%;
|
||||
--set-temperature-font-size: 25px;
|
||||
--current-temperature-font-size: 71px;
|
||||
--current-temperature-margin-top: 10%;
|
||||
--current-temperature-text-padding-left: 15px;
|
||||
--uom-font-size: 20px;
|
||||
--uom-margin-left: -18px;
|
||||
--current-mode-font-size: 18px;
|
||||
--set-temperature-padding-bottom: 5px;
|
||||
}
|
||||
.small {
|
||||
--thermostat-padding-top: 15px;
|
||||
--thermostat-margin-bottom: 15px;
|
||||
--title-font-size: 18px;
|
||||
--title-margin-top: 20%;
|
||||
--climate-info-margin-top: 7.5%;
|
||||
--modes-margin-top: 1%;
|
||||
--set-temperature-font-size: 16px;
|
||||
--current-temperature-font-size: 25px;
|
||||
--current-temperature-margin-top: 5%;
|
||||
--current-temperature-text-padding-left: 7px;
|
||||
--uom-font-size: 12px;
|
||||
--uom-margin-left: -5px;
|
||||
--current-mode-font-size: 14px;
|
||||
--set-temperature-padding-bottom: 0px;
|
||||
}
|
||||
#thermostat {
|
||||
margin: 0 auto var(--thermostat-margin-bottom);
|
||||
padding-top: var(--thermostat-padding-top);
|
||||
}
|
||||
#thermostat .rs-range-color {
|
||||
background-color: var(--mode-color, var(--disabled-text-color));
|
||||
}
|
||||
#thermostat .rs-path-color {
|
||||
background-color: var(--disabled-text-color);
|
||||
}
|
||||
#thermostat .rs-handle {
|
||||
background-color: var(--paper-card-background-color, white);
|
||||
padding: 7px;
|
||||
border: 2px solid var(--disabled-text-color);
|
||||
}
|
||||
#thermostat .rs-handle.rs-focus {
|
||||
border-color: var(--mode-color, var(--disabled-text-color));
|
||||
}
|
||||
#thermostat .rs-handle:after {
|
||||
border-color: var(--mode-color, var(--disabled-text-color));
|
||||
background-color: var(--mode-color, var(--disabled-text-color));
|
||||
}
|
||||
#thermostat .rs-border {
|
||||
border-color: var(--rail-border-color);
|
||||
}
|
||||
#thermostat .rs-bar.rs-transition.rs-first, .rs-bar.rs-transition.rs-second{
|
||||
z-index: 20 !important;
|
||||
}
|
||||
#thermostat .rs-inner.rs-bg-color.rs-border,
|
||||
#thermostat .rs-overlay.rs-transition.rs-bg-color {
|
||||
background-color: var(--paper-card-background-color, white);
|
||||
}
|
||||
#tooltip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
z-index: 15;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
#set-temperature {
|
||||
font-size: var(--set-temperature-font-size);
|
||||
padding-bottom: var(--set-temperature-padding-bottom);
|
||||
}
|
||||
.title {
|
||||
font-size: var(--title-font-size);
|
||||
margin-top: var(--title-margin-top);
|
||||
}
|
||||
.climate-info {
|
||||
margin-top: var(--climate-info-margin-top);
|
||||
}
|
||||
.current-mode {
|
||||
font-size: var(--current-mode-font-size);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.modes {
|
||||
margin-top: var(--modes-margin-top);
|
||||
}
|
||||
.modes ha-icon {
|
||||
color: var(--disabled-text-color);
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
.modes ha-icon.selected-icon {
|
||||
color: var(--mode-color);
|
||||
}
|
||||
.current-temperature {
|
||||
margin-top: var(--current-temperature-margin-top);
|
||||
font-size: var(--current-temperature-font-size);
|
||||
}
|
||||
.current-temperature-text {
|
||||
padding-left: var(--current-temperature-text-padding-left);
|
||||
}
|
||||
.uom {
|
||||
font-size: var(--uom-font-size);
|
||||
vertical-align: top;
|
||||
margin-left: var(--uom-margin-left);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _dragEvent(e): void {
|
||||
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = formatTemp(
|
||||
String(e.value).split(",")
|
||||
);
|
||||
}
|
||||
|
||||
private _setTemperature(e): void {
|
||||
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
|
||||
if (
|
||||
stateObj.attributes.target_temp_low &&
|
||||
stateObj.attributes.target_temp_high
|
||||
) {
|
||||
if (e.handle.index === 1) {
|
||||
this.hass!.callService("climate", "set_temperature", {
|
||||
entity_id: this._config!.entity,
|
||||
target_temp_low: e.handle.value,
|
||||
target_temp_high: stateObj.attributes.target_temp_high,
|
||||
});
|
||||
} else {
|
||||
this.hass!.callService("climate", "set_temperature", {
|
||||
entity_id: this._config!.entity,
|
||||
target_temp_low: stateObj.attributes.target_temp_low,
|
||||
target_temp_high: e.handle.value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.hass!.callService("climate", "set_temperature", {
|
||||
entity_id: this._config!.entity,
|
||||
temperature: e.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _renderIcon(mode: string, currentMode: string): TemplateResult {
|
||||
if (!modeIcons[mode]) {
|
||||
return html``;
|
||||
}
|
||||
return html`<ha-icon
|
||||
class="${classMap({ "selected-icon": currentMode === mode })}"
|
||||
.mode="${mode}"
|
||||
.icon="${modeIcons[mode]}"
|
||||
@click="${this._handleModeClick}"
|
||||
></ha-icon>`;
|
||||
}
|
||||
|
||||
private _handleModeClick(e: MouseEvent): void {
|
||||
this.hass!.callService("climate", "set_operation_mode", {
|
||||
entity_id: this._config!.entity,
|
||||
operation_mode: (e.currentTarget as any).mode,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-thermostat-card": HuiThermostatCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-thermostat-card", HuiThermostatCard);
|
||||
|
|
|
@ -1,50 +1,50 @@
|
|||
import { html } from "@polymer/lit-element";
|
||||
|
||||
import computeCardSize from "../common/compute-card-size";
|
||||
|
||||
import { HuiStackCard } from "./hui-stack-card";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
class HuiVerticalStackCard extends HuiStackCard {
|
||||
public getCardSize() {
|
||||
let totalSize = 0;
|
||||
|
||||
if (!this._cards) {
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
for (const element of this._cards) {
|
||||
totalSize += computeCardSize(element);
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
protected renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#root > * {
|
||||
margin: 4px 0 4px 0;
|
||||
}
|
||||
#root > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
#root > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-vertical-stack-card": HuiVerticalStackCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-vertical-stack-card", HuiVerticalStackCard);
|
||||
import { html } from "@polymer/lit-element";
|
||||
|
||||
import computeCardSize from "../common/compute-card-size";
|
||||
|
||||
import { HuiStackCard } from "./hui-stack-card";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
class HuiVerticalStackCard extends HuiStackCard {
|
||||
public getCardSize() {
|
||||
let totalSize = 0;
|
||||
|
||||
if (!this._cards) {
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
for (const element of this._cards) {
|
||||
totalSize += computeCardSize(element);
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
protected renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#root > * {
|
||||
margin: 4px 0 4px 0;
|
||||
}
|
||||
#root > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
#root > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-vertical-stack-card": HuiVerticalStackCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-vertical-stack-card", HuiVerticalStackCard);
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import "../../../cards/ha-camera-card";
|
||||
|
||||
import LegacyWrapperCard from "./hui-legacy-wrapper-card";
|
||||
|
||||
class HuiWeatherForecastCard extends LegacyWrapperCard {
|
||||
constructor() {
|
||||
super("ha-weather-card", "weather");
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-weather-forecast-card", HuiWeatherForecastCard);
|
||||
import "../../../cards/ha-camera-card";
|
||||
|
||||
import LegacyWrapperCard from "./hui-legacy-wrapper-card";
|
||||
|
||||
class HuiWeatherForecastCard extends LegacyWrapperCard {
|
||||
constructor() {
|
||||
super("ha-weather-card", "weather");
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-weather-forecast-card", HuiWeatherForecastCard);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import computeDomain from "../../../common/entity/compute_domain";
|
||||
|
||||
export default function computeNotifications(states) {
|
||||
return Object.keys(states)
|
||||
.filter((entityId) => computeDomain(entityId) === "configurator")
|
||||
.map((entityId) => states[entityId]);
|
||||
}
|
||||
import computeDomain from "../../../common/entity/compute_domain";
|
||||
|
||||
export default function computeNotifications(states) {
|
||||
return Object.keys(states)
|
||||
.filter((entityId) => computeDomain(entityId) === "configurator")
|
||||
.map((entityId) => states[entityId]);
|
||||
}
|
||||
|
|
|
@ -1,38 +1,38 @@
|
|||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceElementConfig } from "../elements/types";
|
||||
|
||||
export const computeTooltip = (
|
||||
hass: HomeAssistant,
|
||||
config: LovelaceElementConfig
|
||||
): string => {
|
||||
if (config.title) {
|
||||
return config.title;
|
||||
}
|
||||
|
||||
let stateName = "";
|
||||
let tooltip: string;
|
||||
|
||||
if (config.entity) {
|
||||
stateName =
|
||||
config.entity in hass.states
|
||||
? computeStateName(hass.states[config.entity])
|
||||
: config.entity;
|
||||
}
|
||||
|
||||
switch (config.tap_action) {
|
||||
case "navigate":
|
||||
tooltip = `Navigate to ${config.navigation_path}`;
|
||||
break;
|
||||
case "toggle":
|
||||
tooltip = `Toggle ${stateName}`;
|
||||
break;
|
||||
case "call-service":
|
||||
tooltip = `Call service ${config.service}`;
|
||||
break;
|
||||
default:
|
||||
tooltip = `Show more-info: ${stateName}`;
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
};
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceElementConfig } from "../elements/types";
|
||||
|
||||
export const computeTooltip = (
|
||||
hass: HomeAssistant,
|
||||
config: LovelaceElementConfig
|
||||
): string => {
|
||||
if (config.title) {
|
||||
return config.title;
|
||||
}
|
||||
|
||||
let stateName = "";
|
||||
let tooltip: string;
|
||||
|
||||
if (config.entity) {
|
||||
stateName =
|
||||
config.entity in hass.states
|
||||
? computeStateName(hass.states[config.entity])
|
||||
: config.entity;
|
||||
}
|
||||
|
||||
switch (config.tap_action) {
|
||||
case "navigate":
|
||||
tooltip = `Navigate to ${config.navigation_path}`;
|
||||
break;
|
||||
case "toggle":
|
||||
tooltip = `Toggle ${stateName}`;
|
||||
break;
|
||||
case "call-service":
|
||||
tooltip = `Call service ${config.service}`;
|
||||
break;
|
||||
default:
|
||||
tooltip = `Show more-info: ${stateName}`;
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
};
|
||||
|
|
|
@ -1,112 +1,112 @@
|
|||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
import "../cards/hui-alarm-panel-card";
|
||||
import "../cards/hui-conditional-card.ts";
|
||||
import "../cards/hui-entities-card.ts";
|
||||
import "../cards/hui-entity-button-card.ts";
|
||||
import "../cards/hui-entity-filter-card";
|
||||
import "../cards/hui-error-card.ts";
|
||||
import "../cards/hui-glance-card.ts";
|
||||
import "../cards/hui-history-graph-card";
|
||||
import "../cards/hui-horizontal-stack-card.ts";
|
||||
import "../cards/hui-iframe-card.ts";
|
||||
import "../cards/hui-light-card";
|
||||
import "../cards/hui-map-card";
|
||||
import "../cards/hui-markdown-card.ts";
|
||||
import "../cards/hui-media-control-card";
|
||||
import "../cards/hui-picture-card";
|
||||
import "../cards/hui-picture-elements-card";
|
||||
import "../cards/hui-picture-entity-card";
|
||||
import "../cards/hui-picture-glance-card";
|
||||
import "../cards/hui-plant-status-card";
|
||||
import "../cards/hui-sensor-card";
|
||||
import "../cards/hui-vertical-stack-card.ts";
|
||||
import "../cards/hui-thermostat-card.ts";
|
||||
import "../cards/hui-weather-forecast-card";
|
||||
import "../cards/hui-gauge-card";
|
||||
|
||||
import createErrorCardConfig from "./create-error-card-config";
|
||||
|
||||
const CARD_TYPES = new Set([
|
||||
"alarm-panel",
|
||||
"conditional",
|
||||
"entities",
|
||||
"entity-button",
|
||||
"entity-filter",
|
||||
"error",
|
||||
"gauge",
|
||||
"glance",
|
||||
"history-graph",
|
||||
"horizontal-stack",
|
||||
"iframe",
|
||||
"light",
|
||||
"map",
|
||||
"markdown",
|
||||
"media-control",
|
||||
"picture",
|
||||
"picture-elements",
|
||||
"picture-entity",
|
||||
"picture-glance",
|
||||
"plant-status",
|
||||
"sensor",
|
||||
"thermostat",
|
||||
"vertical-stack",
|
||||
"weather-forecast",
|
||||
]);
|
||||
const CUSTOM_TYPE_PREFIX = "custom:";
|
||||
const TIMEOUT = 2000;
|
||||
|
||||
function _createElement(tag, config) {
|
||||
const element = document.createElement(tag);
|
||||
try {
|
||||
element.setConfig(config);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error(tag, err);
|
||||
// eslint-disable-next-line
|
||||
return _createErrorElement(err.message, config);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function _createErrorElement(error, config) {
|
||||
return _createElement("hui-error-card", createErrorCardConfig(error, config));
|
||||
}
|
||||
|
||||
export default function createCardElement(config) {
|
||||
if (!config || typeof config !== "object" || !config.type) {
|
||||
return _createErrorElement("No card type configured.", config);
|
||||
}
|
||||
|
||||
if (config.type.startsWith(CUSTOM_TYPE_PREFIX)) {
|
||||
const tag = config.type.substr(CUSTOM_TYPE_PREFIX.length);
|
||||
|
||||
if (customElements.get(tag)) {
|
||||
return _createElement(tag, config);
|
||||
}
|
||||
const element = _createErrorElement(
|
||||
`Custom element doesn't exist: ${tag}.`,
|
||||
config
|
||||
);
|
||||
element.style.display = "None";
|
||||
const timer = window.setTimeout(() => {
|
||||
element.style.display = "";
|
||||
}, TIMEOUT);
|
||||
|
||||
customElements.whenDefined(tag).then(() => {
|
||||
clearTimeout(timer);
|
||||
fireEvent(element, "rebuild-view");
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
if (!CARD_TYPES.has(config.type)) {
|
||||
return _createErrorElement(
|
||||
`Unknown card type encountered: ${config.type}.`,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
return _createElement(`hui-${config.type}-card`, config);
|
||||
}
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
import "../cards/hui-alarm-panel-card";
|
||||
import "../cards/hui-conditional-card.ts";
|
||||
import "../cards/hui-entities-card.ts";
|
||||
import "../cards/hui-entity-button-card.ts";
|
||||
import "../cards/hui-entity-filter-card";
|
||||
import "../cards/hui-error-card.ts";
|
||||
import "../cards/hui-glance-card.ts";
|
||||
import "../cards/hui-history-graph-card";
|
||||
import "../cards/hui-horizontal-stack-card.ts";
|
||||
import "../cards/hui-iframe-card.ts";
|
||||
import "../cards/hui-light-card";
|
||||
import "../cards/hui-map-card";
|
||||
import "../cards/hui-markdown-card.ts";
|
||||
import "../cards/hui-media-control-card";
|
||||
import "../cards/hui-picture-card";
|
||||
import "../cards/hui-picture-elements-card";
|
||||
import "../cards/hui-picture-entity-card";
|
||||
import "../cards/hui-picture-glance-card";
|
||||
import "../cards/hui-plant-status-card";
|
||||
import "../cards/hui-sensor-card";
|
||||
import "../cards/hui-vertical-stack-card.ts";
|
||||
import "../cards/hui-thermostat-card.ts";
|
||||
import "../cards/hui-weather-forecast-card";
|
||||
import "../cards/hui-gauge-card";
|
||||
|
||||
import createErrorCardConfig from "./create-error-card-config";
|
||||
|
||||
const CARD_TYPES = new Set([
|
||||
"alarm-panel",
|
||||
"conditional",
|
||||
"entities",
|
||||
"entity-button",
|
||||
"entity-filter",
|
||||
"error",
|
||||
"gauge",
|
||||
"glance",
|
||||
"history-graph",
|
||||
"horizontal-stack",
|
||||
"iframe",
|
||||
"light",
|
||||
"map",
|
||||
"markdown",
|
||||
"media-control",
|
||||
"picture",
|
||||
"picture-elements",
|
||||
"picture-entity",
|
||||
"picture-glance",
|
||||
"plant-status",
|
||||
"sensor",
|
||||
"thermostat",
|
||||
"vertical-stack",
|
||||
"weather-forecast",
|
||||
]);
|
||||
const CUSTOM_TYPE_PREFIX = "custom:";
|
||||
const TIMEOUT = 2000;
|
||||
|
||||
function _createElement(tag, config) {
|
||||
const element = document.createElement(tag);
|
||||
try {
|
||||
element.setConfig(config);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error(tag, err);
|
||||
// eslint-disable-next-line
|
||||
return _createErrorElement(err.message, config);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function _createErrorElement(error, config) {
|
||||
return _createElement("hui-error-card", createErrorCardConfig(error, config));
|
||||
}
|
||||
|
||||
export default function createCardElement(config) {
|
||||
if (!config || typeof config !== "object" || !config.type) {
|
||||
return _createErrorElement("No card type configured.", config);
|
||||
}
|
||||
|
||||
if (config.type.startsWith(CUSTOM_TYPE_PREFIX)) {
|
||||
const tag = config.type.substr(CUSTOM_TYPE_PREFIX.length);
|
||||
|
||||
if (customElements.get(tag)) {
|
||||
return _createElement(tag, config);
|
||||
}
|
||||
const element = _createErrorElement(
|
||||
`Custom element doesn't exist: ${tag}.`,
|
||||
config
|
||||
);
|
||||
element.style.display = "None";
|
||||
const timer = window.setTimeout(() => {
|
||||
element.style.display = "";
|
||||
}, TIMEOUT);
|
||||
|
||||
customElements.whenDefined(tag).then(() => {
|
||||
clearTimeout(timer);
|
||||
fireEvent(element, "rebuild-view");
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
if (!CARD_TYPES.has(config.type)) {
|
||||
return _createErrorElement(
|
||||
`Unknown card type encountered: ${config.type}.`,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
return _createElement(`hui-${config.type}-card`, config);
|
||||
}
|
||||
|
|
|
@ -1,79 +1,79 @@
|
|||
import "../elements/hui-icon-element";
|
||||
import "../elements/hui-image-element";
|
||||
import "../elements/hui-service-button-element";
|
||||
import "../elements/hui-state-badge-element";
|
||||
import "../elements/hui-state-icon-element";
|
||||
import "../elements/hui-state-label-element";
|
||||
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import createErrorCardConfig from "./create-error-card-config";
|
||||
|
||||
const CUSTOM_TYPE_PREFIX = "custom:";
|
||||
const ELEMENT_TYPES = new Set([
|
||||
"icon",
|
||||
"image",
|
||||
"service-button",
|
||||
"state-badge",
|
||||
"state-icon",
|
||||
"state-label",
|
||||
]);
|
||||
const TIMEOUT = 2000;
|
||||
|
||||
function _createElement(tag, config) {
|
||||
const element = document.createElement(tag);
|
||||
try {
|
||||
element.setConfig(config);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error(tag, err);
|
||||
// eslint-disable-next-line
|
||||
return _createErrorElement(err.message, config);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function _createErrorElement(error, config) {
|
||||
return _createElement("hui-error-card", createErrorCardConfig(error, config));
|
||||
}
|
||||
|
||||
function _hideErrorElement(element) {
|
||||
element.style.display = "None";
|
||||
return window.setTimeout(() => {
|
||||
element.style.display = "";
|
||||
}, TIMEOUT);
|
||||
}
|
||||
|
||||
export default function createHuiElement(config) {
|
||||
if (!config || typeof config !== "object" || !config.type) {
|
||||
return _createErrorElement("No element type configured.", config);
|
||||
}
|
||||
|
||||
if (config.type.startsWith(CUSTOM_TYPE_PREFIX)) {
|
||||
const tag = config.type.substr(CUSTOM_TYPE_PREFIX.length);
|
||||
|
||||
if (customElements.get(tag)) {
|
||||
return _createElement(tag, config);
|
||||
}
|
||||
const element = _createErrorElement(
|
||||
`Custom element doesn't exist: ${tag}.`,
|
||||
config
|
||||
);
|
||||
const timer = _hideErrorElement(element);
|
||||
|
||||
customElements.whenDefined(tag).then(() => {
|
||||
clearTimeout(timer);
|
||||
fireEvent(element, "rebuild-view");
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
if (!ELEMENT_TYPES.has(config.type)) {
|
||||
return _createErrorElement(
|
||||
`Unknown element type encountered: ${config.type}.`,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
return _createElement(`hui-${config.type}-element`, config);
|
||||
}
|
||||
import "../elements/hui-icon-element";
|
||||
import "../elements/hui-image-element";
|
||||
import "../elements/hui-service-button-element";
|
||||
import "../elements/hui-state-badge-element";
|
||||
import "../elements/hui-state-icon-element";
|
||||
import "../elements/hui-state-label-element";
|
||||
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import createErrorCardConfig from "./create-error-card-config";
|
||||
|
||||
const CUSTOM_TYPE_PREFIX = "custom:";
|
||||
const ELEMENT_TYPES = new Set([
|
||||
"icon",
|
||||
"image",
|
||||
"service-button",
|
||||
"state-badge",
|
||||
"state-icon",
|
||||
"state-label",
|
||||
]);
|
||||
const TIMEOUT = 2000;
|
||||
|
||||
function _createElement(tag, config) {
|
||||
const element = document.createElement(tag);
|
||||
try {
|
||||
element.setConfig(config);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error(tag, err);
|
||||
// eslint-disable-next-line
|
||||
return _createErrorElement(err.message, config);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function _createErrorElement(error, config) {
|
||||
return _createElement("hui-error-card", createErrorCardConfig(error, config));
|
||||
}
|
||||
|
||||
function _hideErrorElement(element) {
|
||||
element.style.display = "None";
|
||||
return window.setTimeout(() => {
|
||||
element.style.display = "";
|
||||
}, TIMEOUT);
|
||||
}
|
||||
|
||||
export default function createHuiElement(config) {
|
||||
if (!config || typeof config !== "object" || !config.type) {
|
||||
return _createErrorElement("No element type configured.", config);
|
||||
}
|
||||
|
||||
if (config.type.startsWith(CUSTOM_TYPE_PREFIX)) {
|
||||
const tag = config.type.substr(CUSTOM_TYPE_PREFIX.length);
|
||||
|
||||
if (customElements.get(tag)) {
|
||||
return _createElement(tag, config);
|
||||
}
|
||||
const element = _createErrorElement(
|
||||
`Custom element doesn't exist: ${tag}.`,
|
||||
config
|
||||
);
|
||||
const timer = _hideErrorElement(element);
|
||||
|
||||
customElements.whenDefined(tag).then(() => {
|
||||
clearTimeout(timer);
|
||||
fireEvent(element, "rebuild-view");
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
if (!ELEMENT_TYPES.has(config.type)) {
|
||||
return _createErrorElement(
|
||||
`Unknown element type encountered: ${config.type}.`,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
return _createElement(`hui-${config.type}-element`, config);
|
||||
}
|
||||
|
|
|
@ -1,117 +1,117 @@
|
|||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
import "../entity-rows/hui-climate-entity-row";
|
||||
import "../entity-rows/hui-cover-entity-row";
|
||||
import "../entity-rows/hui-group-entity-row";
|
||||
import "../entity-rows/hui-input-number-entity-row";
|
||||
import "../entity-rows/hui-input-select-entity-row";
|
||||
import "../entity-rows/hui-input-text-entity-row";
|
||||
import "../entity-rows/hui-lock-entity-row";
|
||||
import "../entity-rows/hui-media-player-entity-row";
|
||||
import "../entity-rows/hui-scene-entity-row";
|
||||
import "../entity-rows/hui-script-entity-row";
|
||||
import "../entity-rows/hui-text-entity-row";
|
||||
import "../entity-rows/hui-timer-entity-row";
|
||||
import "../entity-rows/hui-toggle-entity-row";
|
||||
|
||||
import "../special-rows/hui-call-service-row";
|
||||
import "../special-rows/hui-divider-row";
|
||||
import "../special-rows/hui-section-row";
|
||||
import "../special-rows/hui-weblink-row";
|
||||
|
||||
import createErrorCardConfig from "./create-error-card-config";
|
||||
|
||||
const CUSTOM_TYPE_PREFIX = "custom:";
|
||||
const SPECIAL_TYPES = new Set([
|
||||
"call-service",
|
||||
"divider",
|
||||
"section",
|
||||
"weblink",
|
||||
]);
|
||||
const DOMAIN_TO_ELEMENT_TYPE = {
|
||||
automation: "toggle",
|
||||
climate: "climate",
|
||||
cover: "cover",
|
||||
fan: "toggle",
|
||||
group: "group",
|
||||
input_boolean: "toggle",
|
||||
input_number: "input-number",
|
||||
input_select: "input-select",
|
||||
input_text: "input-text",
|
||||
light: "toggle",
|
||||
media_player: "media-player",
|
||||
lock: "lock",
|
||||
scene: "scene",
|
||||
script: "script",
|
||||
timer: "timer",
|
||||
switch: "toggle",
|
||||
vacuum: "toggle",
|
||||
};
|
||||
const TIMEOUT = 2000;
|
||||
|
||||
function _createElement(tag, config) {
|
||||
const element = document.createElement(tag);
|
||||
try {
|
||||
if ("setConfig" in element) element.setConfig(config);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error(tag, err);
|
||||
// eslint-disable-next-line
|
||||
return _createErrorElement(err.message, config);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function _createErrorElement(error, config) {
|
||||
return _createElement("hui-error-card", createErrorCardConfig(error, config));
|
||||
}
|
||||
|
||||
function _hideErrorElement(element) {
|
||||
element.style.display = "None";
|
||||
return window.setTimeout(() => {
|
||||
element.style.display = "";
|
||||
}, TIMEOUT);
|
||||
}
|
||||
|
||||
export default function createRowElement(config) {
|
||||
let tag;
|
||||
|
||||
if (
|
||||
!config ||
|
||||
typeof config !== "object" ||
|
||||
(!config.entity && !config.type)
|
||||
) {
|
||||
return _createErrorElement("Invalid config given.", config);
|
||||
}
|
||||
|
||||
const type = config.type || "default";
|
||||
if (SPECIAL_TYPES.has(type)) {
|
||||
return _createElement(`hui-${type}-row`, config);
|
||||
}
|
||||
|
||||
if (type.startsWith(CUSTOM_TYPE_PREFIX)) {
|
||||
tag = type.substr(CUSTOM_TYPE_PREFIX.length);
|
||||
|
||||
if (customElements.get(tag)) {
|
||||
return _createElement(tag, config);
|
||||
}
|
||||
const element = _createErrorElement(
|
||||
`Custom element doesn't exist: ${tag}.`,
|
||||
config
|
||||
);
|
||||
const timer = _hideErrorElement(element);
|
||||
|
||||
customElements.whenDefined(tag).then(() => {
|
||||
clearTimeout(timer);
|
||||
fireEvent(element, "rebuild-view");
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
const domain = config.entity.split(".", 1)[0];
|
||||
tag = `hui-${DOMAIN_TO_ELEMENT_TYPE[domain] || "text"}-entity-row`;
|
||||
|
||||
return _createElement(tag, config);
|
||||
}
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
import "../entity-rows/hui-climate-entity-row";
|
||||
import "../entity-rows/hui-cover-entity-row";
|
||||
import "../entity-rows/hui-group-entity-row";
|
||||
import "../entity-rows/hui-input-number-entity-row";
|
||||
import "../entity-rows/hui-input-select-entity-row";
|
||||
import "../entity-rows/hui-input-text-entity-row";
|
||||
import "../entity-rows/hui-lock-entity-row";
|
||||
import "../entity-rows/hui-media-player-entity-row";
|
||||
import "../entity-rows/hui-scene-entity-row";
|
||||
import "../entity-rows/hui-script-entity-row";
|
||||
import "../entity-rows/hui-text-entity-row";
|
||||
import "../entity-rows/hui-timer-entity-row";
|
||||
import "../entity-rows/hui-toggle-entity-row";
|
||||
|
||||
import "../special-rows/hui-call-service-row";
|
||||
import "../special-rows/hui-divider-row";
|
||||
import "../special-rows/hui-section-row";
|
||||
import "../special-rows/hui-weblink-row";
|
||||
|
||||
import createErrorCardConfig from "./create-error-card-config";
|
||||
|
||||
const CUSTOM_TYPE_PREFIX = "custom:";
|
||||
const SPECIAL_TYPES = new Set([
|
||||
"call-service",
|
||||
"divider",
|
||||
"section",
|
||||
"weblink",
|
||||
]);
|
||||
const DOMAIN_TO_ELEMENT_TYPE = {
|
||||
automation: "toggle",
|
||||
climate: "climate",
|
||||
cover: "cover",
|
||||
fan: "toggle",
|
||||
group: "group",
|
||||
input_boolean: "toggle",
|
||||
input_number: "input-number",
|
||||
input_select: "input-select",
|
||||
input_text: "input-text",
|
||||
light: "toggle",
|
||||
media_player: "media-player",
|
||||
lock: "lock",
|
||||
scene: "scene",
|
||||
script: "script",
|
||||
timer: "timer",
|
||||
switch: "toggle",
|
||||
vacuum: "toggle",
|
||||
};
|
||||
const TIMEOUT = 2000;
|
||||
|
||||
function _createElement(tag, config) {
|
||||
const element = document.createElement(tag);
|
||||
try {
|
||||
if ("setConfig" in element) element.setConfig(config);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error(tag, err);
|
||||
// eslint-disable-next-line
|
||||
return _createErrorElement(err.message, config);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function _createErrorElement(error, config) {
|
||||
return _createElement("hui-error-card", createErrorCardConfig(error, config));
|
||||
}
|
||||
|
||||
function _hideErrorElement(element) {
|
||||
element.style.display = "None";
|
||||
return window.setTimeout(() => {
|
||||
element.style.display = "";
|
||||
}, TIMEOUT);
|
||||
}
|
||||
|
||||
export default function createRowElement(config) {
|
||||
let tag;
|
||||
|
||||
if (
|
||||
!config ||
|
||||
typeof config !== "object" ||
|
||||
(!config.entity && !config.type)
|
||||
) {
|
||||
return _createErrorElement("Invalid config given.", config);
|
||||
}
|
||||
|
||||
const type = config.type || "default";
|
||||
if (SPECIAL_TYPES.has(type)) {
|
||||
return _createElement(`hui-${type}-row`, config);
|
||||
}
|
||||
|
||||
if (type.startsWith(CUSTOM_TYPE_PREFIX)) {
|
||||
tag = type.substr(CUSTOM_TYPE_PREFIX.length);
|
||||
|
||||
if (customElements.get(tag)) {
|
||||
return _createElement(tag, config);
|
||||
}
|
||||
const element = _createErrorElement(
|
||||
`Custom element doesn't exist: ${tag}.`,
|
||||
config
|
||||
);
|
||||
const timer = _hideErrorElement(element);
|
||||
|
||||
customElements.whenDefined(tag).then(() => {
|
||||
clearTimeout(timer);
|
||||
fireEvent(element, "rebuild-view");
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
const domain = config.entity.split(".", 1)[0];
|
||||
tag = `hui-${DOMAIN_TO_ELEMENT_TYPE[domain] || "text"}-entity-row`;
|
||||
|
||||
return _createElement(tag, config);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { STATES_OFF } from "../../../../common/const";
|
||||
import turnOnOffEntity from "./turn-on-off-entity";
|
||||
|
||||
export default function toggleEntity(hass, entityId) {
|
||||
const turnOn = STATES_OFF.includes(hass.states[entityId].state);
|
||||
turnOnOffEntity(hass, entityId, turnOn);
|
||||
}
|
||||
import { STATES_OFF } from "../../../../common/const";
|
||||
import turnOnOffEntity from "./turn-on-off-entity";
|
||||
|
||||
export default function toggleEntity(hass, entityId) {
|
||||
const turnOn = STATES_OFF.includes(hass.states[entityId].state);
|
||||
turnOnOffEntity(hass, entityId, turnOn);
|
||||
}
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
import { STATES_OFF } from "../../../../common/const";
|
||||
import computeDomain from "../../../../common/entity/compute_domain";
|
||||
|
||||
export default function turnOnOffEntities(hass, entityIds, turnOn = true) {
|
||||
const domainsToCall = {};
|
||||
entityIds.forEach((entityId) => {
|
||||
if (STATES_OFF.includes(hass.states[entityId].state) === turnOn) {
|
||||
const stateDomain = computeDomain(entityId);
|
||||
const serviceDomain = ["cover", "lock"].includes(stateDomain)
|
||||
? stateDomain
|
||||
: "homeassistant";
|
||||
|
||||
if (!(serviceDomain in domainsToCall)) domainsToCall[serviceDomain] = [];
|
||||
domainsToCall[serviceDomain].push(entityId);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(domainsToCall).forEach((domain) => {
|
||||
let service;
|
||||
switch (domain) {
|
||||
case "lock":
|
||||
service = turnOn ? "unlock" : "lock";
|
||||
break;
|
||||
case "cover":
|
||||
service = turnOn ? "open_cover" : "close_cover";
|
||||
break;
|
||||
default:
|
||||
service = turnOn ? "turn_on" : "turn_off";
|
||||
}
|
||||
|
||||
const entities = domainsToCall[domain];
|
||||
hass.callService(domain, service, { entity_id: entities });
|
||||
});
|
||||
}
|
||||
import { STATES_OFF } from "../../../../common/const";
|
||||
import computeDomain from "../../../../common/entity/compute_domain";
|
||||
|
||||
export default function turnOnOffEntities(hass, entityIds, turnOn = true) {
|
||||
const domainsToCall = {};
|
||||
entityIds.forEach((entityId) => {
|
||||
if (STATES_OFF.includes(hass.states[entityId].state) === turnOn) {
|
||||
const stateDomain = computeDomain(entityId);
|
||||
const serviceDomain = ["cover", "lock"].includes(stateDomain)
|
||||
? stateDomain
|
||||
: "homeassistant";
|
||||
|
||||
if (!(serviceDomain in domainsToCall)) domainsToCall[serviceDomain] = [];
|
||||
domainsToCall[serviceDomain].push(entityId);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(domainsToCall).forEach((domain) => {
|
||||
let service;
|
||||
switch (domain) {
|
||||
case "lock":
|
||||
service = turnOn ? "unlock" : "lock";
|
||||
break;
|
||||
case "cover":
|
||||
service = turnOn ? "open_cover" : "close_cover";
|
||||
break;
|
||||
default:
|
||||
service = turnOn ? "turn_on" : "turn_off";
|
||||
}
|
||||
|
||||
const entities = domainsToCall[domain];
|
||||
hass.callService(domain, service, { entity_id: entities });
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import computeDomain from "../../../../common/entity/compute_domain";
|
||||
|
||||
export default function turnOnOffEntity(hass, entityId, turnOn = true) {
|
||||
const stateDomain = computeDomain(entityId);
|
||||
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;
|
||||
|
||||
let service;
|
||||
switch (stateDomain) {
|
||||
case "lock":
|
||||
service = turnOn ? "unlock" : "lock";
|
||||
break;
|
||||
case "cover":
|
||||
service = turnOn ? "open_cover" : "close_cover";
|
||||
break;
|
||||
default:
|
||||
service = turnOn ? "turn_on" : "turn_off";
|
||||
}
|
||||
|
||||
hass.callService(serviceDomain, service, { entity_id: entityId });
|
||||
}
|
||||
import computeDomain from "../../../../common/entity/compute_domain";
|
||||
|
||||
export default function turnOnOffEntity(hass, entityId, turnOn = true) {
|
||||
const stateDomain = computeDomain(entityId);
|
||||
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;
|
||||
|
||||
let service;
|
||||
switch (stateDomain) {
|
||||
case "lock":
|
||||
service = turnOn ? "unlock" : "lock";
|
||||
break;
|
||||
case "cover":
|
||||
service = turnOn ? "open_cover" : "close_cover";
|
||||
break;
|
||||
default:
|
||||
service = turnOn ? "turn_on" : "turn_off";
|
||||
}
|
||||
|
||||
hass.callService(serviceDomain, service, { entity_id: entityId });
|
||||
}
|
||||
|
|
|
@ -1,44 +1,44 @@
|
|||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceElementConfig } from "../elements/types";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import toggleEntity from "../../../../src/panels/lovelace/common/entity/toggle-entity";
|
||||
|
||||
export const handleClick = (
|
||||
node: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
config: LovelaceElementConfig,
|
||||
hold: boolean
|
||||
): void => {
|
||||
let action = config.tap_action || "more-info";
|
||||
|
||||
if (hold && config.hold_action) {
|
||||
action = config.hold_action;
|
||||
}
|
||||
|
||||
if (action === "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "more-info":
|
||||
fireEvent(node, "hass-more-info", { entityId: config.entity });
|
||||
break;
|
||||
case "navigate":
|
||||
navigate(node, config.navigation_path ? config.navigation_path : "");
|
||||
break;
|
||||
case "toggle":
|
||||
toggleEntity(hass, config.entity);
|
||||
break;
|
||||
case "call-service": {
|
||||
if (config.service) {
|
||||
const [domain, service] = config.service.split(".", 2);
|
||||
const serviceData = {
|
||||
entity_id: config.entity,
|
||||
...config.service_data,
|
||||
};
|
||||
hass.callService(domain, service, serviceData);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceElementConfig } from "../elements/types";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import toggleEntity from "../../../../src/panels/lovelace/common/entity/toggle-entity";
|
||||
|
||||
export const handleClick = (
|
||||
node: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
config: LovelaceElementConfig,
|
||||
hold: boolean
|
||||
): void => {
|
||||
let action = config.tap_action || "more-info";
|
||||
|
||||
if (hold && config.hold_action) {
|
||||
action = config.hold_action;
|
||||
}
|
||||
|
||||
if (action === "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "more-info":
|
||||
fireEvent(node, "hass-more-info", { entityId: config.entity });
|
||||
break;
|
||||
case "navigate":
|
||||
navigate(node, config.navigation_path ? config.navigation_path : "");
|
||||
break;
|
||||
case "toggle":
|
||||
toggleEntity(hass, config.entity);
|
||||
break;
|
||||
case "call-service": {
|
||||
if (config.service) {
|
||||
const [domain, service] = config.service.split(".", 2);
|
||||
const serviceData = {
|
||||
entity_id: config.entity,
|
||||
...config.service_data,
|
||||
};
|
||||
hass.callService(domain, service, serviceData);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,38 +1,38 @@
|
|||
// Parse array of entity objects from config
|
||||
import isValidEntityId from "../../../common/entity/valid_entity_id";
|
||||
|
||||
export default function processConfigEntities(entities) {
|
||||
if (!entities || !Array.isArray(entities)) {
|
||||
throw new Error("Entities need to be an array");
|
||||
}
|
||||
|
||||
return entities.map((entityConf, index) => {
|
||||
if (
|
||||
typeof entityConf === "object" &&
|
||||
!Array.isArray(entityConf) &&
|
||||
entityConf.type
|
||||
) {
|
||||
return entityConf;
|
||||
}
|
||||
|
||||
if (typeof entityConf === "string") {
|
||||
entityConf = { entity: entityConf };
|
||||
} else if (typeof entityConf === "object" && !Array.isArray(entityConf)) {
|
||||
if (!entityConf.entity) {
|
||||
throw new Error(
|
||||
`Entity object at position ${index} is missing entity field.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Invalid entity specified at position ${index}.`);
|
||||
}
|
||||
|
||||
if (!isValidEntityId(entityConf.entity)) {
|
||||
throw new Error(
|
||||
`Invalid entity ID at position ${index}: ${entityConf.entity}`
|
||||
);
|
||||
}
|
||||
|
||||
return entityConf;
|
||||
});
|
||||
}
|
||||
// Parse array of entity objects from config
|
||||
import isValidEntityId from "../../../common/entity/valid_entity_id";
|
||||
|
||||
export default function processConfigEntities(entities) {
|
||||
if (!entities || !Array.isArray(entities)) {
|
||||
throw new Error("Entities need to be an array");
|
||||
}
|
||||
|
||||
return entities.map((entityConf, index) => {
|
||||
if (
|
||||
typeof entityConf === "object" &&
|
||||
!Array.isArray(entityConf) &&
|
||||
entityConf.type
|
||||
) {
|
||||
return entityConf;
|
||||
}
|
||||
|
||||
if (typeof entityConf === "string") {
|
||||
entityConf = { entity: entityConf };
|
||||
} else if (typeof entityConf === "object" && !Array.isArray(entityConf)) {
|
||||
if (!entityConf.entity) {
|
||||
throw new Error(
|
||||
`Entity object at position ${index} is missing entity field.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Invalid entity specified at position ${index}.`);
|
||||
}
|
||||
|
||||
if (!isValidEntityId(entityConf.entity)) {
|
||||
throw new Error(
|
||||
`Invalid entity ID at position ${index}: ${entityConf.entity}`
|
||||
);
|
||||
}
|
||||
|
||||
return entityConf;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,68 +1,68 @@
|
|||
import "@polymer/paper-button/paper-button";
|
||||
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
let registeredDialog = false;
|
||||
|
||||
export class HuiCardOptions extends LitElement {
|
||||
public cardId?: string;
|
||||
protected hass?: HomeAssistant;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
};
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!registeredDialog) {
|
||||
registeredDialog = true;
|
||||
fireEvent(this, "register-dialog", {
|
||||
dialogShowEvent: "show-edit-card",
|
||||
dialogTag: "hui-dialog-edit-card",
|
||||
dialogImport: () => import("../editor/hui-dialog-edit-card"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<style>
|
||||
div {
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: 5px 16px;
|
||||
background: var(--paper-card-background-color, white);
|
||||
box-shadow: rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 1px 5px 0px, rgba(0, 0, 0, 0.2) 0px 3px 1px -2px;
|
||||
text-align: right;
|
||||
}
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
<div>
|
||||
<paper-button
|
||||
@click="${this._editCard}"
|
||||
>EDIT</paper-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
private _editCard() {
|
||||
fireEvent(this, "show-edit-card", {
|
||||
hass: this.hass,
|
||||
cardId: this.cardId,
|
||||
reloadLovelace: () => fireEvent(this, "config-refresh"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-card-options": HuiCardOptions;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-card-options", HuiCardOptions);
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
let registeredDialog = false;
|
||||
|
||||
export class HuiCardOptions extends LitElement {
|
||||
public cardId?: string;
|
||||
protected hass?: HomeAssistant;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
};
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!registeredDialog) {
|
||||
registeredDialog = true;
|
||||
fireEvent(this, "register-dialog", {
|
||||
dialogShowEvent: "show-edit-card",
|
||||
dialogTag: "hui-dialog-edit-card",
|
||||
dialogImport: () => import("../editor/hui-dialog-edit-card"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<style>
|
||||
div {
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: 5px 16px;
|
||||
background: var(--paper-card-background-color, white);
|
||||
box-shadow: rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 1px 5px 0px, rgba(0, 0, 0, 0.2) 0px 3px 1px -2px;
|
||||
text-align: right;
|
||||
}
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
<div>
|
||||
<paper-button
|
||||
@click="${this._editCard}"
|
||||
>EDIT</paper-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
private _editCard() {
|
||||
fireEvent(this, "show-edit-card", {
|
||||
hass: this.hass,
|
||||
cardId: this.cardId,
|
||||
reloadLovelace: () => fireEvent(this, "config-refresh"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-card-options": HuiCardOptions;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-card-options", HuiCardOptions);
|
||||
|
|
|
@ -1,57 +1,57 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-toggle-button/paper-toggle-button";
|
||||
|
||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||
import turnOnOffEntities from "../common/entity/turn-on-off-entities";
|
||||
|
||||
class HuiEntitiesToggle extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
width: 38px;
|
||||
display: block;
|
||||
}
|
||||
paper-toggle-button {
|
||||
cursor: pointer;
|
||||
--paper-toggle-button-label-spacing: 0;
|
||||
padding: 13px 5px;
|
||||
margin: -4px -5px;
|
||||
}
|
||||
</style>
|
||||
<template is="dom-if" if="[[_toggleEntities.length]]">
|
||||
<paper-toggle-button checked="[[_computeIsChecked(hass, _toggleEntities)]]" on-change="_callService"></paper-toggle-button>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
entities: Array,
|
||||
_toggleEntities: {
|
||||
type: Array,
|
||||
computed: "_computeToggleEntities(hass, entities)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeToggleEntities(hass, entityIds) {
|
||||
return entityIds.filter(
|
||||
(entityId) =>
|
||||
entityId in hass.states && DOMAINS_TOGGLE.has(entityId.split(".", 1)[0])
|
||||
);
|
||||
}
|
||||
|
||||
_computeIsChecked(hass, entityIds) {
|
||||
return entityIds.some((entityId) => hass.states[entityId].state === "on");
|
||||
}
|
||||
|
||||
_callService(ev) {
|
||||
const turnOn = ev.target.checked;
|
||||
turnOnOffEntities(this.hass, this._toggleEntities, turnOn);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-entities-toggle", HuiEntitiesToggle);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-toggle-button/paper-toggle-button";
|
||||
|
||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||
import turnOnOffEntities from "../common/entity/turn-on-off-entities";
|
||||
|
||||
class HuiEntitiesToggle extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
width: 38px;
|
||||
display: block;
|
||||
}
|
||||
paper-toggle-button {
|
||||
cursor: pointer;
|
||||
--paper-toggle-button-label-spacing: 0;
|
||||
padding: 13px 5px;
|
||||
margin: -4px -5px;
|
||||
}
|
||||
</style>
|
||||
<template is="dom-if" if="[[_toggleEntities.length]]">
|
||||
<paper-toggle-button checked="[[_computeIsChecked(hass, _toggleEntities)]]" on-change="_callService"></paper-toggle-button>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
entities: Array,
|
||||
_toggleEntities: {
|
||||
type: Array,
|
||||
computed: "_computeToggleEntities(hass, entities)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeToggleEntities(hass, entityIds) {
|
||||
return entityIds.filter(
|
||||
(entityId) =>
|
||||
entityId in hass.states && DOMAINS_TOGGLE.has(entityId.split(".", 1)[0])
|
||||
);
|
||||
}
|
||||
|
||||
_computeIsChecked(hass, entityIds) {
|
||||
return entityIds.some((entityId) => hass.states[entityId].state === "on");
|
||||
}
|
||||
|
||||
_callService(ev) {
|
||||
const turnOn = ev.target.checked;
|
||||
turnOnOffEntities(this.hass, this._toggleEntities, turnOn);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-entities-toggle", HuiEntitiesToggle);
|
||||
|
|
|
@ -1,137 +1,137 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-relative-time";
|
||||
import "../../../components/ha-icon";
|
||||
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
|
||||
class HuiGenericEntityRow extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<template is="dom-if" if="[[_stateObj]]">
|
||||
${this.stateBadgeTemplate}
|
||||
<div class="flex">
|
||||
${this.infoTemplate}
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_stateObj]]">
|
||||
<div class="not-found">
|
||||
Entity not available: [[config.entity]]
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.flex {
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.info {
|
||||
flex: 1 0 60px;
|
||||
}
|
||||
.info,
|
||||
.info > * {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.flex ::slotted(*) {
|
||||
margin-left: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.flex ::slotted([slot=secondary]) {
|
||||
margin-left: 0;
|
||||
}
|
||||
.secondary,
|
||||
ha-relative-time {
|
||||
display: block;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.not-found {
|
||||
flex: 1;
|
||||
background-color: yellow;
|
||||
padding: 8px;
|
||||
}
|
||||
state-badge {
|
||||
flex: 0 0 40px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get stateBadgeTemplate() {
|
||||
return html`
|
||||
<state-badge
|
||||
state-obj="[[_stateObj]]"
|
||||
override-icon="[[config.icon]]"
|
||||
></state-badge>
|
||||
`;
|
||||
}
|
||||
|
||||
static get infoTemplate() {
|
||||
return html`
|
||||
<div class="info">
|
||||
[[_computeName(config.name, _stateObj)]]
|
||||
<div class="secondary">
|
||||
<template is="dom-if" if="[[showSecondary]]">
|
||||
<template is="dom-if" if="[[_equals(config.secondary_info, 'entity-id')]]">
|
||||
[[_stateObj.entity_id]]
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(config.secondary_info, 'last-changed')]]">
|
||||
<ha-relative-time
|
||||
hass="[[hass]]"
|
||||
datetime="[[_stateObj.last_changed]]"
|
||||
></ha-relative-time>
|
||||
</template>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!showSecondary]">
|
||||
<slot name="secondary"></slot>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, config.entity)",
|
||||
},
|
||||
showSecondary: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_equals(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
_computeName(name, stateObj) {
|
||||
return name || computeStateName(stateObj);
|
||||
}
|
||||
}
|
||||
customElements.define("hui-generic-entity-row", HuiGenericEntityRow);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-relative-time";
|
||||
import "../../../components/ha-icon";
|
||||
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
|
||||
class HuiGenericEntityRow extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<template is="dom-if" if="[[_stateObj]]">
|
||||
${this.stateBadgeTemplate}
|
||||
<div class="flex">
|
||||
${this.infoTemplate}
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_stateObj]]">
|
||||
<div class="not-found">
|
||||
Entity not available: [[config.entity]]
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.flex {
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.info {
|
||||
flex: 1 0 60px;
|
||||
}
|
||||
.info,
|
||||
.info > * {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.flex ::slotted(*) {
|
||||
margin-left: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.flex ::slotted([slot=secondary]) {
|
||||
margin-left: 0;
|
||||
}
|
||||
.secondary,
|
||||
ha-relative-time {
|
||||
display: block;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.not-found {
|
||||
flex: 1;
|
||||
background-color: yellow;
|
||||
padding: 8px;
|
||||
}
|
||||
state-badge {
|
||||
flex: 0 0 40px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get stateBadgeTemplate() {
|
||||
return html`
|
||||
<state-badge
|
||||
state-obj="[[_stateObj]]"
|
||||
override-icon="[[config.icon]]"
|
||||
></state-badge>
|
||||
`;
|
||||
}
|
||||
|
||||
static get infoTemplate() {
|
||||
return html`
|
||||
<div class="info">
|
||||
[[_computeName(config.name, _stateObj)]]
|
||||
<div class="secondary">
|
||||
<template is="dom-if" if="[[showSecondary]]">
|
||||
<template is="dom-if" if="[[_equals(config.secondary_info, 'entity-id')]]">
|
||||
[[_stateObj.entity_id]]
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(config.secondary_info, 'last-changed')]]">
|
||||
<ha-relative-time
|
||||
hass="[[hass]]"
|
||||
datetime="[[_stateObj.last_changed]]"
|
||||
></ha-relative-time>
|
||||
</template>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!showSecondary]">
|
||||
<slot name="secondary"></slot>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, config.entity)",
|
||||
},
|
||||
showSecondary: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_equals(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
_computeName(name, stateObj) {
|
||||
return name || computeStateName(stateObj);
|
||||
}
|
||||
}
|
||||
customElements.define("hui-generic-entity-row", HuiGenericEntityRow);
|
||||
|
|
|
@ -1,198 +1,198 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-toggle-button/paper-toggle-button";
|
||||
|
||||
import { STATES_OFF } from "../../../common/const";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||
|
||||
const UPDATE_INTERVAL = 10000;
|
||||
const DEFAULT_FILTER = "grayscale(100%)";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiImage extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<div id="wrapper">
|
||||
<img
|
||||
id="image"
|
||||
src="[[_imageSrc]]"
|
||||
on-error="_onImageError"
|
||||
on-load="_onImageLoad" />
|
||||
<div id="brokenImage"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
|
||||
img {
|
||||
display: block;
|
||||
height: auto;
|
||||
transition: filter .2s linear;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ratio {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0
|
||||
}
|
||||
|
||||
.ratio img, .ratio div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#brokenImage {
|
||||
background: grey url('/static/images/image-broken.svg') center/36px no-repeat;
|
||||
}
|
||||
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_hassChanged",
|
||||
},
|
||||
entity: String,
|
||||
image: String,
|
||||
stateImage: Object,
|
||||
cameraImage: String,
|
||||
aspectRatio: String,
|
||||
filter: String,
|
||||
stateFilter: Object,
|
||||
_imageSrc: String,
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return ["_configChanged(image, stateImage, cameraImage, aspectRatio)"];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.cameraImage) {
|
||||
this.timer = setInterval(
|
||||
() => this._updateCameraImageSrc(),
|
||||
UPDATE_INTERVAL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
_configChanged(image, stateImage, cameraImage, aspectRatio) {
|
||||
const ratio = parseAspectRatio(aspectRatio);
|
||||
|
||||
if (ratio && ratio.w > 0 && ratio.h > 0) {
|
||||
this.$.wrapper.style.paddingBottom = `${(
|
||||
(100 * ratio.h) /
|
||||
ratio.w
|
||||
).toFixed(2)}%`;
|
||||
this.$.wrapper.classList.add("ratio");
|
||||
}
|
||||
|
||||
if (cameraImage) {
|
||||
this._updateCameraImageSrc();
|
||||
} else if (image && !stateImage) {
|
||||
this._imageSrc = image;
|
||||
}
|
||||
}
|
||||
|
||||
_onImageError() {
|
||||
this._imageSrc = null;
|
||||
this.$.image.classList.add("hidden");
|
||||
if (!this.$.wrapper.classList.contains("ratio")) {
|
||||
this.$.brokenImage.style.setProperty(
|
||||
"height",
|
||||
`${this._lastImageHeight || "100"}px`
|
||||
);
|
||||
}
|
||||
this.$.brokenImage.classList.remove("hidden");
|
||||
}
|
||||
|
||||
_onImageLoad() {
|
||||
this.$.image.classList.remove("hidden");
|
||||
this.$.brokenImage.classList.add("hidden");
|
||||
if (!this.$.wrapper.classList.contains("ratio")) {
|
||||
this._lastImageHeight = this.$.image.offsetHeight;
|
||||
}
|
||||
}
|
||||
|
||||
_hassChanged(hass) {
|
||||
if (this.cameraImage || !this.entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stateObj = hass.states[this.entity];
|
||||
const newState = !stateObj ? "unavailable" : stateObj.state;
|
||||
|
||||
if (newState === this._currentState) return;
|
||||
this._currentState = newState;
|
||||
|
||||
this._updateStateImage();
|
||||
this._updateStateFilter(stateObj);
|
||||
}
|
||||
|
||||
_updateStateImage() {
|
||||
if (!this.stateImage) {
|
||||
this._imageFallback = true;
|
||||
return;
|
||||
}
|
||||
const stateImg = this.stateImage[this._currentState];
|
||||
this._imageSrc = stateImg || this.image;
|
||||
this._imageFallback = !stateImg;
|
||||
}
|
||||
|
||||
_updateStateFilter(stateObj) {
|
||||
let filter;
|
||||
if (!this.stateFilter) {
|
||||
filter = this.filter;
|
||||
} else {
|
||||
filter = this.stateFilter[this._currentState] || this.filter;
|
||||
}
|
||||
|
||||
const isOff = !stateObj || STATES_OFF.includes(stateObj.state);
|
||||
this.$.image.style.filter =
|
||||
filter || (isOff && this._imageFallback && DEFAULT_FILTER) || "";
|
||||
}
|
||||
|
||||
async _updateCameraImageSrc() {
|
||||
try {
|
||||
const { content_type: contentType, content } = await this.hass.callWS({
|
||||
type: "camera_thumbnail",
|
||||
entity_id: this.cameraImage,
|
||||
});
|
||||
this._imageSrc = `data:${contentType};base64, ${content}`;
|
||||
this._onImageLoad();
|
||||
} catch (err) {
|
||||
this._onImageError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-image", HuiImage);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-toggle-button/paper-toggle-button";
|
||||
|
||||
import { STATES_OFF } from "../../../common/const";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||
|
||||
const UPDATE_INTERVAL = 10000;
|
||||
const DEFAULT_FILTER = "grayscale(100%)";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiImage extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<div id="wrapper">
|
||||
<img
|
||||
id="image"
|
||||
src="[[_imageSrc]]"
|
||||
on-error="_onImageError"
|
||||
on-load="_onImageLoad" />
|
||||
<div id="brokenImage"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
|
||||
img {
|
||||
display: block;
|
||||
height: auto;
|
||||
transition: filter .2s linear;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ratio {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0
|
||||
}
|
||||
|
||||
.ratio img, .ratio div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#brokenImage {
|
||||
background: grey url('/static/images/image-broken.svg') center/36px no-repeat;
|
||||
}
|
||||
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_hassChanged",
|
||||
},
|
||||
entity: String,
|
||||
image: String,
|
||||
stateImage: Object,
|
||||
cameraImage: String,
|
||||
aspectRatio: String,
|
||||
filter: String,
|
||||
stateFilter: Object,
|
||||
_imageSrc: String,
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return ["_configChanged(image, stateImage, cameraImage, aspectRatio)"];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.cameraImage) {
|
||||
this.timer = setInterval(
|
||||
() => this._updateCameraImageSrc(),
|
||||
UPDATE_INTERVAL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
_configChanged(image, stateImage, cameraImage, aspectRatio) {
|
||||
const ratio = parseAspectRatio(aspectRatio);
|
||||
|
||||
if (ratio && ratio.w > 0 && ratio.h > 0) {
|
||||
this.$.wrapper.style.paddingBottom = `${(
|
||||
(100 * ratio.h) /
|
||||
ratio.w
|
||||
).toFixed(2)}%`;
|
||||
this.$.wrapper.classList.add("ratio");
|
||||
}
|
||||
|
||||
if (cameraImage) {
|
||||
this._updateCameraImageSrc();
|
||||
} else if (image && !stateImage) {
|
||||
this._imageSrc = image;
|
||||
}
|
||||
}
|
||||
|
||||
_onImageError() {
|
||||
this._imageSrc = null;
|
||||
this.$.image.classList.add("hidden");
|
||||
if (!this.$.wrapper.classList.contains("ratio")) {
|
||||
this.$.brokenImage.style.setProperty(
|
||||
"height",
|
||||
`${this._lastImageHeight || "100"}px`
|
||||
);
|
||||
}
|
||||
this.$.brokenImage.classList.remove("hidden");
|
||||
}
|
||||
|
||||
_onImageLoad() {
|
||||
this.$.image.classList.remove("hidden");
|
||||
this.$.brokenImage.classList.add("hidden");
|
||||
if (!this.$.wrapper.classList.contains("ratio")) {
|
||||
this._lastImageHeight = this.$.image.offsetHeight;
|
||||
}
|
||||
}
|
||||
|
||||
_hassChanged(hass) {
|
||||
if (this.cameraImage || !this.entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stateObj = hass.states[this.entity];
|
||||
const newState = !stateObj ? "unavailable" : stateObj.state;
|
||||
|
||||
if (newState === this._currentState) return;
|
||||
this._currentState = newState;
|
||||
|
||||
this._updateStateImage();
|
||||
this._updateStateFilter(stateObj);
|
||||
}
|
||||
|
||||
_updateStateImage() {
|
||||
if (!this.stateImage) {
|
||||
this._imageFallback = true;
|
||||
return;
|
||||
}
|
||||
const stateImg = this.stateImage[this._currentState];
|
||||
this._imageSrc = stateImg || this.image;
|
||||
this._imageFallback = !stateImg;
|
||||
}
|
||||
|
||||
_updateStateFilter(stateObj) {
|
||||
let filter;
|
||||
if (!this.stateFilter) {
|
||||
filter = this.filter;
|
||||
} else {
|
||||
filter = this.stateFilter[this._currentState] || this.filter;
|
||||
}
|
||||
|
||||
const isOff = !stateObj || STATES_OFF.includes(stateObj.state);
|
||||
this.$.image.style.filter =
|
||||
filter || (isOff && this._imageFallback && DEFAULT_FILTER) || "";
|
||||
}
|
||||
|
||||
async _updateCameraImageSrc() {
|
||||
try {
|
||||
const { content_type: contentType, content } = await this.hass.callWS({
|
||||
type: "camera_thumbnail",
|
||||
entity_id: this.cameraImage,
|
||||
});
|
||||
this._imageSrc = `data:${contentType};base64, ${content}`;
|
||||
this._onImageLoad();
|
||||
} catch (err) {
|
||||
this._onImageError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-image", HuiImage);
|
||||
|
|
|
@ -1,62 +1,62 @@
|
|||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "./hui-notification-item-template";
|
||||
|
||||
import EventsMixin from "../../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
export class HuiConfiguratorNotificationItem extends EventsMixin(
|
||||
LocalizeMixin(PolymerElement)
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
<hui-notification-item-template>
|
||||
<span slot="header">[[localize('domain.configurator')]]</span>
|
||||
|
||||
<div>[[_getMessage(notification)]]</div>
|
||||
|
||||
<paper-button
|
||||
slot="actions"
|
||||
class="primary"
|
||||
on-click="_handleClick"
|
||||
>[[_localizeState(notification.state)]]</paper-button>
|
||||
</hui-notification-item-template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
notification: Object,
|
||||
};
|
||||
}
|
||||
|
||||
_handleClick() {
|
||||
this.fire("hass-more-info", { entityId: this.notification.entity_id });
|
||||
}
|
||||
|
||||
_localizeState(state) {
|
||||
return this.localize(`state.configurator.${state}`);
|
||||
}
|
||||
|
||||
_getMessage(notification) {
|
||||
const friendlyName = notification.attributes.friendly_name;
|
||||
return this.localize(
|
||||
"ui.notification_drawer.click_to_configure",
|
||||
"entity",
|
||||
friendlyName
|
||||
);
|
||||
}
|
||||
}
|
||||
customElements.define(
|
||||
"hui-configurator-notification-item",
|
||||
HuiConfiguratorNotificationItem
|
||||
);
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "./hui-notification-item-template";
|
||||
|
||||
import EventsMixin from "../../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
export class HuiConfiguratorNotificationItem extends EventsMixin(
|
||||
LocalizeMixin(PolymerElement)
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
<hui-notification-item-template>
|
||||
<span slot="header">[[localize('domain.configurator')]]</span>
|
||||
|
||||
<div>[[_getMessage(notification)]]</div>
|
||||
|
||||
<paper-button
|
||||
slot="actions"
|
||||
class="primary"
|
||||
on-click="_handleClick"
|
||||
>[[_localizeState(notification.state)]]</paper-button>
|
||||
</hui-notification-item-template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
notification: Object,
|
||||
};
|
||||
}
|
||||
|
||||
_handleClick() {
|
||||
this.fire("hass-more-info", { entityId: this.notification.entity_id });
|
||||
}
|
||||
|
||||
_localizeState(state) {
|
||||
return this.localize(`state.configurator.${state}`);
|
||||
}
|
||||
|
||||
_getMessage(notification) {
|
||||
const friendlyName = notification.attributes.friendly_name;
|
||||
return this.localize(
|
||||
"ui.notification_drawer.click_to_configure",
|
||||
"entity",
|
||||
friendlyName
|
||||
);
|
||||
}
|
||||
}
|
||||
customElements.define(
|
||||
"hui-configurator-notification-item",
|
||||
HuiConfiguratorNotificationItem
|
||||
);
|
||||
|
|
|
@ -1,175 +1,175 @@
|
|||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "./hui-notification-item";
|
||||
|
||||
import EventsMixin from "../../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
export class HuiNotificationDrawer extends EventsMixin(
|
||||
LocalizeMixin(PolymerElement)
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="paper-material-styles">
|
||||
:host {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
:host([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
align-items: stretch;
|
||||
background: var(--sidebar-background-color, var(--primary-background-color));
|
||||
bottom: 0;
|
||||
box-shadow: var(--paper-material-elevation-1_-_box-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
transition: right .2s ease-in;
|
||||
width: 500px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
:host(:not(narrow)) .container {
|
||||
right: -500px;
|
||||
}
|
||||
|
||||
:host([narrow]) .container {
|
||||
right: -100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host(.open) .container,
|
||||
:host(.open[narrow]) .container {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
app-toolbar {
|
||||
color: var(--primary-text-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
background-color: var(--primary-background-color);
|
||||
min-height: 64px;
|
||||
width: calc(100% - 32px);
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host(.open) .overlay {
|
||||
bottom: 0;
|
||||
display: block;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
overflow-y: auto;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.notification {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<div class="overlay" on-click="_closeDrawer"></div>
|
||||
<div class="container">
|
||||
<app-toolbar>
|
||||
<div main-title>[[localize('ui.notification_drawer.title')]]</div>
|
||||
<paper-icon-button icon="hass:chevron-right" on-click="_closeDrawer"></paper-icon-button>
|
||||
</app-toolbar>
|
||||
<div class="notifications">
|
||||
<template is="dom-if" if="[[!_empty(notifications)]]">
|
||||
<dom-repeat items="[[notifications]]">
|
||||
<template>
|
||||
<div class="notification">
|
||||
<hui-notification-item hass="[[hass]]" notification="[[item]]"></hui-notification-item>
|
||||
</div>
|
||||
</template>
|
||||
</dom-repeat>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_empty(notifications)]]">
|
||||
<div class="empty">[[localize('ui.notification_drawer.empty')]]<div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
notify: true,
|
||||
observer: "_openChanged",
|
||||
},
|
||||
hidden: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
notifications: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_closeDrawer(ev) {
|
||||
ev.stopPropagation();
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
_empty(notifications) {
|
||||
return notifications.length === 0;
|
||||
}
|
||||
|
||||
_openChanged(open) {
|
||||
clearTimeout(this._openTimer);
|
||||
if (open) {
|
||||
// Render closed then animate open
|
||||
this.hidden = false;
|
||||
this._openTimer = setTimeout(() => {
|
||||
this.classList.add("open");
|
||||
}, 50);
|
||||
} else {
|
||||
// Animate closed then hide
|
||||
this.classList.remove("open");
|
||||
this._openTimer = setTimeout(() => {
|
||||
this.hidden = true;
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("hui-notification-drawer", HuiNotificationDrawer);
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "./hui-notification-item";
|
||||
|
||||
import EventsMixin from "../../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
export class HuiNotificationDrawer extends EventsMixin(
|
||||
LocalizeMixin(PolymerElement)
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="paper-material-styles">
|
||||
:host {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
:host([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
align-items: stretch;
|
||||
background: var(--sidebar-background-color, var(--primary-background-color));
|
||||
bottom: 0;
|
||||
box-shadow: var(--paper-material-elevation-1_-_box-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
transition: right .2s ease-in;
|
||||
width: 500px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
:host(:not(narrow)) .container {
|
||||
right: -500px;
|
||||
}
|
||||
|
||||
:host([narrow]) .container {
|
||||
right: -100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host(.open) .container,
|
||||
:host(.open[narrow]) .container {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
app-toolbar {
|
||||
color: var(--primary-text-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
background-color: var(--primary-background-color);
|
||||
min-height: 64px;
|
||||
width: calc(100% - 32px);
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host(.open) .overlay {
|
||||
bottom: 0;
|
||||
display: block;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
overflow-y: auto;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.notification {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<div class="overlay" on-click="_closeDrawer"></div>
|
||||
<div class="container">
|
||||
<app-toolbar>
|
||||
<div main-title>[[localize('ui.notification_drawer.title')]]</div>
|
||||
<paper-icon-button icon="hass:chevron-right" on-click="_closeDrawer"></paper-icon-button>
|
||||
</app-toolbar>
|
||||
<div class="notifications">
|
||||
<template is="dom-if" if="[[!_empty(notifications)]]">
|
||||
<dom-repeat items="[[notifications]]">
|
||||
<template>
|
||||
<div class="notification">
|
||||
<hui-notification-item hass="[[hass]]" notification="[[item]]"></hui-notification-item>
|
||||
</div>
|
||||
</template>
|
||||
</dom-repeat>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_empty(notifications)]]">
|
||||
<div class="empty">[[localize('ui.notification_drawer.empty')]]<div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
notify: true,
|
||||
observer: "_openChanged",
|
||||
},
|
||||
hidden: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
notifications: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_closeDrawer(ev) {
|
||||
ev.stopPropagation();
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
_empty(notifications) {
|
||||
return notifications.length === 0;
|
||||
}
|
||||
|
||||
_openChanged(open) {
|
||||
clearTimeout(this._openTimer);
|
||||
if (open) {
|
||||
// Render closed then animate open
|
||||
this.hidden = false;
|
||||
this._openTimer = setTimeout(() => {
|
||||
this.classList.add("open");
|
||||
}, 50);
|
||||
} else {
|
||||
// Animate closed then hide
|
||||
this.classList.remove("open");
|
||||
this._openTimer = setTimeout(() => {
|
||||
this.hidden = true;
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("hui-notification-drawer", HuiNotificationDrawer);
|
||||
|
|
|
@ -1,49 +1,49 @@
|
|||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../../components/ha-card";
|
||||
|
||||
export class HuiNotificationItemTemplate extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
.contents {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
ha-card .header {
|
||||
@apply --paper-font-headline;
|
||||
color: var(--primary-text-color);
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: 5px 16px;
|
||||
}
|
||||
|
||||
::slotted(.primary) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
<ha-card>
|
||||
<div class="header">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div class="contents">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define(
|
||||
"hui-notification-item-template",
|
||||
HuiNotificationItemTemplate
|
||||
);
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../../components/ha-card";
|
||||
|
||||
export class HuiNotificationItemTemplate extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
.contents {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
ha-card .header {
|
||||
@apply --paper-font-headline;
|
||||
color: var(--primary-text-color);
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: 5px 16px;
|
||||
}
|
||||
|
||||
::slotted(.primary) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
<ha-card>
|
||||
<div class="header">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div class="contents">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define(
|
||||
"hui-notification-item-template",
|
||||
HuiNotificationItemTemplate
|
||||
);
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import computeDomain from "../../../../common/entity/compute_domain";
|
||||
|
||||
import "./hui-configurator-notification-item";
|
||||
import "./hui-persistent-notification-item";
|
||||
|
||||
export class HuiNotificationItem extends PolymerElement {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
notification: {
|
||||
type: Object,
|
||||
observer: "_stateChanged",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_stateChanged(notification) {
|
||||
if (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
|
||||
if (!notification) return;
|
||||
|
||||
const domain = notification.entity_id
|
||||
? computeDomain(notification.entity_id)
|
||||
: "persistent_notification";
|
||||
const tag = `hui-${domain}-notification-item`;
|
||||
const el = document.createElement(tag);
|
||||
el.hass = this.hass;
|
||||
el.notification = notification;
|
||||
this.appendChild(el);
|
||||
}
|
||||
}
|
||||
customElements.define("hui-notification-item", HuiNotificationItem);
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import computeDomain from "../../../../common/entity/compute_domain";
|
||||
|
||||
import "./hui-configurator-notification-item";
|
||||
import "./hui-persistent-notification-item";
|
||||
|
||||
export class HuiNotificationItem extends PolymerElement {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
notification: {
|
||||
type: Object,
|
||||
observer: "_stateChanged",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_stateChanged(notification) {
|
||||
if (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
|
||||
if (!notification) return;
|
||||
|
||||
const domain = notification.entity_id
|
||||
? computeDomain(notification.entity_id)
|
||||
: "persistent_notification";
|
||||
const tag = `hui-${domain}-notification-item`;
|
||||
const el = document.createElement(tag);
|
||||
el.hass = this.hass;
|
||||
el.notification = notification;
|
||||
this.appendChild(el);
|
||||
}
|
||||
}
|
||||
customElements.define("hui-notification-item", HuiNotificationItem);
|
||||
|
|
|
@ -1,62 +1,62 @@
|
|||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import EventsMixin from "../../../../mixins/events-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
export class HuiNotificationsButton extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.indicator[hidden] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<paper-icon-button icon="hass:bell" on-click="_clicked"></paper-icon-button>
|
||||
<span class="indicator" hidden$="[[!_hasNotifications(notifications)]]"></span>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
notificationsOpen: {
|
||||
type: Boolean,
|
||||
notify: true,
|
||||
},
|
||||
notifications: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_clicked() {
|
||||
this.notificationsOpen = true;
|
||||
}
|
||||
|
||||
_hasNotifications(notifications) {
|
||||
return notifications.length > 0;
|
||||
}
|
||||
}
|
||||
customElements.define("hui-notifications-button", HuiNotificationsButton);
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import EventsMixin from "../../../../mixins/events-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
export class HuiNotificationsButton extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.indicator[hidden] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<paper-icon-button icon="hass:bell" on-click="_clicked"></paper-icon-button>
|
||||
<span class="indicator" hidden$="[[!_hasNotifications(notifications)]]"></span>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
notificationsOpen: {
|
||||
type: Boolean,
|
||||
notify: true,
|
||||
},
|
||||
notifications: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_clicked() {
|
||||
this.notificationsOpen = true;
|
||||
}
|
||||
|
||||
_hasNotifications(notifications) {
|
||||
return notifications.length > 0;
|
||||
}
|
||||
}
|
||||
customElements.define("hui-notifications-button", HuiNotificationsButton);
|
||||
|
|
|
@ -1,89 +1,89 @@
|
|||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../../components/ha-relative-time";
|
||||
import "../../../../components/ha-markdown";
|
||||
import "./hui-notification-item-template";
|
||||
|
||||
import LocalizeMixin from "../../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
export class HuiPersistentNotificationItem extends LocalizeMixin(
|
||||
PolymerElement
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
.time {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 6px;
|
||||
}
|
||||
ha-relative-time {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
</style>
|
||||
<hui-notification-item-template>
|
||||
<span slot="header">[[_computeTitle(notification)]]</span>
|
||||
|
||||
<ha-markdown content="[[notification.message]]"></ha-markdown>
|
||||
|
||||
<div class="time">
|
||||
<span>
|
||||
<ha-relative-time
|
||||
hass="[[hass]]"
|
||||
datetime="[[notification.created_at]]"
|
||||
></ha-relative-time>
|
||||
<paper-tooltip>[[_computeTooltip(hass, notification)]]</paper-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<paper-button
|
||||
slot="actions"
|
||||
class="primary"
|
||||
on-click="_handleDismiss"
|
||||
>[[localize('ui.card.persistent_notification.dismiss')]]</paper-button>
|
||||
</hui-notification-item-template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
notification: Object,
|
||||
};
|
||||
}
|
||||
|
||||
_handleDismiss() {
|
||||
this.hass.callService("persistent_notification", "dismiss", {
|
||||
notification_id: this.notification.notification_id,
|
||||
});
|
||||
}
|
||||
|
||||
_computeTitle(notification) {
|
||||
return notification.title || notification.notification_id;
|
||||
}
|
||||
|
||||
_computeTooltip(hass, notification) {
|
||||
if (!hass || !notification) return null;
|
||||
|
||||
const d = new Date(notification.created_at);
|
||||
return d.toLocaleDateString(hass.language, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
minute: "numeric",
|
||||
hour: "numeric",
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define(
|
||||
"hui-persistent_notification-notification-item",
|
||||
HuiPersistentNotificationItem
|
||||
);
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../../components/ha-relative-time";
|
||||
import "../../../../components/ha-markdown";
|
||||
import "./hui-notification-item-template";
|
||||
|
||||
import LocalizeMixin from "../../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
export class HuiPersistentNotificationItem extends LocalizeMixin(
|
||||
PolymerElement
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
.time {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 6px;
|
||||
}
|
||||
ha-relative-time {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
</style>
|
||||
<hui-notification-item-template>
|
||||
<span slot="header">[[_computeTitle(notification)]]</span>
|
||||
|
||||
<ha-markdown content="[[notification.message]]"></ha-markdown>
|
||||
|
||||
<div class="time">
|
||||
<span>
|
||||
<ha-relative-time
|
||||
hass="[[hass]]"
|
||||
datetime="[[notification.created_at]]"
|
||||
></ha-relative-time>
|
||||
<paper-tooltip>[[_computeTooltip(hass, notification)]]</paper-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<paper-button
|
||||
slot="actions"
|
||||
class="primary"
|
||||
on-click="_handleDismiss"
|
||||
>[[localize('ui.card.persistent_notification.dismiss')]]</paper-button>
|
||||
</hui-notification-item-template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
notification: Object,
|
||||
};
|
||||
}
|
||||
|
||||
_handleDismiss() {
|
||||
this.hass.callService("persistent_notification", "dismiss", {
|
||||
notification_id: this.notification.notification_id,
|
||||
});
|
||||
}
|
||||
|
||||
_computeTitle(notification) {
|
||||
return notification.title || notification.notification_id;
|
||||
}
|
||||
|
||||
_computeTooltip(hass, notification) {
|
||||
if (!hass || !notification) return null;
|
||||
|
||||
const d = new Date(notification.created_at);
|
||||
return d.toLocaleDateString(hass.language, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
minute: "numeric",
|
||||
hour: "numeric",
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define(
|
||||
"hui-persistent_notification-notification-item",
|
||||
HuiPersistentNotificationItem
|
||||
);
|
||||
|
|
|
@ -1,122 +1,122 @@
|
|||
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-input/paper-textarea";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-dialog/paper-dialog";
|
||||
// This is not a duplicate import, one is for types, one is for element.
|
||||
// tslint:disable-next-line
|
||||
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { getCardConfig, updateCardConfig } from "../common/data";
|
||||
|
||||
import "./hui-yaml-editor";
|
||||
import "./hui-yaml-card-preview";
|
||||
// This is not a duplicate import, one is for types, one is for element.
|
||||
// tslint:disable-next-line
|
||||
import { HuiYAMLCardPreview } from "./hui-yaml-card-preview";
|
||||
|
||||
export class HuiDialogEditCard extends LitElement {
|
||||
protected hass?: HomeAssistant;
|
||||
private _cardId?: string;
|
||||
private _cardConfig?: string;
|
||||
private _reloadLovelace?: () => void;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
cardId: {
|
||||
type: Number,
|
||||
},
|
||||
_cardConfig: {},
|
||||
_dialogClosedCallback: {},
|
||||
};
|
||||
}
|
||||
|
||||
public async showDialog({ hass, cardId, reloadLovelace }) {
|
||||
this.hass = hass;
|
||||
this._cardId = cardId;
|
||||
this._reloadLovelace = reloadLovelace;
|
||||
this._cardConfig = "";
|
||||
this._loadConfig();
|
||||
// Wait till dialog is rendered.
|
||||
await this.updateComplete;
|
||||
this._dialog.open();
|
||||
}
|
||||
|
||||
private get _dialog(): PaperDialogElement {
|
||||
return this.shadowRoot!.querySelector("paper-dialog")!;
|
||||
}
|
||||
|
||||
private get _previewEl(): HuiYAMLCardPreview {
|
||||
return this.shadowRoot!.querySelector("hui-yaml-card-preview")!;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<style>
|
||||
paper-dialog {
|
||||
width: 650px;
|
||||
}
|
||||
</style>
|
||||
<paper-dialog with-backdrop>
|
||||
<h2>Card Configuration</h2>
|
||||
<paper-dialog-scrollable>
|
||||
<hui-yaml-editor
|
||||
.yaml="${this._cardConfig}"
|
||||
@yaml-changed="${this._handleYamlChanged}"
|
||||
></hui-yaml-editor>
|
||||
<hui-yaml-card-preview
|
||||
.hass="${this.hass}"
|
||||
.yaml="${this._cardConfig}"
|
||||
></hui-yaml-card-preview>
|
||||
</paper-dialog-scrollable>
|
||||
<div class="paper-dialog-buttons">
|
||||
<paper-button @click="${this._closeDialog}">Cancel</paper-button>
|
||||
<paper-button @click="${this._updateConfig}">Save</paper-button>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleYamlChanged(ev) {
|
||||
this._previewEl.yaml = ev.detail.yaml;
|
||||
}
|
||||
|
||||
private _closeDialog() {
|
||||
this._dialog.close();
|
||||
}
|
||||
|
||||
private async _loadConfig() {
|
||||
this._cardConfig = await getCardConfig(this.hass!, this._cardId!);
|
||||
await this.updateComplete;
|
||||
// This will center the dialog with the updated config
|
||||
fireEvent(this._dialog, "iron-resize");
|
||||
}
|
||||
|
||||
private async _updateConfig() {
|
||||
const newCardConfig = this.shadowRoot!.querySelector("hui-yaml-editor")!
|
||||
.yaml;
|
||||
|
||||
if (this._cardConfig === newCardConfig) {
|
||||
this._dialog.close();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await updateCardConfig(this.hass!, this._cardId!, newCardConfig);
|
||||
this._dialog.close();
|
||||
this._reloadLovelace!();
|
||||
} catch (err) {
|
||||
alert(`Saving failed: ${err.reason}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-edit-card": HuiDialogEditCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-dialog-edit-card", HuiDialogEditCard);
|
||||
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-input/paper-textarea";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-dialog/paper-dialog";
|
||||
// This is not a duplicate import, one is for types, one is for element.
|
||||
// tslint:disable-next-line
|
||||
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { getCardConfig, updateCardConfig } from "../common/data";
|
||||
|
||||
import "./hui-yaml-editor";
|
||||
import "./hui-yaml-card-preview";
|
||||
// This is not a duplicate import, one is for types, one is for element.
|
||||
// tslint:disable-next-line
|
||||
import { HuiYAMLCardPreview } from "./hui-yaml-card-preview";
|
||||
|
||||
export class HuiDialogEditCard extends LitElement {
|
||||
protected hass?: HomeAssistant;
|
||||
private _cardId?: string;
|
||||
private _cardConfig?: string;
|
||||
private _reloadLovelace?: () => void;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
cardId: {
|
||||
type: Number,
|
||||
},
|
||||
_cardConfig: {},
|
||||
_dialogClosedCallback: {},
|
||||
};
|
||||
}
|
||||
|
||||
public async showDialog({ hass, cardId, reloadLovelace }) {
|
||||
this.hass = hass;
|
||||
this._cardId = cardId;
|
||||
this._reloadLovelace = reloadLovelace;
|
||||
this._cardConfig = "";
|
||||
this._loadConfig();
|
||||
// Wait till dialog is rendered.
|
||||
await this.updateComplete;
|
||||
this._dialog.open();
|
||||
}
|
||||
|
||||
private get _dialog(): PaperDialogElement {
|
||||
return this.shadowRoot!.querySelector("paper-dialog")!;
|
||||
}
|
||||
|
||||
private get _previewEl(): HuiYAMLCardPreview {
|
||||
return this.shadowRoot!.querySelector("hui-yaml-card-preview")!;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<style>
|
||||
paper-dialog {
|
||||
width: 650px;
|
||||
}
|
||||
</style>
|
||||
<paper-dialog with-backdrop>
|
||||
<h2>Card Configuration</h2>
|
||||
<paper-dialog-scrollable>
|
||||
<hui-yaml-editor
|
||||
.yaml="${this._cardConfig}"
|
||||
@yaml-changed="${this._handleYamlChanged}"
|
||||
></hui-yaml-editor>
|
||||
<hui-yaml-card-preview
|
||||
.hass="${this.hass}"
|
||||
.yaml="${this._cardConfig}"
|
||||
></hui-yaml-card-preview>
|
||||
</paper-dialog-scrollable>
|
||||
<div class="paper-dialog-buttons">
|
||||
<paper-button @click="${this._closeDialog}">Cancel</paper-button>
|
||||
<paper-button @click="${this._updateConfig}">Save</paper-button>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleYamlChanged(ev) {
|
||||
this._previewEl.yaml = ev.detail.yaml;
|
||||
}
|
||||
|
||||
private _closeDialog() {
|
||||
this._dialog.close();
|
||||
}
|
||||
|
||||
private async _loadConfig() {
|
||||
this._cardConfig = await getCardConfig(this.hass!, this._cardId!);
|
||||
await this.updateComplete;
|
||||
// This will center the dialog with the updated config
|
||||
fireEvent(this._dialog, "iron-resize");
|
||||
}
|
||||
|
||||
private async _updateConfig() {
|
||||
const newCardConfig = this.shadowRoot!.querySelector("hui-yaml-editor")!
|
||||
.yaml;
|
||||
|
||||
if (this._cardConfig === newCardConfig) {
|
||||
this._dialog.close();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await updateCardConfig(this.hass!, this._cardId!, newCardConfig);
|
||||
this._dialog.close();
|
||||
this._reloadLovelace!();
|
||||
} catch (err) {
|
||||
alert(`Saving failed: ${err.reason}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-edit-card": HuiDialogEditCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-dialog-edit-card", HuiDialogEditCard);
|
||||
|
|
|
@ -1,52 +1,52 @@
|
|||
import yaml from "js-yaml";
|
||||
|
||||
import "@polymer/paper-input/paper-textarea";
|
||||
|
||||
import createCardElement from "../common/create-card-element";
|
||||
import createErrorCardConfig from "../common/create-error-card-config";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
|
||||
export class HuiYAMLCardPreview extends HTMLElement {
|
||||
private _hass?: HomeAssistant;
|
||||
|
||||
set hass(value: HomeAssistant) {
|
||||
this._hass = value;
|
||||
if (this.lastChild) {
|
||||
(this.lastChild as LovelaceCard).hass = value;
|
||||
}
|
||||
}
|
||||
|
||||
set yaml(value: string) {
|
||||
if (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
|
||||
if (value === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
let conf;
|
||||
try {
|
||||
conf = yaml.safeLoad(value);
|
||||
} catch (err) {
|
||||
conf = createErrorCardConfig(`Invalid YAML: ${err.message}`, undefined);
|
||||
}
|
||||
|
||||
const element = createCardElement(conf);
|
||||
|
||||
if (this._hass) {
|
||||
element.hass = this._hass;
|
||||
}
|
||||
|
||||
this.appendChild(element);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-yaml-card-preview": HuiYAMLCardPreview;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-yaml-card-preview", HuiYAMLCardPreview);
|
||||
import yaml from "js-yaml";
|
||||
|
||||
import "@polymer/paper-input/paper-textarea";
|
||||
|
||||
import createCardElement from "../common/create-card-element";
|
||||
import createErrorCardConfig from "../common/create-error-card-config";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
|
||||
export class HuiYAMLCardPreview extends HTMLElement {
|
||||
private _hass?: HomeAssistant;
|
||||
|
||||
set hass(value: HomeAssistant) {
|
||||
this._hass = value;
|
||||
if (this.lastChild) {
|
||||
(this.lastChild as LovelaceCard).hass = value;
|
||||
}
|
||||
}
|
||||
|
||||
set yaml(value: string) {
|
||||
if (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
|
||||
if (value === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
let conf;
|
||||
try {
|
||||
conf = yaml.safeLoad(value);
|
||||
} catch (err) {
|
||||
conf = createErrorCardConfig(`Invalid YAML: ${err.message}`, undefined);
|
||||
}
|
||||
|
||||
const element = createCardElement(conf);
|
||||
|
||||
if (this._hass) {
|
||||
element.hass = this._hass;
|
||||
}
|
||||
|
||||
this.appendChild(element);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-yaml-card-preview": HuiYAMLCardPreview;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-yaml-card-preview", HuiYAMLCardPreview);
|
||||
|
|
|
@ -1,41 +1,41 @@
|
|||
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
import "@polymer/paper-input/paper-textarea";
|
||||
|
||||
export class HuiYAMLEditor extends LitElement {
|
||||
public yaml?: string;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
yaml: {},
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<style>
|
||||
paper-textarea {
|
||||
--paper-input-container-shared-input-style_-_font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
<paper-textarea
|
||||
value="${this.yaml}"
|
||||
@value-changed="${this._valueChanged}"
|
||||
></paper-textarea>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
this.yaml = ev.target.value;
|
||||
fireEvent(this, "yaml-changed", { yaml: ev.target.value });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-yaml-editor": HuiYAMLEditor;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-yaml-editor", HuiYAMLEditor);
|
||||
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
import "@polymer/paper-input/paper-textarea";
|
||||
|
||||
export class HuiYAMLEditor extends LitElement {
|
||||
public yaml?: string;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
yaml: {},
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<style>
|
||||
paper-textarea {
|
||||
--paper-input-container-shared-input-style_-_font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
<paper-textarea
|
||||
value="${this.yaml}"
|
||||
@value-changed="${this._valueChanged}"
|
||||
></paper-textarea>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
this.yaml = ev.target.value;
|
||||
fireEvent(this, "yaml-changed", { yaml: ev.target.value });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-yaml-editor": HuiYAMLEditor;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-yaml-editor", HuiYAMLEditor);
|
||||
|
|
|
@ -1,68 +1,68 @@
|
|||
import { html, LitElement } from "@polymer/lit-element";
|
||||
|
||||
import "../../../components/ha-icon";
|
||||
|
||||
import { computeTooltip } from "../common/compute-tooltip";
|
||||
import { handleClick } from "../common/handle-click";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceElement, LovelaceElementConfig } from "./types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
interface Config extends LovelaceElementConfig {
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export class HuiIconElement extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceElement {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
|
||||
static get properties() {
|
||||
return { hass: {}, _config: {} };
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config.icon) {
|
||||
throw Error("Invalid Configuration: 'icon' required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-icon
|
||||
.icon="${this._config.icon}"
|
||||
.title="${computeTooltip(this.hass!, this._config)}"
|
||||
@ha-click="${() => handleClick(this, this.hass!, this._config!, false)}"
|
||||
@ha-hold="${() => handleClick(this, this.hass!, this._config!, true)}"
|
||||
.longPress="${longPress()}"
|
||||
></ha-icon>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-icon-element": HuiIconElement;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-icon-element", HuiIconElement);
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
|
||||
import "../../../components/ha-icon";
|
||||
|
||||
import { computeTooltip } from "../common/compute-tooltip";
|
||||
import { handleClick } from "../common/handle-click";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceElement, LovelaceElementConfig } from "./types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
interface Config extends LovelaceElementConfig {
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export class HuiIconElement extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceElement {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
|
||||
static get properties() {
|
||||
return { hass: {}, _config: {} };
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config.icon) {
|
||||
throw Error("Invalid Configuration: 'icon' required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-icon
|
||||
.icon="${this._config.icon}"
|
||||
.title="${computeTooltip(this.hass!, this._config)}"
|
||||
@ha-click="${() => handleClick(this, this.hass!, this._config!, false)}"
|
||||
@ha-hold="${() => handleClick(this, this.hass!, this._config!, true)}"
|
||||
.longPress="${longPress()}"
|
||||
></ha-icon>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-icon-element": HuiIconElement;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-icon-element", HuiIconElement);
|
||||
|
|
|
@ -1,94 +1,94 @@
|
|||
import { html, LitElement } from "@polymer/lit-element";
|
||||
|
||||
import "../components/hui-image";
|
||||
|
||||
import { computeTooltip } from "../common/compute-tooltip";
|
||||
import { handleClick } from "../common/handle-click";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceElement, LovelaceElementConfig } from "./types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
interface Config extends LovelaceElementConfig {
|
||||
image?: string;
|
||||
state_image?: string;
|
||||
camera_image?: string;
|
||||
filter?: string;
|
||||
state_filter?: string;
|
||||
aspect_ratio?: string;
|
||||
}
|
||||
|
||||
export class HuiImageElement extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceElement {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
|
||||
static get properties() {
|
||||
return { hass: {}, _config: {} };
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config) {
|
||||
throw Error("Error in element configuration");
|
||||
}
|
||||
|
||||
this.classList.toggle("clickable", config.tap_action !== "none");
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<hui-image
|
||||
.hass=${this.hass}
|
||||
.entity="${this._config.entity}"
|
||||
.image="${this._config.image}"
|
||||
.stateImage=${this._config.state_image}
|
||||
.cameraImage=${this._config.camera_image}
|
||||
.filter=${this._config.filter}
|
||||
.stateFilter=${this._config.state_filter}
|
||||
.title="${computeTooltip(this.hass!, this._config)}"
|
||||
.aspectRatio="${this._config.aspect_ratio}"
|
||||
@ha-click="${this._handleClick}"
|
||||
@ha-hold="${this._handleHold}"
|
||||
.longPress="${longPress()}"
|
||||
></hui-image>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host(.clickable) {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
-webkit-touch-callout: none !important;
|
||||
}
|
||||
hui-image {
|
||||
-webkit-user-select: none !important;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick() {
|
||||
handleClick(this, this.hass!, this._config!, false);
|
||||
}
|
||||
|
||||
private _handleHold() {
|
||||
handleClick(this, this.hass!, this._config!, true);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-image-element": HuiImageElement;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-image-element", HuiImageElement);
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
|
||||
import "../components/hui-image";
|
||||
|
||||
import { computeTooltip } from "../common/compute-tooltip";
|
||||
import { handleClick } from "../common/handle-click";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceElement, LovelaceElementConfig } from "./types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
interface Config extends LovelaceElementConfig {
|
||||
image?: string;
|
||||
state_image?: string;
|
||||
camera_image?: string;
|
||||
filter?: string;
|
||||
state_filter?: string;
|
||||
aspect_ratio?: string;
|
||||
}
|
||||
|
||||
export class HuiImageElement extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceElement {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
|
||||
static get properties() {
|
||||
return { hass: {}, _config: {} };
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config) {
|
||||
throw Error("Error in element configuration");
|
||||
}
|
||||
|
||||
this.classList.toggle("clickable", config.tap_action !== "none");
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<hui-image
|
||||
.hass=${this.hass}
|
||||
.entity="${this._config.entity}"
|
||||
.image="${this._config.image}"
|
||||
.stateImage=${this._config.state_image}
|
||||
.cameraImage=${this._config.camera_image}
|
||||
.filter=${this._config.filter}
|
||||
.stateFilter=${this._config.state_filter}
|
||||
.title="${computeTooltip(this.hass!, this._config)}"
|
||||
.aspectRatio="${this._config.aspect_ratio}"
|
||||
@ha-click="${this._handleClick}"
|
||||
@ha-hold="${this._handleHold}"
|
||||
.longPress="${longPress()}"
|
||||
></hui-image>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host(.clickable) {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
-webkit-touch-callout: none !important;
|
||||
}
|
||||
hui-image {
|
||||
-webkit-user-select: none !important;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick() {
|
||||
handleClick(this, this.hass!, this._config!, false);
|
||||
}
|
||||
|
||||
private _handleHold() {
|
||||
handleClick(this, this.hass!, this._config!, true);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-image-element": HuiImageElement;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-image-element", HuiImageElement);
|
||||
|
|
|
@ -1,74 +1,74 @@
|
|||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
|
||||
import { LovelaceElement, LovelaceElementConfig } from "./types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
export class HuiServiceButtonElement extends LitElement
|
||||
implements LovelaceElement {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: LovelaceElementConfig;
|
||||
private _domain?: string;
|
||||
private _service?: string;
|
||||
|
||||
static get properties() {
|
||||
return { _config: {} };
|
||||
}
|
||||
|
||||
public setConfig(config: LovelaceElementConfig): void {
|
||||
if (!config || !config.service) {
|
||||
throw Error("Invalid Configuration: 'service' required");
|
||||
}
|
||||
|
||||
[this._domain, this._service] = config.service.split(".", 2);
|
||||
|
||||
if (!this._domain) {
|
||||
throw Error("Invalid Configuration: 'service' does not have a domain");
|
||||
}
|
||||
|
||||
if (!this._service) {
|
||||
throw Error(
|
||||
"Invalid Configuration: 'service' does not have a service name"
|
||||
);
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-call-service-button
|
||||
.hass="${this.hass}"
|
||||
.domain="${this._domain}"
|
||||
.service="${this._service}"
|
||||
.service-data="${this._config.service_data}"
|
||||
>${this._config.title}</ha-call-service-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
ha-call-service-button {
|
||||
color: var(--primary-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-service-button-element": HuiServiceButtonElement;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-service-button-element", HuiServiceButtonElement);
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
|
||||
import { LovelaceElement, LovelaceElementConfig } from "./types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
export class HuiServiceButtonElement extends LitElement
|
||||
implements LovelaceElement {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: LovelaceElementConfig;
|
||||
private _domain?: string;
|
||||
private _service?: string;
|
||||
|
||||
static get properties() {
|
||||
return { _config: {} };
|
||||
}
|
||||
|
||||
public setConfig(config: LovelaceElementConfig): void {
|
||||
if (!config || !config.service) {
|
||||
throw Error("Invalid Configuration: 'service' required");
|
||||
}
|
||||
|
||||
[this._domain, this._service] = config.service.split(".", 2);
|
||||
|
||||
if (!this._domain) {
|
||||
throw Error("Invalid Configuration: 'service' does not have a domain");
|
||||
}
|
||||
|
||||
if (!this._service) {
|
||||
throw Error(
|
||||
"Invalid Configuration: 'service' does not have a service name"
|
||||
);
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-call-service-button
|
||||
.hass="${this.hass}"
|
||||
.domain="${this._domain}"
|
||||
.service="${this._service}"
|
||||
.service-data="${this._config.service_data}"
|
||||
>${this._config.title}</ha-call-service-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
ha-call-service-button {
|
||||
color: var(--primary-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-service-button-element": HuiServiceButtonElement;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-service-button-element", HuiServiceButtonElement);
|
||||
|
|
|
@ -1,53 +1,53 @@
|
|||
import { html, LitElement } from "@polymer/lit-element";
|
||||
|
||||
import "../../../components/entity/ha-state-label-badge";
|
||||
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import { LovelaceElement, LovelaceElementConfig } from "./types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
export class HuiStateBadgeElement extends LitElement
|
||||
implements LovelaceElement {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: LovelaceElementConfig;
|
||||
|
||||
static get properties() {
|
||||
return { hass: {}, _config: {} };
|
||||
}
|
||||
|
||||
public setConfig(config: LovelaceElementConfig): void {
|
||||
if (!config.entity) {
|
||||
throw Error("Invalid Configuration: 'entity' required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.hass.states[this._config.entity!]
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const state = this.hass.states[this._config.entity!];
|
||||
return html`
|
||||
<ha-state-label-badge
|
||||
.hass=${this.hass}
|
||||
.state=${state}
|
||||
.title="${computeStateName(state)}"
|
||||
></ha-state-label-badge>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-state-badge-element": HuiStateBadgeElement;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-state-badge-element", HuiStateBadgeElement);
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
|
||||
import "../../../components/entity/ha-state-label-badge";
|
||||
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import { LovelaceElement, LovelaceElementConfig } from "./types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
export class HuiStateBadgeElement extends LitElement
|
||||
implements LovelaceElement {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: LovelaceElementConfig;
|
||||
|
||||
static get properties() {
|
||||
return { hass: {}, _config: {} };
|
||||
}
|
||||
|
||||
public setConfig(config: LovelaceElementConfig): void {
|
||||
if (!config.entity) {
|
||||
throw Error("Invalid Configuration: 'entity' required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.hass.states[this._config.entity!]
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const state = this.hass.states[this._config.entity!];
|
||||
return html`
|
||||
<ha-state-label-badge
|
||||
.hass=${this.hass}
|
||||
.state=${state}
|
||||
.title="${computeStateName(state)}"
|
||||
></ha-state-label-badge>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-state-badge-element": HuiStateBadgeElement;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-state-badge-element", HuiStateBadgeElement);
|
||||
|
|
|
@ -1,77 +1,77 @@
|
|||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import "../../../components/entity/state-badge";
|
||||
|
||||
import { computeTooltip } from "../common/compute-tooltip";
|
||||
import { handleClick } from "../common/handle-click";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceElement, LovelaceElementConfig } from "./types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
export class HuiStateIconElement extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceElement {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: LovelaceElementConfig;
|
||||
|
||||
static get properties() {
|
||||
return { hass: {}, _config: {} };
|
||||
}
|
||||
|
||||
public setConfig(config: LovelaceElementConfig): void {
|
||||
if (!config.entity) {
|
||||
throw Error("Invalid Configuration: 'entity' required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.hass.states[this._config.entity!]
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const state = this.hass!.states[this._config.entity!];
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<state-badge
|
||||
.stateObj=${state}
|
||||
.title="${computeTooltip(this.hass!, this._config)}"
|
||||
@ha-click="${this._handleClick}"
|
||||
@ha-hold="${this._handleHold}"
|
||||
.longPress="${longPress()}"
|
||||
></state-badge>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick() {
|
||||
handleClick(this, this.hass!, this._config!, false);
|
||||
}
|
||||
|
||||
private _handleHold() {
|
||||
handleClick(this, this.hass!, this._config!, true);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-state-icon-element": HuiStateIconElement;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-state-icon-element", HuiStateIconElement);
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import "../../../components/entity/state-badge";
|
||||
|
||||
import { computeTooltip } from "../common/compute-tooltip";
|
||||
import { handleClick } from "../common/handle-click";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceElement, LovelaceElementConfig } from "./types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
export class HuiStateIconElement extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceElement {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: LovelaceElementConfig;
|
||||
|
||||
static get properties() {
|
||||
return { hass: {}, _config: {} };
|
||||
}
|
||||
|
||||
public setConfig(config: LovelaceElementConfig): void {
|
||||
if (!config.entity) {
|
||||
throw Error("Invalid Configuration: 'entity' required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.hass.states[this._config.entity!]
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const state = this.hass!.states[this._config.entity!];
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<state-badge
|
||||
.stateObj=${state}
|
||||
.title="${computeTooltip(this.hass!, this._config)}"
|
||||
@ha-click="${this._handleClick}"
|
||||
@ha-hold="${this._handleHold}"
|
||||
.longPress="${longPress()}"
|
||||
></state-badge>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick() {
|
||||
handleClick(this, this.hass!, this._config!, false);
|
||||
}
|
||||
|
||||
private _handleHold() {
|
||||
handleClick(this, this.hass!, this._config!, true);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-state-icon-element": HuiStateIconElement;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-state-icon-element", HuiStateIconElement);
|
||||
|
|
|
@ -1,78 +1,78 @@
|
|||
import { html, LitElement } from "@polymer/lit-element";
|
||||
|
||||
import "../../../components/entity/ha-state-label-badge";
|
||||
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
import { computeTooltip } from "../common/compute-tooltip";
|
||||
import { handleClick } from "../common/handle-click";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceElement, LovelaceElementConfig } from "./types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
interface Config extends LovelaceElementConfig {
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
class HuiStateLabelElement extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceElement {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
|
||||
static get properties() {
|
||||
return { hass: {}, _config: {} };
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config.entity) {
|
||||
throw Error("Invalid Configuration: 'entity' required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const state = this.hass!.states[this._config.entity!];
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<div
|
||||
.title="${computeTooltip(this.hass!, this._config)}"
|
||||
@ha-click="${() => handleClick(this, this.hass!, this._config!, false)}"
|
||||
@ha-hold="${() => handleClick(this, this.hass!, this._config!, true)}"
|
||||
.longPress="${longPress()}"
|
||||
>
|
||||
${this._config.prefix}${
|
||||
state ? computeStateDisplay(this.localize, state) : "-"
|
||||
}${this._config.suffix}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
cursor: pointer;
|
||||
}
|
||||
div {
|
||||
padding: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-state-label-element": HuiStateLabelElement;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-state-label-element", HuiStateLabelElement);
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
|
||||
import "../../../components/entity/ha-state-label-badge";
|
||||
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
import { computeTooltip } from "../common/compute-tooltip";
|
||||
import { handleClick } from "../common/handle-click";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceElement, LovelaceElementConfig } from "./types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
interface Config extends LovelaceElementConfig {
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
class HuiStateLabelElement extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceElement {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
|
||||
static get properties() {
|
||||
return { hass: {}, _config: {} };
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
if (!config.entity) {
|
||||
throw Error("Invalid Configuration: 'entity' required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const state = this.hass!.states[this._config.entity!];
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<div
|
||||
.title="${computeTooltip(this.hass!, this._config)}"
|
||||
@ha-click="${() => handleClick(this, this.hass!, this._config!, false)}"
|
||||
@ha-hold="${() => handleClick(this, this.hass!, this._config!, true)}"
|
||||
.longPress="${longPress()}"
|
||||
>
|
||||
${this._config.prefix}${
|
||||
state ? computeStateDisplay(this.localize, state) : "-"
|
||||
}${this._config.suffix}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
cursor: pointer;
|
||||
}
|
||||
div {
|
||||
padding: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-state-label-element": HuiStateLabelElement;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-state-label-element", HuiStateLabelElement);
|
||||
|
|
|
@ -1,65 +1,65 @@
|
|||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import "../../../components/ha-climate-state";
|
||||
import "../components/hui-generic-entity-row";
|
||||
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { EntityRow, EntityConfig } from "./types";
|
||||
|
||||
class HuiClimateEntityRow extends LitElement implements EntityRow {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: EntityConfig;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: EntityConfig): void {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Invalid Configuration: 'entity' required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<hui-generic-entity-row
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
>
|
||||
<ha-climate-state
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.hass.states[this._config.entity]}
|
||||
></ha-climate-state>
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
ha-climate-state {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-climate-entity-row": HuiClimateEntityRow;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-climate-entity-row", HuiClimateEntityRow);
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
import "../../../components/ha-climate-state";
|
||||
import "../components/hui-generic-entity-row";
|
||||
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { EntityRow, EntityConfig } from "./types";
|
||||
|
||||
class HuiClimateEntityRow extends LitElement implements EntityRow {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: EntityConfig;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: EntityConfig): void {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Invalid Configuration: 'entity' required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<hui-generic-entity-row
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
>
|
||||
<ha-climate-state
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.hass.states[this._config.entity]}
|
||||
></ha-climate-state>
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
ha-climate-state {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-climate-entity-row": HuiClimateEntityRow;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-climate-entity-row", HuiClimateEntityRow);
|
||||
|
|
|
@ -1,74 +1,74 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
import "../../../components/ha-cover-controls";
|
||||
import "../../../components/ha-cover-tilt-controls";
|
||||
import CoverEntity from "../../../util/cover-model";
|
||||
|
||||
class HuiCoverEntityRow extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.coverControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
ha-cover-controls,
|
||||
ha-cover-tilt-controls {
|
||||
margin-right: -.57em;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get coverControlTemplate() {
|
||||
return html`
|
||||
<template is="dom-if" if="[[!_entityObj.isTiltOnly]]">
|
||||
<ha-cover-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-controls>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_entityObj.isTiltOnly]]">
|
||||
<ha-cover-tilt-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-tilt-controls>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
_entityObj: {
|
||||
type: Object,
|
||||
computed: "_computeEntityObj(hass, _stateObj)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
_computeEntityObj(hass, stateObj) {
|
||||
return stateObj ? new CoverEntity(hass, stateObj) : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
}
|
||||
customElements.define("hui-cover-entity-row", HuiCoverEntityRow);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
import "../../../components/ha-cover-controls";
|
||||
import "../../../components/ha-cover-tilt-controls";
|
||||
import CoverEntity from "../../../util/cover-model";
|
||||
|
||||
class HuiCoverEntityRow extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.coverControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
ha-cover-controls,
|
||||
ha-cover-tilt-controls {
|
||||
margin-right: -.57em;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get coverControlTemplate() {
|
||||
return html`
|
||||
<template is="dom-if" if="[[!_entityObj.isTiltOnly]]">
|
||||
<ha-cover-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-controls>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_entityObj.isTiltOnly]]">
|
||||
<ha-cover-tilt-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-tilt-controls>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
_entityObj: {
|
||||
type: Object,
|
||||
computed: "_computeEntityObj(hass, _stateObj)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
_computeEntityObj(hass, stateObj) {
|
||||
return stateObj ? new CoverEntity(hass, stateObj) : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
}
|
||||
customElements.define("hui-cover-entity-row", HuiCoverEntityRow);
|
||||
|
|
|
@ -1,78 +1,78 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiGroupEntityRow extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.groupControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get groupControlTemplate() {
|
||||
return html`
|
||||
<template is="dom-if" if="[[_canToggle]]">
|
||||
<ha-entity-toggle
|
||||
hass="[[hass]]"
|
||||
state-obj="[[_stateObj]]"
|
||||
></ha-entity-toggle>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_canToggle]]">
|
||||
<div>
|
||||
[[_computeState(_stateObj)]]
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
_canToggle: {
|
||||
type: Boolean,
|
||||
computed: "_computeCanToggle(_stateObj.attributes.entity_id)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
_computeCanToggle(entityIds) {
|
||||
return entityIds.some((entityId) =>
|
||||
DOMAINS_TOGGLE.has(entityId.split(".", 1)[0])
|
||||
);
|
||||
}
|
||||
|
||||
_computeState(stateObj) {
|
||||
return computeStateDisplay(this.localize, stateObj);
|
||||
}
|
||||
}
|
||||
customElements.define("hui-group-entity-row", HuiGroupEntityRow);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiGroupEntityRow extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.groupControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get groupControlTemplate() {
|
||||
return html`
|
||||
<template is="dom-if" if="[[_canToggle]]">
|
||||
<ha-entity-toggle
|
||||
hass="[[hass]]"
|
||||
state-obj="[[_stateObj]]"
|
||||
></ha-entity-toggle>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_canToggle]]">
|
||||
<div>
|
||||
[[_computeState(_stateObj)]]
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
_canToggle: {
|
||||
type: Boolean,
|
||||
computed: "_computeCanToggle(_stateObj.attributes.entity_id)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
_computeCanToggle(entityIds) {
|
||||
return entityIds.some((entityId) =>
|
||||
DOMAINS_TOGGLE.has(entityId.split(".", 1)[0])
|
||||
);
|
||||
}
|
||||
|
||||
_computeState(stateObj) {
|
||||
return computeStateDisplay(this.localize, stateObj);
|
||||
}
|
||||
}
|
||||
customElements.define("hui-group-entity-row", HuiGroupEntityRow);
|
||||
|
|
|
@ -1,170 +1,170 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior";
|
||||
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
import "../../../components/ha-slider";
|
||||
|
||||
class HuiInputNumberEntityRow extends mixinBehaviors(
|
||||
[IronResizableBehavior],
|
||||
PolymerElement
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
id="input_number_card"
|
||||
>
|
||||
${this.inputNumberControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.state {
|
||||
min-width: 45px;
|
||||
text-align: center;
|
||||
}
|
||||
paper-input {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get inputNumberControlTemplate() {
|
||||
return html`
|
||||
<div>
|
||||
<template is="dom-if" if="[[_equals(_stateObj.attributes.mode, 'slider')]]">
|
||||
<div class="flex">
|
||||
<ha-slider
|
||||
min="[[_min]]"
|
||||
max="[[_max]]"
|
||||
value="{{_value}}"
|
||||
step="[[_step]]"
|
||||
pin
|
||||
on-change="_selectedValueChanged"
|
||||
ignore-bar-touch
|
||||
></ha-slider>
|
||||
<span class="state">[[_value]] [[_stateObj.attributes.unit_of_measurement]]</span>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_stateObj.attributes.mode, 'box')]]">
|
||||
<paper-input
|
||||
no-label-float
|
||||
auto-validate
|
||||
pattern="[0-9]+([\\.][0-9]+)?"
|
||||
step="[[_step]]"
|
||||
min="[[_min]]"
|
||||
max="[[_max]]"
|
||||
value="{{_value}}"
|
||||
type="number"
|
||||
on-change="_selectedValueChanged"
|
||||
></paper-input>
|
||||
</template>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
observer: "_stateObjChanged",
|
||||
},
|
||||
_min: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
_max: {
|
||||
type: Number,
|
||||
value: 100,
|
||||
},
|
||||
_step: Number,
|
||||
_value: Number,
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
if (typeof ResizeObserver === "function") {
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
entries.forEach(() => {
|
||||
this._hiddenState();
|
||||
});
|
||||
});
|
||||
ro.observe(this.$.input_number_card);
|
||||
} else {
|
||||
this.addEventListener("iron-resize", this._hiddenState);
|
||||
}
|
||||
}
|
||||
|
||||
_equals(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_hiddenState() {
|
||||
if (
|
||||
!this.$ ||
|
||||
!this._stateObj ||
|
||||
this._stateObj.attributes.mode !== "slider"
|
||||
)
|
||||
return;
|
||||
const width = this.$.input_number_card.offsetWidth;
|
||||
const stateEl = this.shadowRoot.querySelector(".state");
|
||||
if (!stateEl) return;
|
||||
stateEl.hidden = width <= 350;
|
||||
}
|
||||
|
||||
_stateObjChanged(stateObj, oldStateObj) {
|
||||
if (!stateObj) return;
|
||||
|
||||
this.setProperties({
|
||||
_min: Number(stateObj.attributes.min),
|
||||
_max: Number(stateObj.attributes.max),
|
||||
_step: Number(stateObj.attributes.step),
|
||||
_value: Number(stateObj.state),
|
||||
});
|
||||
if (
|
||||
oldStateObj &&
|
||||
stateObj.attributes.mode === "slider" &&
|
||||
oldStateObj.attributes.mode !== "slider"
|
||||
) {
|
||||
this._hiddenState();
|
||||
}
|
||||
}
|
||||
|
||||
_selectedValueChanged() {
|
||||
if (this._value === Number(this._stateObj.state)) return;
|
||||
|
||||
this.hass.callService("input_number", "set_value", {
|
||||
value: this._value,
|
||||
entity_id: this._stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("hui-input-number-entity-row", HuiInputNumberEntityRow);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior";
|
||||
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
import "../../../components/ha-slider";
|
||||
|
||||
class HuiInputNumberEntityRow extends mixinBehaviors(
|
||||
[IronResizableBehavior],
|
||||
PolymerElement
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
id="input_number_card"
|
||||
>
|
||||
${this.inputNumberControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.state {
|
||||
min-width: 45px;
|
||||
text-align: center;
|
||||
}
|
||||
paper-input {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get inputNumberControlTemplate() {
|
||||
return html`
|
||||
<div>
|
||||
<template is="dom-if" if="[[_equals(_stateObj.attributes.mode, 'slider')]]">
|
||||
<div class="flex">
|
||||
<ha-slider
|
||||
min="[[_min]]"
|
||||
max="[[_max]]"
|
||||
value="{{_value}}"
|
||||
step="[[_step]]"
|
||||
pin
|
||||
on-change="_selectedValueChanged"
|
||||
ignore-bar-touch
|
||||
></ha-slider>
|
||||
<span class="state">[[_value]] [[_stateObj.attributes.unit_of_measurement]]</span>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_stateObj.attributes.mode, 'box')]]">
|
||||
<paper-input
|
||||
no-label-float
|
||||
auto-validate
|
||||
pattern="[0-9]+([\\.][0-9]+)?"
|
||||
step="[[_step]]"
|
||||
min="[[_min]]"
|
||||
max="[[_max]]"
|
||||
value="{{_value}}"
|
||||
type="number"
|
||||
on-change="_selectedValueChanged"
|
||||
></paper-input>
|
||||
</template>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
observer: "_stateObjChanged",
|
||||
},
|
||||
_min: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
_max: {
|
||||
type: Number,
|
||||
value: 100,
|
||||
},
|
||||
_step: Number,
|
||||
_value: Number,
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
if (typeof ResizeObserver === "function") {
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
entries.forEach(() => {
|
||||
this._hiddenState();
|
||||
});
|
||||
});
|
||||
ro.observe(this.$.input_number_card);
|
||||
} else {
|
||||
this.addEventListener("iron-resize", this._hiddenState);
|
||||
}
|
||||
}
|
||||
|
||||
_equals(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_hiddenState() {
|
||||
if (
|
||||
!this.$ ||
|
||||
!this._stateObj ||
|
||||
this._stateObj.attributes.mode !== "slider"
|
||||
)
|
||||
return;
|
||||
const width = this.$.input_number_card.offsetWidth;
|
||||
const stateEl = this.shadowRoot.querySelector(".state");
|
||||
if (!stateEl) return;
|
||||
stateEl.hidden = width <= 350;
|
||||
}
|
||||
|
||||
_stateObjChanged(stateObj, oldStateObj) {
|
||||
if (!stateObj) return;
|
||||
|
||||
this.setProperties({
|
||||
_min: Number(stateObj.attributes.min),
|
||||
_max: Number(stateObj.attributes.max),
|
||||
_step: Number(stateObj.attributes.step),
|
||||
_value: Number(stateObj.state),
|
||||
});
|
||||
if (
|
||||
oldStateObj &&
|
||||
stateObj.attributes.mode === "slider" &&
|
||||
oldStateObj.attributes.mode !== "slider"
|
||||
) {
|
||||
this._hiddenState();
|
||||
}
|
||||
}
|
||||
|
||||
_selectedValueChanged() {
|
||||
if (this._value === Number(this._stateObj.state)) return;
|
||||
|
||||
this.hass.callService("input_number", "set_value", {
|
||||
value: this._value,
|
||||
entity_id: this._stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("hui-input-number-entity-row", HuiInputNumberEntityRow);
|
||||
|
|
|
@ -1,107 +1,107 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
|
||||
import "../../../components/entity/state-badge";
|
||||
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HuiInputSelectEntityRow extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<template is="dom-if" if="[[_stateObj]]">
|
||||
<state-badge state-obj="[[_stateObj]]"></state-badge>
|
||||
<paper-dropdown-menu on-click="_stopPropagation" selected-item-label="{{_selected}}" label="[[_computeName(_config.name, _stateObj)]]">
|
||||
<paper-listbox slot="dropdown-content" selected="[[_computeSelected(_stateObj)]]">
|
||||
<template is="dom-repeat" items="[[_stateObj.attributes.options]]">
|
||||
<paper-item>[[item]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_stateObj]]">
|
||||
<div class="not-found">
|
||||
Entity not available: [[_config.entity]]
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
paper-dropdown-menu {
|
||||
margin-left: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
.not-found {
|
||||
flex: 1;
|
||||
background-color: yellow;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
_selected: {
|
||||
type: String,
|
||||
observer: "_selectedChanged",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
_computeName(name, stateObj) {
|
||||
return name || computeStateName(stateObj);
|
||||
}
|
||||
|
||||
_computeSelected(stateObj) {
|
||||
return stateObj.attributes.options.indexOf(stateObj.state);
|
||||
}
|
||||
|
||||
_selectedChanged(option) {
|
||||
// Selected Option will transition to '' before transitioning to new value
|
||||
if (option === "" || option === this._stateObj.state) {
|
||||
return;
|
||||
}
|
||||
this.hass.callService("input_select", "select_option", {
|
||||
option: option,
|
||||
entity_id: this._stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
_stopPropagation(ev) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
customElements.define("hui-input-select-entity-row", HuiInputSelectEntityRow);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
|
||||
import "../../../components/entity/state-badge";
|
||||
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HuiInputSelectEntityRow extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<template is="dom-if" if="[[_stateObj]]">
|
||||
<state-badge state-obj="[[_stateObj]]"></state-badge>
|
||||
<paper-dropdown-menu on-click="_stopPropagation" selected-item-label="{{_selected}}" label="[[_computeName(_config.name, _stateObj)]]">
|
||||
<paper-listbox slot="dropdown-content" selected="[[_computeSelected(_stateObj)]]">
|
||||
<template is="dom-repeat" items="[[_stateObj.attributes.options]]">
|
||||
<paper-item>[[item]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_stateObj]]">
|
||||
<div class="not-found">
|
||||
Entity not available: [[_config.entity]]
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
paper-dropdown-menu {
|
||||
margin-left: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
.not-found {
|
||||
flex: 1;
|
||||
background-color: yellow;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
_selected: {
|
||||
type: String,
|
||||
observer: "_selectedChanged",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
_computeName(name, stateObj) {
|
||||
return name || computeStateName(stateObj);
|
||||
}
|
||||
|
||||
_computeSelected(stateObj) {
|
||||
return stateObj.attributes.options.indexOf(stateObj.state);
|
||||
}
|
||||
|
||||
_selectedChanged(option) {
|
||||
// Selected Option will transition to '' before transitioning to new value
|
||||
if (option === "" || option === this._stateObj.state) {
|
||||
return;
|
||||
}
|
||||
this.hass.callService("input_select", "select_option", {
|
||||
option: option,
|
||||
entity_id: this._stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
_stopPropagation(ev) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
customElements.define("hui-input-select-entity-row", HuiInputSelectEntityRow);
|
||||
|
|
|
@ -1,73 +1,73 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
|
||||
class HuiInputTextEntityRow extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.inputTextControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get inputTextControlTemplate() {
|
||||
return html`
|
||||
<paper-input
|
||||
no-label-float
|
||||
minlength="[[_stateObj.attributes.min]]"
|
||||
maxlength="[[_stateObj.attributes.max]]"
|
||||
value="{{_value}}"
|
||||
auto-validate="[[_stateObj.attributes.pattern]]"
|
||||
pattern="[[_stateObj.attributes.pattern]]"
|
||||
type="[[_stateObj.attributes.mode]]"
|
||||
on-change="_selectedValueChanged"
|
||||
placeholder="(empty value)"
|
||||
></paper-input>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
observer: "_stateObjChanged",
|
||||
},
|
||||
_value: String,
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_stateObjChanged(stateObj) {
|
||||
this._value = stateObj && stateObj.state;
|
||||
}
|
||||
|
||||
_selectedValueChanged() {
|
||||
if (this._value === this._stateObj.state) {
|
||||
return;
|
||||
}
|
||||
this.hass.callService("input_text", "set_value", {
|
||||
value: this._value,
|
||||
entity_id: this._stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("hui-input-text-entity-row", HuiInputTextEntityRow);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
|
||||
class HuiInputTextEntityRow extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.inputTextControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get inputTextControlTemplate() {
|
||||
return html`
|
||||
<paper-input
|
||||
no-label-float
|
||||
minlength="[[_stateObj.attributes.min]]"
|
||||
maxlength="[[_stateObj.attributes.max]]"
|
||||
value="{{_value}}"
|
||||
auto-validate="[[_stateObj.attributes.pattern]]"
|
||||
pattern="[[_stateObj.attributes.pattern]]"
|
||||
type="[[_stateObj.attributes.mode]]"
|
||||
on-change="_selectedValueChanged"
|
||||
placeholder="(empty value)"
|
||||
></paper-input>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
observer: "_stateObjChanged",
|
||||
},
|
||||
_value: String,
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_stateObjChanged(stateObj) {
|
||||
this._value = stateObj && stateObj.state;
|
||||
}
|
||||
|
||||
_selectedValueChanged() {
|
||||
if (this._value === this._stateObj.state) {
|
||||
return;
|
||||
}
|
||||
this.hass.callService("input_text", "set_value", {
|
||||
value: this._value,
|
||||
entity_id: this._stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("hui-input-text-entity-row", HuiInputTextEntityRow);
|
||||
|
|
|
@ -1,83 +1,83 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiLockEntityRow extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.lockControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
margin-right: -.57em;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get lockControlTemplate() {
|
||||
return html`
|
||||
<paper-button on-click="_callService">
|
||||
[[_computeButtonTitle(_stateObj.state)]]
|
||||
</paper-button>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_computeButtonTitle(state) {
|
||||
return state === "locked"
|
||||
? this.localize("ui.card.lock.unlock")
|
||||
: this.localize("ui.card.lock.lock");
|
||||
}
|
||||
|
||||
_callService(ev) {
|
||||
ev.stopPropagation();
|
||||
const stateObj = this._stateObj;
|
||||
this.hass.callService(
|
||||
"lock",
|
||||
stateObj.state === "locked" ? "unlock" : "lock",
|
||||
{ entity_id: stateObj.entity_id }
|
||||
);
|
||||
}
|
||||
}
|
||||
customElements.define("hui-lock-entity-row", HuiLockEntityRow);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiLockEntityRow extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.lockControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
margin-right: -.57em;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get lockControlTemplate() {
|
||||
return html`
|
||||
<paper-button on-click="_callService">
|
||||
[[_computeButtonTitle(_stateObj.state)]]
|
||||
</paper-button>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_computeButtonTitle(state) {
|
||||
return state === "locked"
|
||||
? this.localize("ui.card.lock.unlock")
|
||||
: this.localize("ui.card.lock.lock");
|
||||
}
|
||||
|
||||
_callService(ev) {
|
||||
ev.stopPropagation();
|
||||
const stateObj = this._stateObj;
|
||||
this.hass.callService(
|
||||
"lock",
|
||||
stateObj.state === "locked" ? "unlock" : "lock",
|
||||
{ entity_id: stateObj.entity_id }
|
||||
);
|
||||
}
|
||||
}
|
||||
customElements.define("hui-lock-entity-row", HuiLockEntityRow);
|
||||
|
|
|
@ -1,162 +1,162 @@
|
|||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
const SUPPORT_PAUSE = 1;
|
||||
const SUPPORT_NEXT_TRACK = 32;
|
||||
const SUPPORTS_PLAY = 16384;
|
||||
const OFF_STATES = ["off", "idle"];
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiMediaPlayerEntityRow extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
show-secondary="false"
|
||||
>
|
||||
${this.mediaPlayerControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
.controls {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get mediaPlayerControlTemplate() {
|
||||
return html`
|
||||
<template is="dom-if" if="[[!_isOff(_stateObj.state)]]">
|
||||
<div class="controls">
|
||||
<template is="dom-if" if="[[_computeControlIcon(_stateObj)]]">
|
||||
<paper-icon-button
|
||||
icon="[[_computeControlIcon(_stateObj)]]"
|
||||
on-click="_playPause"
|
||||
></paper-icon-button>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_supportsNext(_stateObj)]]">
|
||||
<paper-icon-button
|
||||
icon="hass:skip-next"
|
||||
on-click="_nextTrack"
|
||||
></paper-icon-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_isOff(_stateObj.state)]]">
|
||||
<div>[[_computeState(_stateObj.state)]]</div>
|
||||
</template>
|
||||
|
||||
<div slot="secondary">
|
||||
[[_computeMediaTitle(_stateObj)]]
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_computeControlIcon(stateObj) {
|
||||
if (!stateObj) return null;
|
||||
|
||||
if (stateObj.state !== "playing") {
|
||||
return stateObj.attributes.supported_features & SUPPORTS_PLAY
|
||||
? "hass:play"
|
||||
: "";
|
||||
}
|
||||
|
||||
return stateObj.attributes.supported_features & SUPPORT_PAUSE
|
||||
? "hass:pause"
|
||||
: "hass:stop";
|
||||
}
|
||||
|
||||
_computeMediaTitle(stateObj) {
|
||||
if (!stateObj || this._isOff(stateObj.state)) return null;
|
||||
|
||||
switch (stateObj.attributes.media_content_type) {
|
||||
case "music":
|
||||
return `${stateObj.attributes.media_artist}: ${
|
||||
stateObj.attributes.media_title
|
||||
}`;
|
||||
case "tvshow":
|
||||
return `${stateObj.attributes.media_series_title}: ${
|
||||
stateObj.attributes.media_title
|
||||
}`;
|
||||
default:
|
||||
return (
|
||||
stateObj.attributes.media_title ||
|
||||
stateObj.attributes.app_name ||
|
||||
stateObj.state
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_computeState(state) {
|
||||
return (
|
||||
this.localize(`state.media_player.${state}`) ||
|
||||
this.localize(`state.default.${state}`) ||
|
||||
state
|
||||
);
|
||||
}
|
||||
|
||||
_callService(service) {
|
||||
this.hass.callService("media_player", service, {
|
||||
entity_id: this._config.entity,
|
||||
});
|
||||
}
|
||||
|
||||
_playPause(event) {
|
||||
event.stopPropagation();
|
||||
this._callService("media_play_pause");
|
||||
}
|
||||
|
||||
_nextTrack(event) {
|
||||
event.stopPropagation();
|
||||
if (this._stateObj.attributes.supported_features & SUPPORT_NEXT_TRACK) {
|
||||
this._callService("media_next_track");
|
||||
}
|
||||
}
|
||||
|
||||
_isOff(state) {
|
||||
return OFF_STATES.includes(state);
|
||||
}
|
||||
|
||||
_supportsNext(stateObj) {
|
||||
return (
|
||||
stateObj && stateObj.attributes.supported_features & SUPPORT_NEXT_TRACK
|
||||
);
|
||||
}
|
||||
}
|
||||
customElements.define("hui-media-player-entity-row", HuiMediaPlayerEntityRow);
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
const SUPPORT_PAUSE = 1;
|
||||
const SUPPORT_NEXT_TRACK = 32;
|
||||
const SUPPORTS_PLAY = 16384;
|
||||
const OFF_STATES = ["off", "idle"];
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiMediaPlayerEntityRow extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
show-secondary="false"
|
||||
>
|
||||
${this.mediaPlayerControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
.controls {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get mediaPlayerControlTemplate() {
|
||||
return html`
|
||||
<template is="dom-if" if="[[!_isOff(_stateObj.state)]]">
|
||||
<div class="controls">
|
||||
<template is="dom-if" if="[[_computeControlIcon(_stateObj)]]">
|
||||
<paper-icon-button
|
||||
icon="[[_computeControlIcon(_stateObj)]]"
|
||||
on-click="_playPause"
|
||||
></paper-icon-button>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_supportsNext(_stateObj)]]">
|
||||
<paper-icon-button
|
||||
icon="hass:skip-next"
|
||||
on-click="_nextTrack"
|
||||
></paper-icon-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_isOff(_stateObj.state)]]">
|
||||
<div>[[_computeState(_stateObj.state)]]</div>
|
||||
</template>
|
||||
|
||||
<div slot="secondary">
|
||||
[[_computeMediaTitle(_stateObj)]]
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_computeControlIcon(stateObj) {
|
||||
if (!stateObj) return null;
|
||||
|
||||
if (stateObj.state !== "playing") {
|
||||
return stateObj.attributes.supported_features & SUPPORTS_PLAY
|
||||
? "hass:play"
|
||||
: "";
|
||||
}
|
||||
|
||||
return stateObj.attributes.supported_features & SUPPORT_PAUSE
|
||||
? "hass:pause"
|
||||
: "hass:stop";
|
||||
}
|
||||
|
||||
_computeMediaTitle(stateObj) {
|
||||
if (!stateObj || this._isOff(stateObj.state)) return null;
|
||||
|
||||
switch (stateObj.attributes.media_content_type) {
|
||||
case "music":
|
||||
return `${stateObj.attributes.media_artist}: ${
|
||||
stateObj.attributes.media_title
|
||||
}`;
|
||||
case "tvshow":
|
||||
return `${stateObj.attributes.media_series_title}: ${
|
||||
stateObj.attributes.media_title
|
||||
}`;
|
||||
default:
|
||||
return (
|
||||
stateObj.attributes.media_title ||
|
||||
stateObj.attributes.app_name ||
|
||||
stateObj.state
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_computeState(state) {
|
||||
return (
|
||||
this.localize(`state.media_player.${state}`) ||
|
||||
this.localize(`state.default.${state}`) ||
|
||||
state
|
||||
);
|
||||
}
|
||||
|
||||
_callService(service) {
|
||||
this.hass.callService("media_player", service, {
|
||||
entity_id: this._config.entity,
|
||||
});
|
||||
}
|
||||
|
||||
_playPause(event) {
|
||||
event.stopPropagation();
|
||||
this._callService("media_play_pause");
|
||||
}
|
||||
|
||||
_nextTrack(event) {
|
||||
event.stopPropagation();
|
||||
if (this._stateObj.attributes.supported_features & SUPPORT_NEXT_TRACK) {
|
||||
this._callService("media_next_track");
|
||||
}
|
||||
}
|
||||
|
||||
_isOff(state) {
|
||||
return OFF_STATES.includes(state);
|
||||
}
|
||||
|
||||
_supportsNext(stateObj) {
|
||||
return (
|
||||
stateObj && stateObj.attributes.supported_features & SUPPORT_NEXT_TRACK
|
||||
);
|
||||
}
|
||||
}
|
||||
customElements.define("hui-media-player-entity-row", HuiMediaPlayerEntityRow);
|
||||
|
|
|
@ -1,70 +1,70 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiSceneEntityRow extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.sceneControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
margin-right: -.57em;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get sceneControlTemplate() {
|
||||
return html`
|
||||
<paper-button on-click="_callService">
|
||||
[[localize('ui.card.scene.activate')]]
|
||||
</paper-button>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_callService(ev) {
|
||||
ev.stopPropagation();
|
||||
this.hass.callService("scene", "turn_on", {
|
||||
entity_id: this._config.entity,
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("hui-scene-entity-row", HuiSceneEntityRow);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiSceneEntityRow extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.sceneControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
margin-right: -.57em;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get sceneControlTemplate() {
|
||||
return html`
|
||||
<paper-button on-click="_callService">
|
||||
[[localize('ui.card.scene.activate')]]
|
||||
</paper-button>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_callService(ev) {
|
||||
ev.stopPropagation();
|
||||
this.hass.callService("scene", "turn_on", {
|
||||
entity_id: this._config.entity,
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("hui-scene-entity-row", HuiSceneEntityRow);
|
||||
|
|
|
@ -1,78 +1,78 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiScriptEntityRow extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.scriptControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
margin-right: -.57em;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get scriptControlTemplate() {
|
||||
return html`
|
||||
<template is="dom-if" if="[[_stateObj.attributes.can_cancel]]">
|
||||
<ha-entity-toggle state-obj="[[_stateObj]]" hass="[[hass]]"></ha-entity-toggle>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_stateObj.attributes.can_cancel]]">
|
||||
<paper-button on-click="_callService">[[localize('ui.card.script.execute')]]</paper-button>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_callService(ev) {
|
||||
ev.stopPropagation();
|
||||
this.hass.callService("script", "turn_on", {
|
||||
entity_id: this._config.entity,
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("hui-script-entity-row", HuiScriptEntityRow);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiScriptEntityRow extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.scriptControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
margin-right: -.57em;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get scriptControlTemplate() {
|
||||
return html`
|
||||
<template is="dom-if" if="[[_stateObj.attributes.can_cancel]]">
|
||||
<ha-entity-toggle state-obj="[[_stateObj]]" hass="[[hass]]"></ha-entity-toggle>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_stateObj.attributes.can_cancel]]">
|
||||
<paper-button on-click="_callService">[[localize('ui.card.script.execute')]]</paper-button>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_callService(ev) {
|
||||
ev.stopPropagation();
|
||||
this.hass.callService("script", "turn_on", {
|
||||
entity_id: this._config.entity,
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("hui-script-entity-row", HuiScriptEntityRow);
|
||||
|
|
|
@ -1,70 +1,70 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiTextEntityRow extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.textControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
div {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get textControlTemplate() {
|
||||
return html`
|
||||
<div>
|
||||
[[_computeState(_stateObj)]]
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_computeState(stateObj) {
|
||||
return stateObj && computeStateDisplay(this.localize, stateObj);
|
||||
}
|
||||
}
|
||||
customElements.define("hui-text-entity-row", HuiTextEntityRow);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiTextEntityRow extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate}
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.textControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
div {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get textControlTemplate() {
|
||||
return html`
|
||||
<div>
|
||||
[[_computeState(_stateObj)]]
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
_computeState(stateObj) {
|
||||
return stateObj && computeStateDisplay(this.localize, stateObj);
|
||||
}
|
||||
}
|
||||
customElements.define("hui-text-entity-row", HuiTextEntityRow);
|
||||
|
|
|
@ -1,103 +1,103 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
|
||||
import timerTimeRemaining from "../../../common/entity/timer_time_remaining";
|
||||
import secondsToDuration from "../../../common/datetime/seconds_to_duration";
|
||||
|
||||
class HuiTimerEntityRow extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.timerControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get timerControlTemplate() {
|
||||
return html`
|
||||
<div>
|
||||
[[_computeDisplay(_stateObj, _timeRemaining)]]
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
observer: "_stateObjChanged",
|
||||
},
|
||||
_timeRemaining: Number,
|
||||
};
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._clearInterval();
|
||||
}
|
||||
|
||||
_stateObjChanged(stateObj) {
|
||||
if (stateObj) {
|
||||
this._startInterval(stateObj);
|
||||
} else {
|
||||
this._clearInterval();
|
||||
}
|
||||
}
|
||||
|
||||
_clearInterval() {
|
||||
if (this._updateRemaining) {
|
||||
clearInterval(this._updateRemaining);
|
||||
this._updateRemaining = null;
|
||||
}
|
||||
}
|
||||
|
||||
_startInterval(stateObj) {
|
||||
this._clearInterval();
|
||||
this._calculateRemaining(stateObj);
|
||||
|
||||
if (stateObj.state === "active") {
|
||||
this._updateRemaining = setInterval(
|
||||
() => this._calculateRemaining(this._stateObj),
|
||||
1000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_calculateRemaining(stateObj) {
|
||||
this._timeRemaining = timerTimeRemaining(stateObj);
|
||||
}
|
||||
|
||||
_computeDisplay(stateObj, time) {
|
||||
if (!stateObj) return null;
|
||||
|
||||
if (stateObj.state === "idle" || time === 0) return stateObj.state;
|
||||
|
||||
let display = secondsToDuration(time);
|
||||
|
||||
if (stateObj.state === "paused") {
|
||||
display += " (paused)";
|
||||
}
|
||||
|
||||
return display;
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
}
|
||||
customElements.define("hui-timer-entity-row", HuiTimerEntityRow);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
|
||||
import timerTimeRemaining from "../../../common/entity/timer_time_remaining";
|
||||
import secondsToDuration from "../../../common/datetime/seconds_to_duration";
|
||||
|
||||
class HuiTimerEntityRow extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.timerControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get timerControlTemplate() {
|
||||
return html`
|
||||
<div>
|
||||
[[_computeDisplay(_stateObj, _timeRemaining)]]
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
observer: "_stateObjChanged",
|
||||
},
|
||||
_timeRemaining: Number,
|
||||
};
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._clearInterval();
|
||||
}
|
||||
|
||||
_stateObjChanged(stateObj) {
|
||||
if (stateObj) {
|
||||
this._startInterval(stateObj);
|
||||
} else {
|
||||
this._clearInterval();
|
||||
}
|
||||
}
|
||||
|
||||
_clearInterval() {
|
||||
if (this._updateRemaining) {
|
||||
clearInterval(this._updateRemaining);
|
||||
this._updateRemaining = null;
|
||||
}
|
||||
}
|
||||
|
||||
_startInterval(stateObj) {
|
||||
this._clearInterval();
|
||||
this._calculateRemaining(stateObj);
|
||||
|
||||
if (stateObj.state === "active") {
|
||||
this._updateRemaining = setInterval(
|
||||
() => this._calculateRemaining(this._stateObj),
|
||||
1000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_calculateRemaining(stateObj) {
|
||||
this._timeRemaining = timerTimeRemaining(stateObj);
|
||||
}
|
||||
|
||||
_computeDisplay(stateObj, time) {
|
||||
if (!stateObj) return null;
|
||||
|
||||
if (stateObj.state === "idle" || time === 0) return stateObj.state;
|
||||
|
||||
let display = secondsToDuration(time);
|
||||
|
||||
if (stateObj.state === "paused") {
|
||||
display += " (paused)";
|
||||
}
|
||||
|
||||
return display;
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
}
|
||||
customElements.define("hui-timer-entity-row", HuiTimerEntityRow);
|
||||
|
|
|
@ -1,76 +1,76 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiToggleEntityRow extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.toggleControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get toggleControlTemplate() {
|
||||
return html`
|
||||
<template is="dom-if" if="[[_canToggle]]">
|
||||
<ha-entity-toggle
|
||||
hass="[[hass]]"
|
||||
state-obj="[[_stateObj]]"
|
||||
></ha-entity-toggle>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_canToggle]]">
|
||||
<div>
|
||||
[[_computeState(_stateObj)]]
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
_canToggle: {
|
||||
type: Boolean,
|
||||
computed: "_computeCanToggle(_stateObj.state)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
_computeCanToggle(state) {
|
||||
return state === "on" || state === "off";
|
||||
}
|
||||
|
||||
_computeState(stateObj) {
|
||||
return stateObj && computeStateDisplay(this.localize, stateObj);
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
}
|
||||
customElements.define("hui-toggle-entity-row", HuiToggleEntityRow);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/hui-generic-entity-row";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HuiToggleEntityRow extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<hui-generic-entity-row
|
||||
hass="[[hass]]"
|
||||
config="[[_config]]"
|
||||
>
|
||||
${this.toggleControlTemplate}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get toggleControlTemplate() {
|
||||
return html`
|
||||
<template is="dom-if" if="[[_canToggle]]">
|
||||
<ha-entity-toggle
|
||||
hass="[[hass]]"
|
||||
state-obj="[[_stateObj]]"
|
||||
></ha-entity-toggle>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_canToggle]]">
|
||||
<div>
|
||||
[[_computeState(_stateObj)]]
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_config: Object,
|
||||
_stateObj: {
|
||||
type: Object,
|
||||
computed: "_computeStateObj(hass.states, _config.entity)",
|
||||
},
|
||||
_canToggle: {
|
||||
type: Boolean,
|
||||
computed: "_computeCanToggle(_stateObj.state)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeStateObj(states, entityId) {
|
||||
return states && entityId in states ? states[entityId] : null;
|
||||
}
|
||||
|
||||
_computeCanToggle(state) {
|
||||
return state === "on" || state === "off";
|
||||
}
|
||||
|
||||
_computeState(stateObj) {
|
||||
return stateObj && computeStateDisplay(this.localize, stateObj);
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Entity not configured.");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
}
|
||||
customElements.define("hui-toggle-entity-row", HuiToggleEntityRow);
|
||||
|
|
|
@ -1,125 +1,125 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
|
||||
import "../../layouts/hass-loading-screen";
|
||||
import "../../layouts/hass-error-screen";
|
||||
import "./hui-root";
|
||||
|
||||
class Lovelace extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
<template is='dom-if' if='[[_equal(_state, "loaded")]]' restamp>
|
||||
<hui-root
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
hass='[[hass]]'
|
||||
route="[[route]]"
|
||||
config='[[_config]]'
|
||||
columns='[[_columns]]'
|
||||
on-config-refresh='_fetchConfig'
|
||||
></hui-root>
|
||||
</template>
|
||||
<template is='dom-if' if='[[_equal(_state, "loading")]]' restamp>
|
||||
<hass-loading-screen
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
></hass-loading-screen>
|
||||
</template>
|
||||
<template is='dom-if' if='[[_equal(_state, "error")]]' restamp>
|
||||
<hass-error-screen
|
||||
title='Lovelace'
|
||||
error='[[_errorMsg]]'
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
>
|
||||
<paper-button on-click="_fetchConfig">Reload ui-lovelace.yaml</paper-button>
|
||||
</hass-error-screen>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
route: Object,
|
||||
|
||||
_columns: {
|
||||
type: Number,
|
||||
value: 1,
|
||||
},
|
||||
|
||||
_state: {
|
||||
type: String,
|
||||
value: "loading",
|
||||
},
|
||||
|
||||
_errorMsg: String,
|
||||
|
||||
_config: {
|
||||
type: Object,
|
||||
value: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return ["_updateColumns(narrow, showMenu)"];
|
||||
}
|
||||
|
||||
ready() {
|
||||
this._fetchConfig();
|
||||
this._updateColumns = this._updateColumns.bind(this);
|
||||
this.mqls = [300, 600, 900, 1200].map((width) => {
|
||||
const mql = matchMedia(`(min-width: ${width}px)`);
|
||||
mql.addListener(this._updateColumns);
|
||||
return mql;
|
||||
});
|
||||
this._updateColumns();
|
||||
super.ready();
|
||||
}
|
||||
|
||||
_updateColumns() {
|
||||
const matchColumns = this.mqls.reduce((cols, mql) => cols + mql.matches, 0);
|
||||
// Do -1 column if the menu is docked and open
|
||||
this._columns = Math.max(1, matchColumns - (!this.narrow && this.showMenu));
|
||||
}
|
||||
|
||||
async _fetchConfig() {
|
||||
try {
|
||||
const conf = await this.hass.callWS({ type: "lovelace/config" });
|
||||
this.setProperties({
|
||||
_config: conf,
|
||||
_state: "loaded",
|
||||
});
|
||||
} catch (err) {
|
||||
this.setProperties({
|
||||
_state: "error",
|
||||
_errorMsg: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_equal(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-panel-lovelace", Lovelace);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
|
||||
import "../../layouts/hass-loading-screen";
|
||||
import "../../layouts/hass-error-screen";
|
||||
import "./hui-root";
|
||||
|
||||
class Lovelace extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
<template is='dom-if' if='[[_equal(_state, "loaded")]]' restamp>
|
||||
<hui-root
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
hass='[[hass]]'
|
||||
route="[[route]]"
|
||||
config='[[_config]]'
|
||||
columns='[[_columns]]'
|
||||
on-config-refresh='_fetchConfig'
|
||||
></hui-root>
|
||||
</template>
|
||||
<template is='dom-if' if='[[_equal(_state, "loading")]]' restamp>
|
||||
<hass-loading-screen
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
></hass-loading-screen>
|
||||
</template>
|
||||
<template is='dom-if' if='[[_equal(_state, "error")]]' restamp>
|
||||
<hass-error-screen
|
||||
title='Lovelace'
|
||||
error='[[_errorMsg]]'
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
>
|
||||
<paper-button on-click="_fetchConfig">Reload ui-lovelace.yaml</paper-button>
|
||||
</hass-error-screen>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
route: Object,
|
||||
|
||||
_columns: {
|
||||
type: Number,
|
||||
value: 1,
|
||||
},
|
||||
|
||||
_state: {
|
||||
type: String,
|
||||
value: "loading",
|
||||
},
|
||||
|
||||
_errorMsg: String,
|
||||
|
||||
_config: {
|
||||
type: Object,
|
||||
value: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return ["_updateColumns(narrow, showMenu)"];
|
||||
}
|
||||
|
||||
ready() {
|
||||
this._fetchConfig();
|
||||
this._updateColumns = this._updateColumns.bind(this);
|
||||
this.mqls = [300, 600, 900, 1200].map((width) => {
|
||||
const mql = matchMedia(`(min-width: ${width}px)`);
|
||||
mql.addListener(this._updateColumns);
|
||||
return mql;
|
||||
});
|
||||
this._updateColumns();
|
||||
super.ready();
|
||||
}
|
||||
|
||||
_updateColumns() {
|
||||
const matchColumns = this.mqls.reduce((cols, mql) => cols + mql.matches, 0);
|
||||
// Do -1 column if the menu is docked and open
|
||||
this._columns = Math.max(1, matchColumns - (!this.narrow && this.showMenu));
|
||||
}
|
||||
|
||||
async _fetchConfig() {
|
||||
try {
|
||||
const conf = await this.hass.callWS({ type: "lovelace/config" });
|
||||
this.setProperties({
|
||||
_config: conf,
|
||||
_state: "loaded",
|
||||
});
|
||||
} catch (err) {
|
||||
this.setProperties({
|
||||
_state: "error",
|
||||
_errorMsg: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_equal(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-panel-lovelace", Lovelace);
|
||||
|
|
|
@ -1,380 +1,380 @@
|
|||
import "@polymer/app-layout/app-header-layout/app-header-layout";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-scroll-effects/effects/waterfall";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/app-route/app-route";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-menu-button/paper-menu-button";
|
||||
import "@polymer/paper-tabs/paper-tab";
|
||||
import "@polymer/paper-tabs/paper-tabs";
|
||||
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import scrollToTarget from "../../common/dom/scroll-to-target";
|
||||
|
||||
import EventsMixin from "../../mixins/events-mixin";
|
||||
import NavigateMixin from "../../mixins/navigate-mixin";
|
||||
|
||||
import "../../layouts/ha-app-layout";
|
||||
import "../../components/ha-start-voice-button";
|
||||
import "../../components/ha-icon";
|
||||
import { loadModule, loadCSS, loadJS } from "../../common/dom/load_resource";
|
||||
import { subscribeNotifications } from "../../data/ws-notifications";
|
||||
import "./components/notifications/hui-notification-drawer";
|
||||
import "./components/notifications/hui-notifications-button";
|
||||
import "./hui-unused-entities";
|
||||
import "./hui-view";
|
||||
import debounce from "../../common/util/debounce";
|
||||
|
||||
import createCardElement from "./common/create-card-element";
|
||||
import computeNotifications from "./common/compute-notifications";
|
||||
|
||||
// CSS and JS should only be imported once. Modules and HTML are safe.
|
||||
const CSS_CACHE = {};
|
||||
const JS_CACHE = {};
|
||||
|
||||
class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include='ha-style'>
|
||||
:host {
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
ha-app-layout {
|
||||
min-height: 100%;
|
||||
}
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
--paper-tabs-selection-bar-color: var(--text-primary-color, #FFF);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
app-toolbar a {
|
||||
color: var(--text-primary-color, white);
|
||||
}
|
||||
#view {
|
||||
min-height: calc(100vh - 112px);
|
||||
/**
|
||||
* Since we only set min-height, if child nodes need percentage
|
||||
* heights they must use absolute positioning so we need relative
|
||||
* positioning here.
|
||||
*
|
||||
* https://www.w3.org/TR/CSS2/visudet.html#the-height-property
|
||||
*/
|
||||
position: relative;
|
||||
}
|
||||
#view.tabs-hidden {
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<app-route route="[[route]]" pattern="/:view" data="{{routeData}}"></app-route>
|
||||
<hui-notification-drawer
|
||||
hass="[[hass]]"
|
||||
notifications="[[_notifications]]"
|
||||
open="{{notificationsOpen}}"
|
||||
narrow="[[narrow]]"
|
||||
></hui-notification-drawer>
|
||||
<ha-app-layout id="layout">
|
||||
<app-header slot="header" effects="waterfall" fixed condenses>
|
||||
<template is='dom-if' if="[[!_editMode]]">
|
||||
<app-toolbar>
|
||||
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
|
||||
<div main-title>[[_computeTitle(config)]]</div>
|
||||
<hui-notifications-button
|
||||
hass="[[hass]]"
|
||||
notifications-open="{{notificationsOpen}}"
|
||||
notifications="[[_notifications]]"
|
||||
></hui-notifications-button>
|
||||
<ha-start-voice-button hass="[[hass]]"></ha-start-voice-button>
|
||||
<paper-menu-button
|
||||
no-animations
|
||||
horizontal-align="right"
|
||||
horizontal-offset="-5"
|
||||
>
|
||||
<paper-icon-button icon="hass:dots-vertical" slot="dropdown-trigger"></paper-icon-button>
|
||||
<paper-listbox on-iron-select="_deselect" slot="dropdown-content">
|
||||
<paper-item on-click="_handleRefresh">Refresh</paper-item>
|
||||
<paper-item on-click="_handleUnusedEntities">Unused entities</paper-item>
|
||||
<paper-item on-click="_editModeEnable">Configure UI</paper-item>
|
||||
<paper-item on-click="_handleHelp">Help</paper-item>
|
||||
</paper-listbox>
|
||||
</paper-menu-button>
|
||||
</app-toolbar>
|
||||
</template>
|
||||
<template is='dom-if' if="[[_editMode]]">
|
||||
<app-toolbar>
|
||||
<paper-icon-button
|
||||
icon='hass:close'
|
||||
on-click='_editModeDisable'
|
||||
></paper-icon-button>
|
||||
<div main-title>Edit UI</div>
|
||||
</app-toolbar>
|
||||
</template>
|
||||
|
||||
<div sticky hidden$="[[_computeTabsHidden(config.views)]]">
|
||||
<paper-tabs scrollable selected="[[_curView]]" on-iron-activate="_handleViewSelected">
|
||||
<template is="dom-repeat" items="[[config.views]]">
|
||||
<paper-tab>
|
||||
<template is="dom-if" if="[[item.icon]]">
|
||||
<ha-icon title$="[[item.title]]" icon="[[item.icon]]"></ha-icon>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!item.icon]]">
|
||||
[[_computeTabTitle(item.title)]]
|
||||
</template>
|
||||
</paper-tab>
|
||||
</template>
|
||||
</paper-tabs>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<div id='view' on-rebuild-view='_debouncedConfigChanged'></div>
|
||||
</app-header-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
narrow: Boolean,
|
||||
showMenu: Boolean,
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_hassChanged",
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
observer: "_configChanged",
|
||||
},
|
||||
columns: {
|
||||
type: Number,
|
||||
observer: "_columnsChanged",
|
||||
},
|
||||
|
||||
_curView: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
|
||||
route: {
|
||||
type: Object,
|
||||
observer: "_routeChanged",
|
||||
},
|
||||
|
||||
notificationsOpen: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
_persistentNotifications: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
|
||||
_notifications: {
|
||||
type: Array,
|
||||
computed: "_updateNotifications(hass.states, _persistentNotifications)",
|
||||
},
|
||||
|
||||
_editMode: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: "_editModeChanged",
|
||||
},
|
||||
|
||||
routeData: Object,
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._debouncedConfigChanged = debounce(
|
||||
() => this._selectView(this._curView),
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._unsubNotifications = subscribeNotifications(
|
||||
this.hass.connection,
|
||||
(notifications) => {
|
||||
this._persistentNotifications = notifications;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (typeof this._unsubNotifications === "function") {
|
||||
this._unsubNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
_updateNotifications(states, persistent) {
|
||||
if (!states) return persistent;
|
||||
|
||||
const configurator = computeNotifications(states);
|
||||
return persistent.concat(configurator);
|
||||
}
|
||||
|
||||
_routeChanged(route) {
|
||||
const views = this.config && this.config.views;
|
||||
if (route.path === "" && route.prefix === "/lovelace" && views) {
|
||||
this.navigate(`/lovelace/${views[0].id || 0}`, true);
|
||||
} else if (this.routeData.view) {
|
||||
const view = this.routeData.view;
|
||||
let index = 0;
|
||||
for (let i = 0; i < views.length; i++) {
|
||||
if (views[i].id === view || i === parseInt(view)) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index !== this._curView) this._selectView(index);
|
||||
}
|
||||
}
|
||||
|
||||
_computeViewId(id, index) {
|
||||
return id || index;
|
||||
}
|
||||
|
||||
_computeTitle(config) {
|
||||
return config.title || "Home Assistant";
|
||||
}
|
||||
|
||||
_computeTabsHidden(views) {
|
||||
return views.length < 2;
|
||||
}
|
||||
|
||||
_computeTabTitle(title) {
|
||||
return title || "Unnamed view";
|
||||
}
|
||||
|
||||
_handleRefresh() {
|
||||
this.fire("config-refresh");
|
||||
}
|
||||
|
||||
_handleUnusedEntities() {
|
||||
this._selectView("unused");
|
||||
}
|
||||
|
||||
_deselect(ev) {
|
||||
ev.target.selected = null;
|
||||
}
|
||||
|
||||
_handleHelp() {
|
||||
window.open("https://www.home-assistant.io/lovelace/", "_blank");
|
||||
}
|
||||
|
||||
_editModeEnable() {
|
||||
this._editMode = true;
|
||||
}
|
||||
|
||||
_editModeDisable() {
|
||||
this._editMode = false;
|
||||
}
|
||||
|
||||
_editModeChanged() {
|
||||
this._selectView(this._curView);
|
||||
}
|
||||
|
||||
_handleViewSelected(ev) {
|
||||
const index = ev.detail.selected;
|
||||
if (index !== this._curView) {
|
||||
const id = this.config.views[index].id || index;
|
||||
this.navigate(`/lovelace/${id}`);
|
||||
}
|
||||
scrollToTarget(this, this.$.layout.header.scrollTarget);
|
||||
}
|
||||
|
||||
_selectView(viewIndex) {
|
||||
this._curView = viewIndex;
|
||||
|
||||
// Recreate a new element to clear the applied themes.
|
||||
const root = this.$.view;
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
let view;
|
||||
let background = this.config.background || "";
|
||||
|
||||
if (viewIndex === "unused") {
|
||||
view = document.createElement("hui-unused-entities");
|
||||
view.config = this.config;
|
||||
} else {
|
||||
const viewConfig = this.config.views[this._curView];
|
||||
if (viewConfig.panel) {
|
||||
view = createCardElement(viewConfig.cards[0]);
|
||||
view.isPanel = true;
|
||||
view.editMode = this._editMode;
|
||||
} else {
|
||||
view = document.createElement("hui-view");
|
||||
view.config = viewConfig;
|
||||
view.columns = this.columns;
|
||||
view.editMode = this._editMode;
|
||||
}
|
||||
if (viewConfig.background) background = viewConfig.background;
|
||||
}
|
||||
|
||||
this.$.view.style.background = background;
|
||||
|
||||
view.hass = this.hass;
|
||||
root.appendChild(view);
|
||||
}
|
||||
|
||||
_hassChanged(hass) {
|
||||
if (!this.$.view.lastChild) return;
|
||||
this.$.view.lastChild.hass = hass;
|
||||
}
|
||||
|
||||
_configChanged(config) {
|
||||
this._loadResources(config.resources || []);
|
||||
// On config change, recreate the view from scratch.
|
||||
this._selectView(this._curView);
|
||||
this.$.view.classList.toggle("tabs-hidden", config.views.length < 2);
|
||||
}
|
||||
|
||||
_columnsChanged(columns) {
|
||||
if (!this.$.view.lastChild) return;
|
||||
this.$.view.lastChild.columns = columns;
|
||||
}
|
||||
|
||||
_loadResources(resources) {
|
||||
resources.forEach((resource) => {
|
||||
switch (resource.type) {
|
||||
case "css":
|
||||
if (resource.url in CSS_CACHE) break;
|
||||
CSS_CACHE[resource.url] = loadCSS(resource.url);
|
||||
break;
|
||||
|
||||
case "js":
|
||||
if (resource.url in JS_CACHE) break;
|
||||
JS_CACHE[resource.url] = loadJS(resource.url);
|
||||
break;
|
||||
|
||||
case "module":
|
||||
loadModule(resource.url);
|
||||
break;
|
||||
|
||||
case "html":
|
||||
import(/* webpackChunkName: "import-href-polyfill" */ "../../resources/html-import/import-href").then(
|
||||
({ importHref }) => importHref(resource.url)
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
// eslint-disable-next-line
|
||||
console.warn("Unknown resource type specified: ${resource.type}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("hui-root", HUIRoot);
|
||||
import "@polymer/app-layout/app-header-layout/app-header-layout";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-scroll-effects/effects/waterfall";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/app-route/app-route";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-menu-button/paper-menu-button";
|
||||
import "@polymer/paper-tabs/paper-tab";
|
||||
import "@polymer/paper-tabs/paper-tabs";
|
||||
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import scrollToTarget from "../../common/dom/scroll-to-target";
|
||||
|
||||
import EventsMixin from "../../mixins/events-mixin";
|
||||
import NavigateMixin from "../../mixins/navigate-mixin";
|
||||
|
||||
import "../../layouts/ha-app-layout";
|
||||
import "../../components/ha-start-voice-button";
|
||||
import "../../components/ha-icon";
|
||||
import { loadModule, loadCSS, loadJS } from "../../common/dom/load_resource";
|
||||
import { subscribeNotifications } from "../../data/ws-notifications";
|
||||
import "./components/notifications/hui-notification-drawer";
|
||||
import "./components/notifications/hui-notifications-button";
|
||||
import "./hui-unused-entities";
|
||||
import "./hui-view";
|
||||
import debounce from "../../common/util/debounce";
|
||||
|
||||
import createCardElement from "./common/create-card-element";
|
||||
import computeNotifications from "./common/compute-notifications";
|
||||
|
||||
// CSS and JS should only be imported once. Modules and HTML are safe.
|
||||
const CSS_CACHE = {};
|
||||
const JS_CACHE = {};
|
||||
|
||||
class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include='ha-style'>
|
||||
:host {
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
ha-app-layout {
|
||||
min-height: 100%;
|
||||
}
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
--paper-tabs-selection-bar-color: var(--text-primary-color, #FFF);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
app-toolbar a {
|
||||
color: var(--text-primary-color, white);
|
||||
}
|
||||
#view {
|
||||
min-height: calc(100vh - 112px);
|
||||
/**
|
||||
* Since we only set min-height, if child nodes need percentage
|
||||
* heights they must use absolute positioning so we need relative
|
||||
* positioning here.
|
||||
*
|
||||
* https://www.w3.org/TR/CSS2/visudet.html#the-height-property
|
||||
*/
|
||||
position: relative;
|
||||
}
|
||||
#view.tabs-hidden {
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<app-route route="[[route]]" pattern="/:view" data="{{routeData}}"></app-route>
|
||||
<hui-notification-drawer
|
||||
hass="[[hass]]"
|
||||
notifications="[[_notifications]]"
|
||||
open="{{notificationsOpen}}"
|
||||
narrow="[[narrow]]"
|
||||
></hui-notification-drawer>
|
||||
<ha-app-layout id="layout">
|
||||
<app-header slot="header" effects="waterfall" fixed condenses>
|
||||
<template is='dom-if' if="[[!_editMode]]">
|
||||
<app-toolbar>
|
||||
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
|
||||
<div main-title>[[_computeTitle(config)]]</div>
|
||||
<hui-notifications-button
|
||||
hass="[[hass]]"
|
||||
notifications-open="{{notificationsOpen}}"
|
||||
notifications="[[_notifications]]"
|
||||
></hui-notifications-button>
|
||||
<ha-start-voice-button hass="[[hass]]"></ha-start-voice-button>
|
||||
<paper-menu-button
|
||||
no-animations
|
||||
horizontal-align="right"
|
||||
horizontal-offset="-5"
|
||||
>
|
||||
<paper-icon-button icon="hass:dots-vertical" slot="dropdown-trigger"></paper-icon-button>
|
||||
<paper-listbox on-iron-select="_deselect" slot="dropdown-content">
|
||||
<paper-item on-click="_handleRefresh">Refresh</paper-item>
|
||||
<paper-item on-click="_handleUnusedEntities">Unused entities</paper-item>
|
||||
<paper-item on-click="_editModeEnable">Configure UI</paper-item>
|
||||
<paper-item on-click="_handleHelp">Help</paper-item>
|
||||
</paper-listbox>
|
||||
</paper-menu-button>
|
||||
</app-toolbar>
|
||||
</template>
|
||||
<template is='dom-if' if="[[_editMode]]">
|
||||
<app-toolbar>
|
||||
<paper-icon-button
|
||||
icon='hass:close'
|
||||
on-click='_editModeDisable'
|
||||
></paper-icon-button>
|
||||
<div main-title>Edit UI</div>
|
||||
</app-toolbar>
|
||||
</template>
|
||||
|
||||
<div sticky hidden$="[[_computeTabsHidden(config.views)]]">
|
||||
<paper-tabs scrollable selected="[[_curView]]" on-iron-activate="_handleViewSelected">
|
||||
<template is="dom-repeat" items="[[config.views]]">
|
||||
<paper-tab>
|
||||
<template is="dom-if" if="[[item.icon]]">
|
||||
<ha-icon title$="[[item.title]]" icon="[[item.icon]]"></ha-icon>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!item.icon]]">
|
||||
[[_computeTabTitle(item.title)]]
|
||||
</template>
|
||||
</paper-tab>
|
||||
</template>
|
||||
</paper-tabs>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<div id='view' on-rebuild-view='_debouncedConfigChanged'></div>
|
||||
</app-header-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
narrow: Boolean,
|
||||
showMenu: Boolean,
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_hassChanged",
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
observer: "_configChanged",
|
||||
},
|
||||
columns: {
|
||||
type: Number,
|
||||
observer: "_columnsChanged",
|
||||
},
|
||||
|
||||
_curView: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
|
||||
route: {
|
||||
type: Object,
|
||||
observer: "_routeChanged",
|
||||
},
|
||||
|
||||
notificationsOpen: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
_persistentNotifications: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
|
||||
_notifications: {
|
||||
type: Array,
|
||||
computed: "_updateNotifications(hass.states, _persistentNotifications)",
|
||||
},
|
||||
|
||||
_editMode: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: "_editModeChanged",
|
||||
},
|
||||
|
||||
routeData: Object,
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._debouncedConfigChanged = debounce(
|
||||
() => this._selectView(this._curView),
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._unsubNotifications = subscribeNotifications(
|
||||
this.hass.connection,
|
||||
(notifications) => {
|
||||
this._persistentNotifications = notifications;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (typeof this._unsubNotifications === "function") {
|
||||
this._unsubNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
_updateNotifications(states, persistent) {
|
||||
if (!states) return persistent;
|
||||
|
||||
const configurator = computeNotifications(states);
|
||||
return persistent.concat(configurator);
|
||||
}
|
||||
|
||||
_routeChanged(route) {
|
||||
const views = this.config && this.config.views;
|
||||
if (route.path === "" && route.prefix === "/lovelace" && views) {
|
||||
this.navigate(`/lovelace/${views[0].id || 0}`, true);
|
||||
} else if (this.routeData.view) {
|
||||
const view = this.routeData.view;
|
||||
let index = 0;
|
||||
for (let i = 0; i < views.length; i++) {
|
||||
if (views[i].id === view || i === parseInt(view)) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index !== this._curView) this._selectView(index);
|
||||
}
|
||||
}
|
||||
|
||||
_computeViewId(id, index) {
|
||||
return id || index;
|
||||
}
|
||||
|
||||
_computeTitle(config) {
|
||||
return config.title || "Home Assistant";
|
||||
}
|
||||
|
||||
_computeTabsHidden(views) {
|
||||
return views.length < 2;
|
||||
}
|
||||
|
||||
_computeTabTitle(title) {
|
||||
return title || "Unnamed view";
|
||||
}
|
||||
|
||||
_handleRefresh() {
|
||||
this.fire("config-refresh");
|
||||
}
|
||||
|
||||
_handleUnusedEntities() {
|
||||
this._selectView("unused");
|
||||
}
|
||||
|
||||
_deselect(ev) {
|
||||
ev.target.selected = null;
|
||||
}
|
||||
|
||||
_handleHelp() {
|
||||
window.open("https://www.home-assistant.io/lovelace/", "_blank");
|
||||
}
|
||||
|
||||
_editModeEnable() {
|
||||
this._editMode = true;
|
||||
}
|
||||
|
||||
_editModeDisable() {
|
||||
this._editMode = false;
|
||||
}
|
||||
|
||||
_editModeChanged() {
|
||||
this._selectView(this._curView);
|
||||
}
|
||||
|
||||
_handleViewSelected(ev) {
|
||||
const index = ev.detail.selected;
|
||||
if (index !== this._curView) {
|
||||
const id = this.config.views[index].id || index;
|
||||
this.navigate(`/lovelace/${id}`);
|
||||
}
|
||||
scrollToTarget(this, this.$.layout.header.scrollTarget);
|
||||
}
|
||||
|
||||
_selectView(viewIndex) {
|
||||
this._curView = viewIndex;
|
||||
|
||||
// Recreate a new element to clear the applied themes.
|
||||
const root = this.$.view;
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
let view;
|
||||
let background = this.config.background || "";
|
||||
|
||||
if (viewIndex === "unused") {
|
||||
view = document.createElement("hui-unused-entities");
|
||||
view.config = this.config;
|
||||
} else {
|
||||
const viewConfig = this.config.views[this._curView];
|
||||
if (viewConfig.panel) {
|
||||
view = createCardElement(viewConfig.cards[0]);
|
||||
view.isPanel = true;
|
||||
view.editMode = this._editMode;
|
||||
} else {
|
||||
view = document.createElement("hui-view");
|
||||
view.config = viewConfig;
|
||||
view.columns = this.columns;
|
||||
view.editMode = this._editMode;
|
||||
}
|
||||
if (viewConfig.background) background = viewConfig.background;
|
||||
}
|
||||
|
||||
this.$.view.style.background = background;
|
||||
|
||||
view.hass = this.hass;
|
||||
root.appendChild(view);
|
||||
}
|
||||
|
||||
_hassChanged(hass) {
|
||||
if (!this.$.view.lastChild) return;
|
||||
this.$.view.lastChild.hass = hass;
|
||||
}
|
||||
|
||||
_configChanged(config) {
|
||||
this._loadResources(config.resources || []);
|
||||
// On config change, recreate the view from scratch.
|
||||
this._selectView(this._curView);
|
||||
this.$.view.classList.toggle("tabs-hidden", config.views.length < 2);
|
||||
}
|
||||
|
||||
_columnsChanged(columns) {
|
||||
if (!this.$.view.lastChild) return;
|
||||
this.$.view.lastChild.columns = columns;
|
||||
}
|
||||
|
||||
_loadResources(resources) {
|
||||
resources.forEach((resource) => {
|
||||
switch (resource.type) {
|
||||
case "css":
|
||||
if (resource.url in CSS_CACHE) break;
|
||||
CSS_CACHE[resource.url] = loadCSS(resource.url);
|
||||
break;
|
||||
|
||||
case "js":
|
||||
if (resource.url in JS_CACHE) break;
|
||||
JS_CACHE[resource.url] = loadJS(resource.url);
|
||||
break;
|
||||
|
||||
case "module":
|
||||
loadModule(resource.url);
|
||||
break;
|
||||
|
||||
case "html":
|
||||
import(/* webpackChunkName: "import-href-polyfill" */ "../../resources/html-import/import-href").then(
|
||||
({ importHref }) => importHref(resource.url)
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
// eslint-disable-next-line
|
||||
console.warn("Unknown resource type specified: ${resource.type}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("hui-root", HUIRoot);
|
||||
|
|
|
@ -1,61 +1,61 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import computeUnusedEntities from "./common/compute-unused-entities";
|
||||
import createCardElement from "./common/create-card-element";
|
||||
|
||||
import "./cards/hui-entities-card.ts";
|
||||
|
||||
class HuiUnusedEntities extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
#root {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
</style>
|
||||
<div id="root"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_hassChanged",
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
observer: "_configChanged",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_configChanged(config) {
|
||||
const root = this.$.root;
|
||||
if (root.lastChild) root.removeChild(root.lastChild);
|
||||
|
||||
const entities = computeUnusedEntities(this.hass, config).map((entity) => ({
|
||||
entity,
|
||||
secondary_info: "entity-id",
|
||||
}));
|
||||
const cardConfig = {
|
||||
type: "entities",
|
||||
title: "Unused entities",
|
||||
entities,
|
||||
show_header_toggle: false,
|
||||
};
|
||||
const element = createCardElement(cardConfig);
|
||||
element.hass = this.hass;
|
||||
root.appendChild(element);
|
||||
}
|
||||
|
||||
_hassChanged(hass) {
|
||||
const root = this.$.root;
|
||||
if (!root.lastChild) return;
|
||||
root.lastChild.hass = hass;
|
||||
}
|
||||
}
|
||||
customElements.define("hui-unused-entities", HuiUnusedEntities);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import computeUnusedEntities from "./common/compute-unused-entities";
|
||||
import createCardElement from "./common/create-card-element";
|
||||
|
||||
import "./cards/hui-entities-card.ts";
|
||||
|
||||
class HuiUnusedEntities extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
#root {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
</style>
|
||||
<div id="root"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_hassChanged",
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
observer: "_configChanged",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_configChanged(config) {
|
||||
const root = this.$.root;
|
||||
if (root.lastChild) root.removeChild(root.lastChild);
|
||||
|
||||
const entities = computeUnusedEntities(this.hass, config).map((entity) => ({
|
||||
entity,
|
||||
secondary_info: "entity-id",
|
||||
}));
|
||||
const cardConfig = {
|
||||
type: "entities",
|
||||
title: "Unused entities",
|
||||
entities,
|
||||
show_header_toggle: false,
|
||||
};
|
||||
const element = createCardElement(cardConfig);
|
||||
element.hass = this.hass;
|
||||
root.appendChild(element);
|
||||
}
|
||||
|
||||
_hassChanged(hass) {
|
||||
const root = this.$.root;
|
||||
if (!root.lastChild) return;
|
||||
root.lastChild.hass = hass;
|
||||
}
|
||||
}
|
||||
customElements.define("hui-unused-entities", HuiUnusedEntities);
|
||||
|
|
|
@ -1,218 +1,218 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../components/entity/ha-state-label-badge";
|
||||
import "./components/hui-card-options.ts";
|
||||
|
||||
import applyThemesOnElement from "../../common/dom/apply_themes_on_element";
|
||||
|
||||
import createCardElement from "./common/create-card-element";
|
||||
import computeCardSize from "./common/compute-card-size";
|
||||
|
||||
class HUIView extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
padding: 4px 4px 0;
|
||||
transform: translateZ(0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#badges {
|
||||
margin: 8px 16px;
|
||||
font-size: 85%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#columns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
max-width: 500px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.column > * {
|
||||
display: block;
|
||||
margin: 4px 4px 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
:host {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.column > * {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 599px) {
|
||||
.column {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div id="badges"></div>
|
||||
<div id="columns"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_hassChanged",
|
||||
},
|
||||
config: Object,
|
||||
columns: Number,
|
||||
editMode: Boolean,
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return [
|
||||
// Put all properties in 1 observer so we only call configChanged once
|
||||
"_createBadges(config)",
|
||||
"_createCards(config, columns, editMode)",
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._cards = [];
|
||||
this._badges = [];
|
||||
}
|
||||
|
||||
_createBadges(config) {
|
||||
const root = this.$.badges;
|
||||
while (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
if (!config || !config.badges || !Array.isArray(config.badges)) {
|
||||
root.style.display = "none";
|
||||
this._badges = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = [];
|
||||
for (const entityId of config.badges) {
|
||||
if (!(entityId in this.hass.states)) continue;
|
||||
|
||||
const element = document.createElement("ha-state-label-badge");
|
||||
element.setProperties({
|
||||
hass: this.hass,
|
||||
state: this.hass.states[entityId],
|
||||
});
|
||||
elements.push({ element, entityId });
|
||||
root.appendChild(element);
|
||||
}
|
||||
this._badges = elements;
|
||||
root.style.display = elements.length > 0 ? "block" : "none";
|
||||
}
|
||||
|
||||
_createCards(config) {
|
||||
const root = this.$.columns;
|
||||
|
||||
while (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
if (!config || !config.cards || !Array.isArray(config.cards)) {
|
||||
this._cards = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = [];
|
||||
const elementsToAppend = [];
|
||||
for (const cardConfig of config.cards) {
|
||||
const element = createCardElement(cardConfig);
|
||||
element.hass = this.hass;
|
||||
elements.push(element);
|
||||
|
||||
if (!this.editMode) {
|
||||
elementsToAppend.push(element);
|
||||
continue;
|
||||
}
|
||||
|
||||
const wrapper = document.createElement("hui-card-options");
|
||||
wrapper.hass = this.hass;
|
||||
wrapper.cardId = cardConfig.id;
|
||||
wrapper.editMode = this.editMode;
|
||||
wrapper.appendChild(element);
|
||||
elementsToAppend.push(wrapper);
|
||||
}
|
||||
|
||||
let columns = [];
|
||||
const columnEntityCount = [];
|
||||
for (let i = 0; i < this.columns; i++) {
|
||||
columns.push([]);
|
||||
columnEntityCount.push(0);
|
||||
}
|
||||
|
||||
// Find column with < 5 entities, else column with lowest count
|
||||
function getColumnIndex(size) {
|
||||
let minIndex = 0;
|
||||
for (let i = 0; i < columnEntityCount.length; i++) {
|
||||
if (columnEntityCount[i] < 5) {
|
||||
minIndex = i;
|
||||
break;
|
||||
}
|
||||
if (columnEntityCount[i] < columnEntityCount[minIndex]) {
|
||||
minIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
columnEntityCount[minIndex] += size;
|
||||
|
||||
return minIndex;
|
||||
}
|
||||
|
||||
elements.forEach((el, index) => {
|
||||
const cardSize = computeCardSize(el);
|
||||
// Element to append might be the wrapped card when we're editing.
|
||||
columns[getColumnIndex(cardSize)].push(elementsToAppend[index]);
|
||||
});
|
||||
|
||||
// Remove empty columns
|
||||
columns = columns.filter((val) => val.length > 0);
|
||||
|
||||
columns.forEach((column) => {
|
||||
const columnEl = document.createElement("div");
|
||||
columnEl.classList.add("column");
|
||||
column.forEach((el) => columnEl.appendChild(el));
|
||||
root.appendChild(columnEl);
|
||||
});
|
||||
|
||||
this._cards = elements;
|
||||
|
||||
if ("theme" in config) {
|
||||
applyThemesOnElement(root, this.hass.themes, config.theme);
|
||||
}
|
||||
}
|
||||
|
||||
_hassChanged(hass) {
|
||||
this._badges.forEach((badge) => {
|
||||
const { element, entityId } = badge;
|
||||
element.setProperties({
|
||||
hass,
|
||||
state: hass.states[entityId],
|
||||
});
|
||||
});
|
||||
this._cards.forEach((element) => {
|
||||
element.hass = hass;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-view", HUIView);
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../components/entity/ha-state-label-badge";
|
||||
import "./components/hui-card-options.ts";
|
||||
|
||||
import applyThemesOnElement from "../../common/dom/apply_themes_on_element";
|
||||
|
||||
import createCardElement from "./common/create-card-element";
|
||||
import computeCardSize from "./common/compute-card-size";
|
||||
|
||||
class HUIView extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
padding: 4px 4px 0;
|
||||
transform: translateZ(0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#badges {
|
||||
margin: 8px 16px;
|
||||
font-size: 85%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#columns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
max-width: 500px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.column > * {
|
||||
display: block;
|
||||
margin: 4px 4px 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
:host {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.column > * {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 599px) {
|
||||
.column {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div id="badges"></div>
|
||||
<div id="columns"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_hassChanged",
|
||||
},
|
||||
config: Object,
|
||||
columns: Number,
|
||||
editMode: Boolean,
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return [
|
||||
// Put all properties in 1 observer so we only call configChanged once
|
||||
"_createBadges(config)",
|
||||
"_createCards(config, columns, editMode)",
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._cards = [];
|
||||
this._badges = [];
|
||||
}
|
||||
|
||||
_createBadges(config) {
|
||||
const root = this.$.badges;
|
||||
while (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
if (!config || !config.badges || !Array.isArray(config.badges)) {
|
||||
root.style.display = "none";
|
||||
this._badges = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = [];
|
||||
for (const entityId of config.badges) {
|
||||
if (!(entityId in this.hass.states)) continue;
|
||||
|
||||
const element = document.createElement("ha-state-label-badge");
|
||||
element.setProperties({
|
||||
hass: this.hass,
|
||||
state: this.hass.states[entityId],
|
||||
});
|
||||
elements.push({ element, entityId });
|
||||
root.appendChild(element);
|
||||
}
|
||||
this._badges = elements;
|
||||
root.style.display = elements.length > 0 ? "block" : "none";
|
||||
}
|
||||
|
||||
_createCards(config) {
|
||||
const root = this.$.columns;
|
||||
|
||||
while (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
if (!config || !config.cards || !Array.isArray(config.cards)) {
|
||||
this._cards = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = [];
|
||||
const elementsToAppend = [];
|
||||
for (const cardConfig of config.cards) {
|
||||
const element = createCardElement(cardConfig);
|
||||
element.hass = this.hass;
|
||||
elements.push(element);
|
||||
|
||||
if (!this.editMode) {
|
||||
elementsToAppend.push(element);
|
||||
continue;
|
||||
}
|
||||
|
||||
const wrapper = document.createElement("hui-card-options");
|
||||
wrapper.hass = this.hass;
|
||||
wrapper.cardId = cardConfig.id;
|
||||
wrapper.editMode = this.editMode;
|
||||
wrapper.appendChild(element);
|
||||
elementsToAppend.push(wrapper);
|
||||
}
|
||||
|
||||
let columns = [];
|
||||
const columnEntityCount = [];
|
||||
for (let i = 0; i < this.columns; i++) {
|
||||
columns.push([]);
|
||||
columnEntityCount.push(0);
|
||||
}
|
||||
|
||||
// Find column with < 5 entities, else column with lowest count
|
||||
function getColumnIndex(size) {
|
||||
let minIndex = 0;
|
||||
for (let i = 0; i < columnEntityCount.length; i++) {
|
||||
if (columnEntityCount[i] < 5) {
|
||||
minIndex = i;
|
||||
break;
|
||||
}
|
||||
if (columnEntityCount[i] < columnEntityCount[minIndex]) {
|
||||
minIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
columnEntityCount[minIndex] += size;
|
||||
|
||||
return minIndex;
|
||||
}
|
||||
|
||||
elements.forEach((el, index) => {
|
||||
const cardSize = computeCardSize(el);
|
||||
// Element to append might be the wrapped card when we're editing.
|
||||
columns[getColumnIndex(cardSize)].push(elementsToAppend[index]);
|
||||
});
|
||||
|
||||
// Remove empty columns
|
||||
columns = columns.filter((val) => val.length > 0);
|
||||
|
||||
columns.forEach((column) => {
|
||||
const columnEl = document.createElement("div");
|
||||
columnEl.classList.add("column");
|
||||
column.forEach((el) => columnEl.appendChild(el));
|
||||
root.appendChild(columnEl);
|
||||
});
|
||||
|
||||
this._cards = elements;
|
||||
|
||||
if ("theme" in config) {
|
||||
applyThemesOnElement(root, this.hass.themes, config.theme);
|
||||
}
|
||||
}
|
||||
|
||||
_hassChanged(hass) {
|
||||
this._badges.forEach((badge) => {
|
||||
const { element, entityId } = badge;
|
||||
element.setProperties({
|
||||
hass,
|
||||
state: hass.states[entityId],
|
||||
});
|
||||
});
|
||||
this._cards.forEach((element) => {
|
||||
element.hass = hass;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-view", HUIView);
|
||||
|
|
|
@ -1,93 +1,93 @@
|
|||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
|
||||
import "../../../components/ha-icon";
|
||||
|
||||
import callService from "../common/call-service";
|
||||
import { EntityRow, CallServiceConfig } from "../entity-rows/types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
class HuiCallServiceRow extends LitElement implements EntityRow {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: CallServiceConfig;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: CallServiceConfig): void {
|
||||
if (!config || !config.name || !config.service) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
this._config = { icon: "hass:remote", action_name: "Run", ...config };
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-icon .icon="${this._config.icon}"></ha-icon>
|
||||
<div class="flex">
|
||||
<div>
|
||||
${this._config.name}
|
||||
</div>
|
||||
<paper-button
|
||||
@click="${this._callService}"
|
||||
>${this._config.action_name}</paper-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
ha-icon {
|
||||
padding: 8px;
|
||||
color: var(--paper-item-icon-color);
|
||||
}
|
||||
.flex {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
margin-left: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.flex div {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
margin-right: -.57em;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _callService() {
|
||||
callService(this._config, this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-call-service-row": HuiCallServiceRow;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-call-service-row", HuiCallServiceRow);
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
|
||||
import "../../../components/ha-icon";
|
||||
|
||||
import callService from "../common/call-service";
|
||||
import { EntityRow, CallServiceConfig } from "../entity-rows/types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
class HuiCallServiceRow extends LitElement implements EntityRow {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: CallServiceConfig;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: CallServiceConfig): void {
|
||||
if (!config || !config.name || !config.service) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
this._config = { icon: "hass:remote", action_name: "Run", ...config };
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-icon .icon="${this._config.icon}"></ha-icon>
|
||||
<div class="flex">
|
||||
<div>
|
||||
${this._config.name}
|
||||
</div>
|
||||
<paper-button
|
||||
@click="${this._callService}"
|
||||
>${this._config.action_name}</paper-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
ha-icon {
|
||||
padding: 8px;
|
||||
color: var(--paper-item-icon-color);
|
||||
}
|
||||
.flex {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
margin-left: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.flex div {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
margin-right: -.57em;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _callService() {
|
||||
callService(this._config, this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-call-service-row": HuiCallServiceRow;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-call-service-row", HuiCallServiceRow);
|
||||
|
|
|
@ -1,53 +1,53 @@
|
|||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { EntityRow, DividerConfig } from "../entity-rows/types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
class HuiDividerRow extends LitElement implements EntityRow {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: DividerConfig;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config): void {
|
||||
if (!config) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
this._config = {
|
||||
style: {
|
||||
height: "1px",
|
||||
"background-color": "var(--secondary-text-color)",
|
||||
},
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const el = document.createElement("div");
|
||||
|
||||
Object.keys(this._config.style).forEach((prop) => {
|
||||
el.style.setProperty(prop, this._config!.style[prop]);
|
||||
});
|
||||
|
||||
return html`
|
||||
${el}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-divider-row": HuiDividerRow;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-divider-row", HuiDividerRow);
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { EntityRow, DividerConfig } from "../entity-rows/types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
class HuiDividerRow extends LitElement implements EntityRow {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: DividerConfig;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config): void {
|
||||
if (!config) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
this._config = {
|
||||
style: {
|
||||
height: "1px",
|
||||
"background-color": "var(--secondary-text-color)",
|
||||
},
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const el = document.createElement("div");
|
||||
|
||||
Object.keys(this._config.style).forEach((prop) => {
|
||||
el.style.setProperty(prop, this._config!.style[prop]);
|
||||
});
|
||||
|
||||
return html`
|
||||
${el}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-divider-row": HuiDividerRow;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-divider-row", HuiDividerRow);
|
||||
|
|
|
@ -1,70 +1,70 @@
|
|||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { EntityRow, SectionConfig } from "../entity-rows/types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
import "../../../components/ha-icon";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
class HuiSectionRow extends LitElement implements EntityRow {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: SectionConfig;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: SectionConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<div class=divider></div>
|
||||
${
|
||||
this._config.label
|
||||
? html`<div class="label">${this._config.label}</div>`
|
||||
: html``
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
.label {
|
||||
color: var(--primary-color);
|
||||
margin-left: 8px;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: var(--secondary-text-color);
|
||||
opacity: 0.25;
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-section-row": HuiSectionRow;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-section-row", HuiSectionRow);
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { EntityRow, SectionConfig } from "../entity-rows/types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
import "../../../components/ha-icon";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
class HuiSectionRow extends LitElement implements EntityRow {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: SectionConfig;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: SectionConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<div class=divider></div>
|
||||
${
|
||||
this._config.label
|
||||
? html`<div class="label">${this._config.label}</div>`
|
||||
: html``
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
.label {
|
||||
color: var(--primary-color);
|
||||
margin-left: 8px;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: var(--secondary-text-color);
|
||||
opacity: 0.25;
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-section-row": HuiSectionRow;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-section-row", HuiSectionRow);
|
||||
|
|
|
@ -1,75 +1,75 @@
|
|||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { EntityRow, WeblinkConfig } from "../entity-rows/types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
import "../../../components/ha-icon";
|
||||
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
class HuiWeblinkRow extends LitElement implements EntityRow {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: WeblinkConfig;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: WeblinkConfig): void {
|
||||
if (!config || !config.url) {
|
||||
throw new Error("Invalid Configuration: 'url' required");
|
||||
}
|
||||
|
||||
this._config = {
|
||||
icon: "hass:link",
|
||||
name: config.url,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<a href="${this._config.url}">
|
||||
<ha-icon .icon="${this._config.icon}"></ha-icon>
|
||||
<div>${this._config.name}</div>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-icon {
|
||||
padding: 8px;
|
||||
color: var(--paper-item-icon-color);
|
||||
}
|
||||
div {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: 16px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-weblink-row": HuiWeblinkRow;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-weblink-row", HuiWeblinkRow);
|
||||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { EntityRow, WeblinkConfig } from "../entity-rows/types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
import "../../../components/ha-icon";
|
||||
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
class HuiWeblinkRow extends LitElement implements EntityRow {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: WeblinkConfig;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: WeblinkConfig): void {
|
||||
if (!config || !config.url) {
|
||||
throw new Error("Invalid Configuration: 'url' required");
|
||||
}
|
||||
|
||||
this._config = {
|
||||
icon: "hass:link",
|
||||
name: config.url,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<a href="${this._config.url}">
|
||||
<ha-icon .icon="${this._config.icon}"></ha-icon>
|
||||
<div>${this._config.name}</div>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-icon {
|
||||
padding: 8px;
|
||||
color: var(--paper-item-icon-color);
|
||||
}
|
||||
div {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: 16px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-weblink-row": HuiWeblinkRow;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-weblink-row", HuiWeblinkRow);
|
||||
|
|
|
@ -1,53 +1,53 @@
|
|||
import assert from "assert";
|
||||
|
||||
import parseAspectRatio from "../../../src/common/util/parse-aspect-ratio";
|
||||
|
||||
describe("parseAspectRatio", () => {
|
||||
const ratio16by9 = { w: 16, h: 9 };
|
||||
const ratio178 = { w: 1.78, h: 1 };
|
||||
|
||||
it("Parses 16x9", () => {
|
||||
const r = parseAspectRatio("16x9");
|
||||
assert.deepEqual(r, ratio16by9);
|
||||
});
|
||||
|
||||
it("Parses 16:9", () => {
|
||||
const r = parseAspectRatio("16:9");
|
||||
assert.deepEqual(r, ratio16by9);
|
||||
});
|
||||
|
||||
it("Parses 1.78x1", () => {
|
||||
const r = parseAspectRatio("1.78x1");
|
||||
assert.deepEqual(r, ratio178);
|
||||
});
|
||||
|
||||
it("Parses 1.78:1", () => {
|
||||
const r = parseAspectRatio("1.78:1");
|
||||
assert.deepEqual(r, ratio178);
|
||||
});
|
||||
|
||||
it("Parses 1.78", () => {
|
||||
const r = parseAspectRatio("1.78");
|
||||
assert.deepEqual(r, ratio178);
|
||||
});
|
||||
|
||||
it("Skips null states", () => {
|
||||
const r = parseAspectRatio(null);
|
||||
assert.equal(r, null);
|
||||
});
|
||||
|
||||
it("Skips empty states", () => {
|
||||
const r = parseAspectRatio(" ");
|
||||
assert.equal(r, null);
|
||||
});
|
||||
|
||||
it("Skips invalid input", () => {
|
||||
const r = parseAspectRatio("mary had a little lamb");
|
||||
assert.equal(r, null);
|
||||
});
|
||||
|
||||
it("Skips invalid, but close input", () => {
|
||||
const r = parseAspectRatio("mary:lamb");
|
||||
assert.equal(r, null);
|
||||
});
|
||||
});
|
||||
import assert from "assert";
|
||||
|
||||
import parseAspectRatio from "../../../src/common/util/parse-aspect-ratio";
|
||||
|
||||
describe("parseAspectRatio", () => {
|
||||
const ratio16by9 = { w: 16, h: 9 };
|
||||
const ratio178 = { w: 1.78, h: 1 };
|
||||
|
||||
it("Parses 16x9", () => {
|
||||
const r = parseAspectRatio("16x9");
|
||||
assert.deepEqual(r, ratio16by9);
|
||||
});
|
||||
|
||||
it("Parses 16:9", () => {
|
||||
const r = parseAspectRatio("16:9");
|
||||
assert.deepEqual(r, ratio16by9);
|
||||
});
|
||||
|
||||
it("Parses 1.78x1", () => {
|
||||
const r = parseAspectRatio("1.78x1");
|
||||
assert.deepEqual(r, ratio178);
|
||||
});
|
||||
|
||||
it("Parses 1.78:1", () => {
|
||||
const r = parseAspectRatio("1.78:1");
|
||||
assert.deepEqual(r, ratio178);
|
||||
});
|
||||
|
||||
it("Parses 1.78", () => {
|
||||
const r = parseAspectRatio("1.78");
|
||||
assert.deepEqual(r, ratio178);
|
||||
});
|
||||
|
||||
it("Skips null states", () => {
|
||||
const r = parseAspectRatio(null);
|
||||
assert.equal(r, null);
|
||||
});
|
||||
|
||||
it("Skips empty states", () => {
|
||||
const r = parseAspectRatio(" ");
|
||||
assert.equal(r, null);
|
||||
});
|
||||
|
||||
it("Skips invalid input", () => {
|
||||
const r = parseAspectRatio("mary had a little lamb");
|
||||
assert.equal(r, null);
|
||||
});
|
||||
|
||||
it("Skips invalid, but close input", () => {
|
||||
const r = parseAspectRatio("mary:lamb");
|
||||
assert.equal(r, null);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue