mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-08 01:46:35 +00:00
Remove no longer needed blocks (#1262)
This commit is contained in:
parent
4d48a63141
commit
10c997b7b2
@ -7,118 +7,116 @@ import computeStateName from '../common/entity/compute_state_name.js';
|
|||||||
import EventsMixin from '../mixins/events-mixin.js';
|
import EventsMixin from '../mixins/events-mixin.js';
|
||||||
import LocalizeMixin from '../mixins/localize-mixin.js';
|
import LocalizeMixin from '../mixins/localize-mixin.js';
|
||||||
|
|
||||||
{
|
const UPDATE_INTERVAL = 10000; // ms
|
||||||
const UPDATE_INTERVAL = 10000; // ms
|
/*
|
||||||
/*
|
* @appliesMixin LocalizeMixin
|
||||||
* @appliesMixin LocalizeMixin
|
* @appliesMixin EventsMixin
|
||||||
* @appliesMixin EventsMixin
|
*/
|
||||||
*/
|
class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||||
class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
static get template() {
|
||||||
static get template() {
|
return html`
|
||||||
return html`
|
<style include="paper-material-styles">
|
||||||
<style include="paper-material-styles">
|
:host {
|
||||||
:host {
|
@apply --paper-material-elevation-1;
|
||||||
@apply --paper-material-elevation-1;
|
display: block;
|
||||||
display: block;
|
position: relative;
|
||||||
position: relative;
|
font-size: 0px;
|
||||||
font-size: 0px;
|
border-radius: 2px;
|
||||||
border-radius: 2px;
|
cursor: pointer;
|
||||||
cursor: pointer;
|
min-height: 48px;
|
||||||
min-height: 48px;
|
line-height: 0;
|
||||||
line-height: 0;
|
}
|
||||||
}
|
.camera-feed {
|
||||||
.camera-feed {
|
width: 100%;
|
||||||
width: 100%;
|
height: auto;
|
||||||
height: auto;
|
border-radius: 2px;
|
||||||
border-radius: 2px;
|
}
|
||||||
}
|
.caption {
|
||||||
.caption {
|
@apply --paper-font-common-nowrap;
|
||||||
@apply --paper-font-common-nowrap;
|
position: absolute;
|
||||||
position: absolute;
|
left: 0px;
|
||||||
left: 0px;
|
right: 0px;
|
||||||
right: 0px;
|
bottom: 0px;
|
||||||
bottom: 0px;
|
border-bottom-left-radius: 2px;
|
||||||
border-bottom-left-radius: 2px;
|
border-bottom-right-radius: 2px;
|
||||||
border-bottom-right-radius: 2px;
|
|
||||||
|
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template is="dom-if" if="[[cameraFeedSrc]]">
|
<template is="dom-if" if="[[cameraFeedSrc]]">
|
||||||
<img src="[[cameraFeedSrc]]" class="camera-feed" alt="[[_computeStateName(stateObj)]]">
|
<img src="[[cameraFeedSrc]]" class="camera-feed" alt="[[_computeStateName(stateObj)]]">
|
||||||
|
</template>
|
||||||
|
<div class="caption">
|
||||||
|
[[_computeStateName(stateObj)]]
|
||||||
|
<template is="dom-if" if="[[!imageLoaded]]">
|
||||||
|
([[localize('ui.card.camera.not_available')]])
|
||||||
</template>
|
</template>
|
||||||
<div class="caption">
|
</div>
|
||||||
[[_computeStateName(stateObj)]]
|
|
||||||
<template is="dom-if" if="[[!imageLoaded]]">
|
|
||||||
([[localize('ui.card.camera.not_available')]])
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
stateObj: {
|
|
||||||
type: Object,
|
|
||||||
observer: 'updateCameraFeedSrc',
|
|
||||||
},
|
|
||||||
cameraFeedSrc: {
|
|
||||||
type: String,
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
imageLoaded: {
|
|
||||||
type: Boolean,
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
this.addEventListener('click', () => this.cardTapped());
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this.timer = setInterval(() => this.updateCameraFeedSrc(), UPDATE_INTERVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
clearInterval(this.timer);
|
|
||||||
}
|
|
||||||
|
|
||||||
cardTapped() {
|
|
||||||
this.fire('hass-more-info', { entityId: this.stateObj.entity_id });
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCameraFeedSrc() {
|
|
||||||
this.hass.connection.sendMessagePromise({
|
|
||||||
type: 'camera_thumbnail',
|
|
||||||
entity_id: this.stateObj.entity_id,
|
|
||||||
}).then((resp) => {
|
|
||||||
if (resp.success) {
|
|
||||||
this.setProperties({
|
|
||||||
imageLoaded: true,
|
|
||||||
cameraFeedSrc: `data:${resp.result.content_type};base64, ${resp.result.content}`,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.imageLoaded = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeStateName(stateObj) {
|
|
||||||
return computeStateName(stateObj);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
customElements.define('ha-camera-card', HaCameraCard);
|
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: Object,
|
||||||
|
stateObj: {
|
||||||
|
type: Object,
|
||||||
|
observer: 'updateCameraFeedSrc',
|
||||||
|
},
|
||||||
|
cameraFeedSrc: {
|
||||||
|
type: String,
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
imageLoaded: {
|
||||||
|
type: Boolean,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ready() {
|
||||||
|
super.ready();
|
||||||
|
this.addEventListener('click', () => this.cardTapped());
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.timer = setInterval(() => this.updateCameraFeedSrc(), UPDATE_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
clearInterval(this.timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
cardTapped() {
|
||||||
|
this.fire('hass-more-info', { entityId: this.stateObj.entity_id });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCameraFeedSrc() {
|
||||||
|
this.hass.connection.sendMessagePromise({
|
||||||
|
type: 'camera_thumbnail',
|
||||||
|
entity_id: this.stateObj.entity_id,
|
||||||
|
}).then((resp) => {
|
||||||
|
if (resp.success) {
|
||||||
|
this.setProperties({
|
||||||
|
imageLoaded: true,
|
||||||
|
cameraFeedSrc: `data:${resp.result.content_type};base64, ${resp.result.content}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.imageLoaded = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeStateName(stateObj) {
|
||||||
|
return computeStateName(stateObj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
customElements.define('ha-camera-card', HaCameraCard);
|
||||||
|
@ -12,357 +12,355 @@ import computeStateDomain from '../common/entity/compute_state_domain.js';
|
|||||||
import splitByGroups from '../common/entity/split_by_groups.js';
|
import splitByGroups from '../common/entity/split_by_groups.js';
|
||||||
import getGroupEntities from '../common/entity/get_group_entities.js';
|
import getGroupEntities from '../common/entity/get_group_entities.js';
|
||||||
|
|
||||||
{
|
// mapping domain to size of the card.
|
||||||
// mapping domain to size of the card.
|
const DOMAINS_WITH_CARD = {
|
||||||
const DOMAINS_WITH_CARD = {
|
camera: 4,
|
||||||
camera: 4,
|
history_graph: 4,
|
||||||
history_graph: 4,
|
media_player: 3,
|
||||||
media_player: 3,
|
persistent_notification: 0,
|
||||||
persistent_notification: 0,
|
plant: 3,
|
||||||
plant: 3,
|
weather: 4,
|
||||||
weather: 4,
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// 4 types:
|
// 4 types:
|
||||||
// badges: 0 .. 10
|
// badges: 0 .. 10
|
||||||
|
// before groups < 0
|
||||||
|
// groups: X
|
||||||
|
// rest: 100
|
||||||
|
|
||||||
|
const PRIORITY = {
|
||||||
// before groups < 0
|
// before groups < 0
|
||||||
// groups: X
|
configurator: -20,
|
||||||
// rest: 100
|
persistent_notification: -15,
|
||||||
|
|
||||||
const PRIORITY = {
|
// badges have priority >= 0
|
||||||
// before groups < 0
|
updater: 0,
|
||||||
configurator: -20,
|
sun: 1,
|
||||||
persistent_notification: -15,
|
device_tracker: 2,
|
||||||
|
alarm_control_panel: 3,
|
||||||
|
timer: 4,
|
||||||
|
sensor: 5,
|
||||||
|
binary_sensor: 6,
|
||||||
|
mailbox: 7,
|
||||||
|
};
|
||||||
|
|
||||||
// badges have priority >= 0
|
const getPriority = domain =>
|
||||||
updater: 0,
|
((domain in PRIORITY) ? PRIORITY[domain] : 100);
|
||||||
sun: 1,
|
|
||||||
device_tracker: 2,
|
|
||||||
alarm_control_panel: 3,
|
|
||||||
timer: 4,
|
|
||||||
sensor: 5,
|
|
||||||
binary_sensor: 6,
|
|
||||||
mailbox: 7,
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPriority = domain =>
|
const sortPriority = (domainA, domainB) =>
|
||||||
((domain in PRIORITY) ? PRIORITY[domain] : 100);
|
domainA.priority - domainB.priority;
|
||||||
|
|
||||||
const sortPriority = (domainA, domainB) =>
|
const entitySortBy = (entityA, entityB) => {
|
||||||
domainA.priority - domainB.priority;
|
const nameA = (entityA.attributes.friendly_name ||
|
||||||
|
entityA.entity_id).toLowerCase();
|
||||||
|
const nameB = (entityB.attributes.friendly_name ||
|
||||||
|
entityB.entity_id).toLowerCase();
|
||||||
|
|
||||||
const entitySortBy = (entityA, entityB) => {
|
if (nameA < nameB) {
|
||||||
const nameA = (entityA.attributes.friendly_name ||
|
return -1;
|
||||||
entityA.entity_id).toLowerCase();
|
}
|
||||||
const nameB = (entityB.attributes.friendly_name ||
|
if (nameA > nameB) {
|
||||||
entityB.entity_id).toLowerCase();
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
if (nameA < nameB) {
|
const iterateDomainSorted = (collection, func) => {
|
||||||
return -1;
|
Object.keys(collection)
|
||||||
|
.map(key => collection[key])
|
||||||
|
.sort(sortPriority)
|
||||||
|
.forEach((domain) => {
|
||||||
|
domain.states.sort(entitySortBy);
|
||||||
|
func(domain);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
class HaCards extends PolymerElement {
|
||||||
|
static get template() {
|
||||||
|
return html`
|
||||||
|
<style include="iron-flex iron-flex-factors"></style>
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
transform: translateZ(0);
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
if (nameA > nameB) {
|
|
||||||
return 1;
|
.badges {
|
||||||
|
font-size: 85%;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const iterateDomainSorted = (collection, func) => {
|
.column {
|
||||||
Object.keys(collection)
|
max-width: 500px;
|
||||||
.map(key => collection[key])
|
overflow-x: hidden;
|
||||||
.sort(sortPriority)
|
}
|
||||||
.forEach((domain) => {
|
|
||||||
domain.states.sort(entitySortBy);
|
|
||||||
func(domain);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
class HaCards extends PolymerElement {
|
ha-card-chooser {
|
||||||
static get template() {
|
display: block;
|
||||||
return html`
|
margin-left: 8px;
|
||||||
<style include="iron-flex iron-flex-factors"></style>
|
margin-bottom: 8px;
|
||||||
<style>
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
padding-right: 0;
|
||||||
padding-top: 8px;
|
|
||||||
padding-right: 8px;
|
|
||||||
transform: translateZ(0);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badges {
|
|
||||||
font-size: 85%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column {
|
|
||||||
max-width: 500px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-card-chooser {
|
ha-card-chooser {
|
||||||
display: block;
|
margin-left: 0;
|
||||||
margin-left: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 599px) {
|
||||||
:host {
|
.column {
|
||||||
padding-right: 0;
|
max-width: 600px;
|
||||||
}
|
|
||||||
|
|
||||||
ha-card-chooser {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@media (max-width: 599px) {
|
<div id="main">
|
||||||
.column {
|
<template is="dom-if" if="[[cards.badges]]">
|
||||||
max-width: 600px;
|
<div class="badges">
|
||||||
}
|
<template is="dom-if" if="[[cards.demo]]">
|
||||||
}
|
<ha-demo-badge></ha-demo-badge>
|
||||||
</style>
|
</template>
|
||||||
|
|
||||||
<div id="main">
|
<ha-badges-card states="[[cards.badges]]" hass="[[hass]]"></ha-badges-card>
|
||||||
<template is="dom-if" if="[[cards.badges]]">
|
</div>
|
||||||
<div class="badges">
|
</template>
|
||||||
<template is="dom-if" if="[[cards.demo]]">
|
|
||||||
<ha-demo-badge></ha-demo-badge>
|
<div class="horizontal layout center-justified">
|
||||||
|
<template is="dom-repeat" items="[[cards.columns]]" as="column">
|
||||||
|
<div class="column flex-1">
|
||||||
|
<template is="dom-repeat" items="[[column]]" as="card">
|
||||||
|
<ha-card-chooser card-data="[[card]]"></ha-card-chooser>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ha-badges-card states="[[cards.badges]]" hass="[[hass]]"></ha-badges-card>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="horizontal layout center-justified">
|
|
||||||
<template is="dom-repeat" items="[[cards.columns]]" as="column">
|
|
||||||
<div class="column flex-1">
|
|
||||||
<template is="dom-repeat" items="[[column]]" as="card">
|
|
||||||
<ha-card-chooser card-data="[[card]]"></ha-card-chooser>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: Object,
|
||||||
|
|
||||||
|
columns: {
|
||||||
|
type: Number,
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
states: Object,
|
||||||
|
panelVisible: Boolean,
|
||||||
|
|
||||||
|
viewVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
orderedGroupEntities: Array,
|
||||||
|
|
||||||
|
cards: Object,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get observers() {
|
||||||
|
return [
|
||||||
|
'updateCards(columns, states, panelVisible, viewVisible, orderedGroupEntities)',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCards(
|
||||||
|
columns,
|
||||||
|
states,
|
||||||
|
panelVisible,
|
||||||
|
viewVisible,
|
||||||
|
orderedGroupEntities
|
||||||
|
) {
|
||||||
|
if (!panelVisible || !viewVisible) {
|
||||||
|
if (this.$.main.parentNode) {
|
||||||
|
this.$.main._parentNode = this.$.main.parentNode;
|
||||||
|
this.$.main.parentNode.removeChild(this.$.main);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (!this.$.main.parentNode && this.$.main._parentNode) {
|
||||||
|
this.$.main._parentNode.appendChild(this.$.main);
|
||||||
|
}
|
||||||
|
this._debouncer = Debouncer.debounce(
|
||||||
|
this._debouncer,
|
||||||
|
timeOut.after(10),
|
||||||
|
() => {
|
||||||
|
// Things might have changed since it got scheduled.
|
||||||
|
if (this.panelVisible && this.viewVisible) {
|
||||||
|
this.cards = this.computeCards(columns, states, orderedGroupEntities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyCards() {
|
||||||
|
return {
|
||||||
|
demo: false,
|
||||||
|
badges: [],
|
||||||
|
columns: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
computeCards(columns, states, orderedGroupEntities) {
|
||||||
|
const hass = this.hass;
|
||||||
|
|
||||||
|
const cards = this.emptyCards();
|
||||||
|
|
||||||
|
const entityCount = [];
|
||||||
|
for (let i = 0; i < columns; i++) {
|
||||||
|
cards.columns.push([]);
|
||||||
|
entityCount.push(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get properties() {
|
// Find column with < 5 entities, else column with lowest count
|
||||||
return {
|
function getIndex(size) {
|
||||||
hass: Object,
|
let minIndex = 0;
|
||||||
|
for (let i = 0; i < entityCount.length; i++) {
|
||||||
|
if (entityCount[i] < 5) {
|
||||||
|
minIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (entityCount[i] < entityCount[minIndex]) {
|
||||||
|
minIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
columns: {
|
entityCount[minIndex] += size;
|
||||||
type: Number,
|
|
||||||
value: 2,
|
|
||||||
},
|
|
||||||
|
|
||||||
states: Object,
|
return minIndex;
|
||||||
panelVisible: Boolean,
|
|
||||||
|
|
||||||
viewVisible: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
orderedGroupEntities: Array,
|
|
||||||
|
|
||||||
cards: Object,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get observers() {
|
function addEntitiesCard(name, entities, groupEntity) {
|
||||||
return [
|
if (entities.length === 0) return;
|
||||||
'updateCards(columns, states, panelVisible, viewVisible, orderedGroupEntities)',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCards(
|
const owncard = [];
|
||||||
columns,
|
const other = [];
|
||||||
states,
|
|
||||||
panelVisible,
|
|
||||||
viewVisible,
|
|
||||||
orderedGroupEntities
|
|
||||||
) {
|
|
||||||
if (!panelVisible || !viewVisible) {
|
|
||||||
if (this.$.main.parentNode) {
|
|
||||||
this.$.main._parentNode = this.$.main.parentNode;
|
|
||||||
this.$.main.parentNode.removeChild(this.$.main);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else if (!this.$.main.parentNode && this.$.main._parentNode) {
|
|
||||||
this.$.main._parentNode.appendChild(this.$.main);
|
|
||||||
}
|
|
||||||
this._debouncer = Debouncer.debounce(
|
|
||||||
this._debouncer,
|
|
||||||
timeOut.after(10),
|
|
||||||
() => {
|
|
||||||
// Things might have changed since it got scheduled.
|
|
||||||
if (this.panelVisible && this.viewVisible) {
|
|
||||||
this.cards = this.computeCards(columns, states, orderedGroupEntities);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
emptyCards() {
|
let size = 0;
|
||||||
return {
|
|
||||||
demo: false,
|
|
||||||
badges: [],
|
|
||||||
columns: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
computeCards(columns, states, orderedGroupEntities) {
|
entities.forEach((entity) => {
|
||||||
const hass = this.hass;
|
const domain = computeStateDomain(entity);
|
||||||
|
|
||||||
const cards = this.emptyCards();
|
if (domain in DOMAINS_WITH_CARD) {
|
||||||
|
owncard.push(entity);
|
||||||
const entityCount = [];
|
size += DOMAINS_WITH_CARD[domain];
|
||||||
for (let i = 0; i < columns; i++) {
|
|
||||||
cards.columns.push([]);
|
|
||||||
entityCount.push(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find column with < 5 entities, else column with lowest count
|
|
||||||
function getIndex(size) {
|
|
||||||
let minIndex = 0;
|
|
||||||
for (let i = 0; i < entityCount.length; i++) {
|
|
||||||
if (entityCount[i] < 5) {
|
|
||||||
minIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (entityCount[i] < entityCount[minIndex]) {
|
|
||||||
minIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entityCount[minIndex] += size;
|
|
||||||
|
|
||||||
return minIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addEntitiesCard(name, entities, groupEntity) {
|
|
||||||
if (entities.length === 0) return;
|
|
||||||
|
|
||||||
const owncard = [];
|
|
||||||
const other = [];
|
|
||||||
|
|
||||||
let size = 0;
|
|
||||||
|
|
||||||
entities.forEach((entity) => {
|
|
||||||
const domain = computeStateDomain(entity);
|
|
||||||
|
|
||||||
if (domain in DOMAINS_WITH_CARD) {
|
|
||||||
owncard.push(entity);
|
|
||||||
size += DOMAINS_WITH_CARD[domain];
|
|
||||||
} else {
|
|
||||||
other.push(entity);
|
|
||||||
size++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add 1 to the size if we're rendering entities card
|
|
||||||
size += other.length > 0;
|
|
||||||
|
|
||||||
const curIndex = getIndex(size);
|
|
||||||
|
|
||||||
if (other.length > 0) {
|
|
||||||
cards.columns[curIndex].push({
|
|
||||||
hass: hass,
|
|
||||||
cardType: 'entities',
|
|
||||||
states: other,
|
|
||||||
groupEntity: groupEntity || false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
owncard.forEach((entity) => {
|
|
||||||
cards.columns[curIndex].push({
|
|
||||||
hass: hass,
|
|
||||||
cardType: computeStateDomain(entity),
|
|
||||||
stateObj: entity,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitted = splitByGroups(states);
|
|
||||||
if (orderedGroupEntities) {
|
|
||||||
splitted.groups.sort((gr1, gr2) => orderedGroupEntities[gr1.entity_id] -
|
|
||||||
orderedGroupEntities[gr2.entity_id]);
|
|
||||||
} else {
|
|
||||||
splitted.groups.sort((gr1, gr2) => gr1.attributes.order - gr2.attributes.order);
|
|
||||||
}
|
|
||||||
|
|
||||||
const badgesColl = {};
|
|
||||||
const beforeGroupColl = {};
|
|
||||||
const afterGroupedColl = {};
|
|
||||||
|
|
||||||
Object.keys(splitted.ungrouped).forEach((key) => {
|
|
||||||
const state = splitted.ungrouped[key];
|
|
||||||
const domain = computeStateDomain(state);
|
|
||||||
|
|
||||||
if (domain === 'a') {
|
|
||||||
cards.demo = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const priority = getPriority(domain);
|
|
||||||
let coll;
|
|
||||||
|
|
||||||
if (priority < 0) {
|
|
||||||
coll = beforeGroupColl;
|
|
||||||
} else if (priority < 10) {
|
|
||||||
coll = badgesColl;
|
|
||||||
} else {
|
} else {
|
||||||
coll = afterGroupedColl;
|
other.push(entity);
|
||||||
|
size++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(domain in coll)) {
|
|
||||||
coll[domain] = {
|
|
||||||
domain: domain,
|
|
||||||
priority: priority,
|
|
||||||
states: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
coll[domain].states.push(state);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (orderedGroupEntities) {
|
// Add 1 to the size if we're rendering entities card
|
||||||
Object.keys(badgesColl)
|
size += other.length > 0;
|
||||||
.map(key => badgesColl[key])
|
|
||||||
.forEach((domain) => {
|
|
||||||
cards.badges.push.apply(cards.badges, domain.states);
|
|
||||||
});
|
|
||||||
|
|
||||||
cards.badges.sort((e1, e2) => orderedGroupEntities[e1.entity_id] -
|
const curIndex = getIndex(size);
|
||||||
orderedGroupEntities[e2.entity_id]);
|
|
||||||
|
if (other.length > 0) {
|
||||||
|
cards.columns[curIndex].push({
|
||||||
|
hass: hass,
|
||||||
|
cardType: 'entities',
|
||||||
|
states: other,
|
||||||
|
groupEntity: groupEntity || false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
owncard.forEach((entity) => {
|
||||||
|
cards.columns[curIndex].push({
|
||||||
|
hass: hass,
|
||||||
|
cardType: computeStateDomain(entity),
|
||||||
|
stateObj: entity,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitted = splitByGroups(states);
|
||||||
|
if (orderedGroupEntities) {
|
||||||
|
splitted.groups.sort((gr1, gr2) => orderedGroupEntities[gr1.entity_id] -
|
||||||
|
orderedGroupEntities[gr2.entity_id]);
|
||||||
|
} else {
|
||||||
|
splitted.groups.sort((gr1, gr2) => gr1.attributes.order - gr2.attributes.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
const badgesColl = {};
|
||||||
|
const beforeGroupColl = {};
|
||||||
|
const afterGroupedColl = {};
|
||||||
|
|
||||||
|
Object.keys(splitted.ungrouped).forEach((key) => {
|
||||||
|
const state = splitted.ungrouped[key];
|
||||||
|
const domain = computeStateDomain(state);
|
||||||
|
|
||||||
|
if (domain === 'a') {
|
||||||
|
cards.demo = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const priority = getPriority(domain);
|
||||||
|
let coll;
|
||||||
|
|
||||||
|
if (priority < 0) {
|
||||||
|
coll = beforeGroupColl;
|
||||||
|
} else if (priority < 10) {
|
||||||
|
coll = badgesColl;
|
||||||
} else {
|
} else {
|
||||||
iterateDomainSorted(badgesColl, (domain) => {
|
coll = afterGroupedColl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(domain in coll)) {
|
||||||
|
coll[domain] = {
|
||||||
|
domain: domain,
|
||||||
|
priority: priority,
|
||||||
|
states: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
coll[domain].states.push(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (orderedGroupEntities) {
|
||||||
|
Object.keys(badgesColl)
|
||||||
|
.map(key => badgesColl[key])
|
||||||
|
.forEach((domain) => {
|
||||||
cards.badges.push.apply(cards.badges, domain.states);
|
cards.badges.push.apply(cards.badges, domain.states);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
iterateDomainSorted(beforeGroupColl, (domain) => {
|
cards.badges.sort((e1, e2) => orderedGroupEntities[e1.entity_id] -
|
||||||
addEntitiesCard(domain.domain, domain.states);
|
orderedGroupEntities[e2.entity_id]);
|
||||||
|
} else {
|
||||||
|
iterateDomainSorted(badgesColl, (domain) => {
|
||||||
|
cards.badges.push.apply(cards.badges, domain.states);
|
||||||
});
|
});
|
||||||
|
|
||||||
splitted.groups.forEach((groupState) => {
|
|
||||||
const entities = getGroupEntities(states, groupState);
|
|
||||||
addEntitiesCard(
|
|
||||||
groupState.entity_id,
|
|
||||||
Object.keys(entities).map(key => entities[key]),
|
|
||||||
groupState
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
iterateDomainSorted(afterGroupedColl, (domain) => {
|
|
||||||
addEntitiesCard(domain.domain, domain.states);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove empty columns
|
|
||||||
cards.columns = cards.columns.filter(val => val.length > 0);
|
|
||||||
|
|
||||||
return cards;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
iterateDomainSorted(beforeGroupColl, (domain) => {
|
||||||
|
addEntitiesCard(domain.domain, domain.states);
|
||||||
|
});
|
||||||
|
|
||||||
|
splitted.groups.forEach((groupState) => {
|
||||||
|
const entities = getGroupEntities(states, groupState);
|
||||||
|
addEntitiesCard(
|
||||||
|
groupState.entity_id,
|
||||||
|
Object.keys(entities).map(key => entities[key]),
|
||||||
|
groupState
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
iterateDomainSorted(afterGroupedColl, (domain) => {
|
||||||
|
addEntitiesCard(domain.domain, domain.states);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove empty columns
|
||||||
|
cards.columns = cards.columns.filter(val => val.length > 0);
|
||||||
|
|
||||||
|
return cards;
|
||||||
}
|
}
|
||||||
customElements.define('ha-cards', HaCards);
|
|
||||||
}
|
}
|
||||||
|
customElements.define('ha-cards', HaCards);
|
||||||
|
@ -8,381 +8,379 @@ import computeStateDomain from '../common/entity/compute_state_domain.js';
|
|||||||
import computeStateDisplay from '../common/entity/compute_state_display.js';
|
import computeStateDisplay from '../common/entity/compute_state_display.js';
|
||||||
import LocalizeMixin from '../mixins/localize-mixin.js';
|
import LocalizeMixin from '../mixins/localize-mixin.js';
|
||||||
|
|
||||||
{
|
const RECENT_THRESHOLD = 60000; // 1 minute
|
||||||
const RECENT_THRESHOLD = 60000; // 1 minute
|
const RECENT_CACHE = {};
|
||||||
const RECENT_CACHE = {};
|
const DOMAINS_USE_LAST_UPDATED = ['thermostat', 'climate'];
|
||||||
const DOMAINS_USE_LAST_UPDATED = ['thermostat', 'climate'];
|
const LINE_ATTRIBUTES_TO_KEEP = ['temperature', 'current_temperature', 'target_temp_low', 'target_temp_high'];
|
||||||
const LINE_ATTRIBUTES_TO_KEEP = ['temperature', 'current_temperature', 'target_temp_low', 'target_temp_high'];
|
const stateHistoryCache = {};
|
||||||
const stateHistoryCache = {};
|
|
||||||
|
|
||||||
function computeHistory(stateHistory, localize, language) {
|
function computeHistory(stateHistory, localize, language) {
|
||||||
const lineChartDevices = {};
|
const lineChartDevices = {};
|
||||||
const timelineDevices = [];
|
const timelineDevices = [];
|
||||||
if (!stateHistory) {
|
if (!stateHistory) {
|
||||||
return { line: [], timeline: [] };
|
return { line: [], timeline: [] };
|
||||||
}
|
|
||||||
|
|
||||||
stateHistory.forEach((stateInfo) => {
|
|
||||||
if (stateInfo.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateWithUnit = stateInfo.find(state => 'unit_of_measurement' in state.attributes);
|
|
||||||
|
|
||||||
const unit = stateWithUnit ?
|
|
||||||
stateWithUnit.attributes.unit_of_measurement : false;
|
|
||||||
|
|
||||||
if (!unit) {
|
|
||||||
timelineDevices.push({
|
|
||||||
name: computeStateName(stateInfo[0]),
|
|
||||||
entity_id: stateInfo[0].entity_id,
|
|
||||||
data: stateInfo
|
|
||||||
.map(state => ({
|
|
||||||
state_localize: computeStateDisplay(localize, state, language),
|
|
||||||
state: state.state,
|
|
||||||
last_changed: state.last_changed,
|
|
||||||
}))
|
|
||||||
.filter((element, index, arr) => {
|
|
||||||
if (index === 0) return true;
|
|
||||||
return element.state !== arr[index - 1].state;
|
|
||||||
})
|
|
||||||
});
|
|
||||||
} else if (unit in lineChartDevices) {
|
|
||||||
lineChartDevices[unit].push(stateInfo);
|
|
||||||
} else {
|
|
||||||
lineChartDevices[unit] = [stateInfo];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const unitStates = Object.keys(lineChartDevices).map(unit => ({
|
|
||||||
unit: unit,
|
|
||||||
identifier: lineChartDevices[unit].map(states => states[0].entity_id).join(''),
|
|
||||||
data: lineChartDevices[unit].map((states) => {
|
|
||||||
const last = states[states.length - 1];
|
|
||||||
const domain = computeStateDomain(last);
|
|
||||||
return {
|
|
||||||
domain: domain,
|
|
||||||
name: computeStateName(last),
|
|
||||||
entity_id: last.entity_id,
|
|
||||||
states: states.map((state) => {
|
|
||||||
const result = {
|
|
||||||
state: state.state,
|
|
||||||
last_changed: state.last_changed,
|
|
||||||
};
|
|
||||||
if (DOMAINS_USE_LAST_UPDATED.includes(domain)) {
|
|
||||||
result.last_changed = state.last_updated;
|
|
||||||
}
|
|
||||||
LINE_ATTRIBUTES_TO_KEEP.forEach((attr) => {
|
|
||||||
if (attr in state.attributes) {
|
|
||||||
result.attributes = result.attributes || {};
|
|
||||||
result.attributes[attr] = state.attributes[attr];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}).filter((element, index, arr) => {
|
|
||||||
// Remove data point if it is equal to previous point and next point.
|
|
||||||
if (index === 0 || index === (arr.length - 1)) return true;
|
|
||||||
function compare(obj1, obj2) {
|
|
||||||
if (obj1.state !== obj2.state) return false;
|
|
||||||
if (!obj1.attributes && !obj2.attributes) return true;
|
|
||||||
if (!obj1.attributes || !obj2.attributes) return false;
|
|
||||||
return LINE_ATTRIBUTES_TO_KEEP.every(attr =>
|
|
||||||
obj1.attributes[attr] === obj2.attributes[attr]);
|
|
||||||
}
|
|
||||||
return !compare(element, arr[index - 1]) || !compare(element, arr[index + 1]);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return { line: unitStates, timeline: timelineDevices };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
stateHistory.forEach((stateInfo) => {
|
||||||
* @appliesMixin LocalizeMixin
|
if (stateInfo.length === 0) {
|
||||||
*/
|
return;
|
||||||
class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
|
}
|
||||||
static get properties() {
|
|
||||||
|
const stateWithUnit = stateInfo.find(state => 'unit_of_measurement' in state.attributes);
|
||||||
|
|
||||||
|
const unit = stateWithUnit ?
|
||||||
|
stateWithUnit.attributes.unit_of_measurement : false;
|
||||||
|
|
||||||
|
if (!unit) {
|
||||||
|
timelineDevices.push({
|
||||||
|
name: computeStateName(stateInfo[0]),
|
||||||
|
entity_id: stateInfo[0].entity_id,
|
||||||
|
data: stateInfo
|
||||||
|
.map(state => ({
|
||||||
|
state_localize: computeStateDisplay(localize, state, language),
|
||||||
|
state: state.state,
|
||||||
|
last_changed: state.last_changed,
|
||||||
|
}))
|
||||||
|
.filter((element, index, arr) => {
|
||||||
|
if (index === 0) return true;
|
||||||
|
return element.state !== arr[index - 1].state;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else if (unit in lineChartDevices) {
|
||||||
|
lineChartDevices[unit].push(stateInfo);
|
||||||
|
} else {
|
||||||
|
lineChartDevices[unit] = [stateInfo];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const unitStates = Object.keys(lineChartDevices).map(unit => ({
|
||||||
|
unit: unit,
|
||||||
|
identifier: lineChartDevices[unit].map(states => states[0].entity_id).join(''),
|
||||||
|
data: lineChartDevices[unit].map((states) => {
|
||||||
|
const last = states[states.length - 1];
|
||||||
|
const domain = computeStateDomain(last);
|
||||||
return {
|
return {
|
||||||
hass: {
|
domain: domain,
|
||||||
type: Object,
|
name: computeStateName(last),
|
||||||
observer: 'hassChanged',
|
entity_id: last.entity_id,
|
||||||
},
|
states: states.map((state) => {
|
||||||
|
const result = {
|
||||||
filterType: String,
|
state: state.state,
|
||||||
|
last_changed: state.last_changed,
|
||||||
cacheConfig: Object,
|
};
|
||||||
|
if (DOMAINS_USE_LAST_UPDATED.includes(domain)) {
|
||||||
startTime: Date,
|
result.last_changed = state.last_updated;
|
||||||
endTime: Date,
|
}
|
||||||
|
LINE_ATTRIBUTES_TO_KEEP.forEach((attr) => {
|
||||||
entityId: String,
|
if (attr in state.attributes) {
|
||||||
|
result.attributes = result.attributes || {};
|
||||||
isLoading: {
|
result.attributes[attr] = state.attributes[attr];
|
||||||
type: Boolean,
|
}
|
||||||
value: true,
|
});
|
||||||
readOnly: true,
|
return result;
|
||||||
notify: true,
|
}).filter((element, index, arr) => {
|
||||||
},
|
// Remove data point if it is equal to previous point and next point.
|
||||||
|
if (index === 0 || index === (arr.length - 1)) return true;
|
||||||
data: {
|
function compare(obj1, obj2) {
|
||||||
type: Object,
|
if (obj1.state !== obj2.state) return false;
|
||||||
value: null,
|
if (!obj1.attributes && !obj2.attributes) return true;
|
||||||
readOnly: true,
|
if (!obj1.attributes || !obj2.attributes) return false;
|
||||||
notify: true,
|
return LINE_ATTRIBUTES_TO_KEEP.every(attr =>
|
||||||
},
|
obj1.attributes[attr] === obj2.attributes[attr]);
|
||||||
|
}
|
||||||
|
return !compare(element, arr[index - 1]) || !compare(element, arr[index + 1]);
|
||||||
|
})
|
||||||
};
|
};
|
||||||
}
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
static get observers() {
|
return { line: unitStates, timeline: timelineDevices };
|
||||||
return [
|
}
|
||||||
'filterChangedDebouncer(filterType, entityId, startTime, endTime, cacheConfig, localize, language)',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
/*
|
||||||
super.connectedCallback();
|
* @appliesMixin LocalizeMixin
|
||||||
|
*/
|
||||||
|
class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: {
|
||||||
|
type: Object,
|
||||||
|
observer: 'hassChanged',
|
||||||
|
},
|
||||||
|
|
||||||
|
filterType: String,
|
||||||
|
|
||||||
|
cacheConfig: Object,
|
||||||
|
|
||||||
|
startTime: Date,
|
||||||
|
endTime: Date,
|
||||||
|
|
||||||
|
entityId: String,
|
||||||
|
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
value: true,
|
||||||
|
readOnly: true,
|
||||||
|
notify: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
value: null,
|
||||||
|
readOnly: true,
|
||||||
|
notify: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get observers() {
|
||||||
|
return [
|
||||||
|
'filterChangedDebouncer(filterType, entityId, startTime, endTime, cacheConfig, localize, language)',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.filterChangedDebouncer(
|
||||||
|
this.filterType, this.entityId, this.startTime, this.endTime,
|
||||||
|
this.cacheConfig, this.localize, this.language
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
if (this._refreshTimeoutId) {
|
||||||
|
window.clearInterval(this._refreshTimeoutId);
|
||||||
|
this._refreshTimeoutId = null;
|
||||||
|
}
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
hassChanged(newHass, oldHass) {
|
||||||
|
if (!oldHass && !this._madeFirstCall) {
|
||||||
this.filterChangedDebouncer(
|
this.filterChangedDebouncer(
|
||||||
this.filterType, this.entityId, this.startTime, this.endTime,
|
this.filterType, this.entityId, this.startTime, this.endTime,
|
||||||
this.cacheConfig, this.localize, this.language
|
this.cacheConfig, this.localize, this.language
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
if (this._refreshTimeoutId) {
|
|
||||||
window.clearInterval(this._refreshTimeoutId);
|
|
||||||
this._refreshTimeoutId = null;
|
|
||||||
}
|
|
||||||
super.disconnectedCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
hassChanged(newHass, oldHass) {
|
|
||||||
if (!oldHass && !this._madeFirstCall) {
|
|
||||||
this.filterChangedDebouncer(
|
|
||||||
this.filterType, this.entityId, this.startTime, this.endTime,
|
|
||||||
this.cacheConfig, this.localize, this.language
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filterChangedDebouncer(...args) {
|
|
||||||
this._debounceFilterChanged = Debouncer.debounce(
|
|
||||||
this._debounceFilterChanged,
|
|
||||||
timeOut.after(0),
|
|
||||||
() => {
|
|
||||||
this.filterChanged(...args);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
filterChanged(filterType, entityId, startTime, endTime, cacheConfig, localize, language) {
|
|
||||||
if (!this.hass) return;
|
|
||||||
if (cacheConfig && !cacheConfig.cacheKey) return;
|
|
||||||
if (!localize || !language) return;
|
|
||||||
this._madeFirstCall = true;
|
|
||||||
let data;
|
|
||||||
|
|
||||||
if (filterType === 'date') {
|
|
||||||
if (!startTime || !endTime) return;
|
|
||||||
data = this.getDate(startTime, endTime, localize, language);
|
|
||||||
} else if (filterType === 'recent-entity') {
|
|
||||||
if (!entityId) return;
|
|
||||||
if (cacheConfig) {
|
|
||||||
data = this.getRecentWithCacheRefresh(entityId, cacheConfig, localize, language);
|
|
||||||
} else {
|
|
||||||
data = this.getRecent(entityId, startTime, endTime, localize, language);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._setIsLoading(true);
|
|
||||||
|
|
||||||
data.then((stateHistory) => {
|
|
||||||
this._setData(stateHistory);
|
|
||||||
this._setIsLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getEmptyCache(language) {
|
|
||||||
return {
|
|
||||||
prom: Promise.resolve({ line: [], timeline: [] }),
|
|
||||||
language: language,
|
|
||||||
data: { line: [], timeline: [] },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getRecentWithCacheRefresh(entityId, cacheConfig, localize, language) {
|
|
||||||
if (this._refreshTimeoutId) {
|
|
||||||
window.clearInterval(this._refreshTimeoutId);
|
|
||||||
this._refreshTimeoutId = null;
|
|
||||||
}
|
|
||||||
if (cacheConfig.refresh) {
|
|
||||||
this._refreshTimeoutId = window.setInterval(() => {
|
|
||||||
this.getRecentWithCache(entityId, cacheConfig, localize, language)
|
|
||||||
.then((stateHistory) => {
|
|
||||||
this._setData(Object.assign({}, stateHistory));
|
|
||||||
});
|
|
||||||
}, cacheConfig.refresh * 1000);
|
|
||||||
}
|
|
||||||
return this.getRecentWithCache(entityId, cacheConfig, localize, language);
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeLine(historyLines, cacheLines) {
|
|
||||||
historyLines.forEach((line) => {
|
|
||||||
const unit = line.unit;
|
|
||||||
const oldLine = cacheLines.find(cacheLine => cacheLine.unit === unit);
|
|
||||||
if (oldLine) {
|
|
||||||
line.data.forEach((entity) => {
|
|
||||||
const oldEntity =
|
|
||||||
oldLine.data.find(cacheEntity => entity.entity_id === cacheEntity.entity_id);
|
|
||||||
if (oldEntity) {
|
|
||||||
oldEntity.states = oldEntity.states.concat(entity.states);
|
|
||||||
} else {
|
|
||||||
oldLine.data.push(entity);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
cacheLines.push(line);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeTimeline(historyTimelines, cacheTimelines) {
|
|
||||||
historyTimelines.forEach((timeline) => {
|
|
||||||
const oldTimeline =
|
|
||||||
cacheTimelines.find(cacheTimeline => cacheTimeline.entity_id === timeline.entity_id);
|
|
||||||
if (oldTimeline) {
|
|
||||||
oldTimeline.data = oldTimeline.data.concat(timeline.data);
|
|
||||||
} else {
|
|
||||||
cacheTimelines.push(timeline);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pruneArray(originalStartTime, arr) {
|
|
||||||
if (arr.length === 0) return arr;
|
|
||||||
const changedAfterStartTime = arr.findIndex((state) => {
|
|
||||||
const lastChanged = new Date(state.last_changed);
|
|
||||||
return lastChanged > originalStartTime;
|
|
||||||
});
|
|
||||||
if (changedAfterStartTime === 0) {
|
|
||||||
// If all changes happened after originalStartTime then we are done.
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all changes happened at or before originalStartTime. Use last index.
|
|
||||||
const updateIndex = changedAfterStartTime === -1 ? arr.length - 1 : changedAfterStartTime - 1;
|
|
||||||
arr[updateIndex].last_changed = originalStartTime;
|
|
||||||
return arr.slice(updateIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
pruneStartTime(originalStartTime, cacheData) {
|
|
||||||
cacheData.line.forEach((line) => {
|
|
||||||
line.data.forEach((entity) => {
|
|
||||||
entity.states = this.pruneArray(originalStartTime, entity.states);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cacheData.timeline.forEach((timeline) => {
|
|
||||||
timeline.data = this.pruneArray(originalStartTime, timeline.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getRecentWithCache(entityId, cacheConfig, localize, language) {
|
|
||||||
const cacheKey = cacheConfig.cacheKey;
|
|
||||||
const endTime = new Date();
|
|
||||||
const originalStartTime = new Date(endTime);
|
|
||||||
originalStartTime.setHours(originalStartTime.getHours() - cacheConfig.hoursToShow);
|
|
||||||
let startTime = originalStartTime;
|
|
||||||
let appendingToCache = false;
|
|
||||||
let cache = stateHistoryCache[cacheKey];
|
|
||||||
if (cache && startTime >= cache.startTime && startTime <= cache.endTime
|
|
||||||
&& cache.language === language) {
|
|
||||||
startTime = cache.endTime;
|
|
||||||
appendingToCache = true;
|
|
||||||
if (endTime <= cache.endTime) {
|
|
||||||
return cache.prom;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cache = stateHistoryCache[cacheKey] = this.getEmptyCache(language);
|
|
||||||
}
|
|
||||||
// Use Promise.all in order to make sure the old and the new fetches have both completed.
|
|
||||||
const prom = Promise.all([cache.prom,
|
|
||||||
this.fetchRecent(entityId, startTime, endTime, appendingToCache)])
|
|
||||||
// Use only data from the new fetch. Old fetch is already stored in cache.data
|
|
||||||
.then(oldAndNew => oldAndNew[1])
|
|
||||||
// Convert data into format state-history-chart-* understands.
|
|
||||||
.then(stateHistory => computeHistory(stateHistory, localize, language))
|
|
||||||
// Merge old and new.
|
|
||||||
.then((stateHistory) => {
|
|
||||||
this.mergeLine(stateHistory.line, cache.data.line);
|
|
||||||
this.mergeTimeline(stateHistory.timeline, cache.data.timeline);
|
|
||||||
if (appendingToCache) {
|
|
||||||
this.pruneStartTime(originalStartTime, cache.data);
|
|
||||||
}
|
|
||||||
return cache.data;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
console.error(err);
|
|
||||||
stateHistoryCache[cacheKey] = undefined;
|
|
||||||
});
|
|
||||||
cache.prom = prom;
|
|
||||||
cache.startTime = originalStartTime;
|
|
||||||
cache.endTime = endTime;
|
|
||||||
return prom;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRecent(entityId, startTime, endTime, localize, language) {
|
|
||||||
const cacheKey = entityId;
|
|
||||||
const cache = RECENT_CACHE[cacheKey];
|
|
||||||
|
|
||||||
if (cache && Date.now() - cache.created < RECENT_THRESHOLD && cache.language === language) {
|
|
||||||
return cache.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prom = this.fetchRecent(entityId, startTime, endTime).then(
|
|
||||||
stateHistory => computeHistory(stateHistory, localize, language),
|
|
||||||
() => {
|
|
||||||
RECENT_CACHE[entityId] = false;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
RECENT_CACHE[cacheKey] = {
|
|
||||||
created: Date.now(),
|
|
||||||
language: language,
|
|
||||||
data: prom,
|
|
||||||
};
|
|
||||||
return prom;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchRecent(entityId, startTime, endTime, skipInitialState = false) {
|
|
||||||
let url = 'history/period';
|
|
||||||
if (startTime) {
|
|
||||||
url += '/' + startTime.toISOString();
|
|
||||||
}
|
|
||||||
url += '?filter_entity_id=' + entityId;
|
|
||||||
if (endTime) {
|
|
||||||
url += '&end_time=' + endTime.toISOString();
|
|
||||||
}
|
|
||||||
if (skipInitialState) {
|
|
||||||
url += '&skip_initial_state';
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.hass.callApi('GET', url);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDate(startTime, endTime, localize, language) {
|
|
||||||
const filter = startTime.toISOString() + '?end_time=' + endTime.toISOString();
|
|
||||||
|
|
||||||
const prom = this.hass.callApi('GET', 'history/period/' + filter).then(
|
|
||||||
stateHistory => computeHistory(stateHistory, localize, language),
|
|
||||||
() => null
|
|
||||||
);
|
|
||||||
|
|
||||||
return prom;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
customElements.define('ha-state-history-data', HaStateHistoryData);
|
|
||||||
|
filterChangedDebouncer(...args) {
|
||||||
|
this._debounceFilterChanged = Debouncer.debounce(
|
||||||
|
this._debounceFilterChanged,
|
||||||
|
timeOut.after(0),
|
||||||
|
() => {
|
||||||
|
this.filterChanged(...args);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChanged(filterType, entityId, startTime, endTime, cacheConfig, localize, language) {
|
||||||
|
if (!this.hass) return;
|
||||||
|
if (cacheConfig && !cacheConfig.cacheKey) return;
|
||||||
|
if (!localize || !language) return;
|
||||||
|
this._madeFirstCall = true;
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (filterType === 'date') {
|
||||||
|
if (!startTime || !endTime) return;
|
||||||
|
data = this.getDate(startTime, endTime, localize, language);
|
||||||
|
} else if (filterType === 'recent-entity') {
|
||||||
|
if (!entityId) return;
|
||||||
|
if (cacheConfig) {
|
||||||
|
data = this.getRecentWithCacheRefresh(entityId, cacheConfig, localize, language);
|
||||||
|
} else {
|
||||||
|
data = this.getRecent(entityId, startTime, endTime, localize, language);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._setIsLoading(true);
|
||||||
|
|
||||||
|
data.then((stateHistory) => {
|
||||||
|
this._setData(stateHistory);
|
||||||
|
this._setIsLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getEmptyCache(language) {
|
||||||
|
return {
|
||||||
|
prom: Promise.resolve({ line: [], timeline: [] }),
|
||||||
|
language: language,
|
||||||
|
data: { line: [], timeline: [] },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecentWithCacheRefresh(entityId, cacheConfig, localize, language) {
|
||||||
|
if (this._refreshTimeoutId) {
|
||||||
|
window.clearInterval(this._refreshTimeoutId);
|
||||||
|
this._refreshTimeoutId = null;
|
||||||
|
}
|
||||||
|
if (cacheConfig.refresh) {
|
||||||
|
this._refreshTimeoutId = window.setInterval(() => {
|
||||||
|
this.getRecentWithCache(entityId, cacheConfig, localize, language)
|
||||||
|
.then((stateHistory) => {
|
||||||
|
this._setData(Object.assign({}, stateHistory));
|
||||||
|
});
|
||||||
|
}, cacheConfig.refresh * 1000);
|
||||||
|
}
|
||||||
|
return this.getRecentWithCache(entityId, cacheConfig, localize, language);
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeLine(historyLines, cacheLines) {
|
||||||
|
historyLines.forEach((line) => {
|
||||||
|
const unit = line.unit;
|
||||||
|
const oldLine = cacheLines.find(cacheLine => cacheLine.unit === unit);
|
||||||
|
if (oldLine) {
|
||||||
|
line.data.forEach((entity) => {
|
||||||
|
const oldEntity =
|
||||||
|
oldLine.data.find(cacheEntity => entity.entity_id === cacheEntity.entity_id);
|
||||||
|
if (oldEntity) {
|
||||||
|
oldEntity.states = oldEntity.states.concat(entity.states);
|
||||||
|
} else {
|
||||||
|
oldLine.data.push(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cacheLines.push(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeTimeline(historyTimelines, cacheTimelines) {
|
||||||
|
historyTimelines.forEach((timeline) => {
|
||||||
|
const oldTimeline =
|
||||||
|
cacheTimelines.find(cacheTimeline => cacheTimeline.entity_id === timeline.entity_id);
|
||||||
|
if (oldTimeline) {
|
||||||
|
oldTimeline.data = oldTimeline.data.concat(timeline.data);
|
||||||
|
} else {
|
||||||
|
cacheTimelines.push(timeline);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneArray(originalStartTime, arr) {
|
||||||
|
if (arr.length === 0) return arr;
|
||||||
|
const changedAfterStartTime = arr.findIndex((state) => {
|
||||||
|
const lastChanged = new Date(state.last_changed);
|
||||||
|
return lastChanged > originalStartTime;
|
||||||
|
});
|
||||||
|
if (changedAfterStartTime === 0) {
|
||||||
|
// If all changes happened after originalStartTime then we are done.
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all changes happened at or before originalStartTime. Use last index.
|
||||||
|
const updateIndex = changedAfterStartTime === -1 ? arr.length - 1 : changedAfterStartTime - 1;
|
||||||
|
arr[updateIndex].last_changed = originalStartTime;
|
||||||
|
return arr.slice(updateIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneStartTime(originalStartTime, cacheData) {
|
||||||
|
cacheData.line.forEach((line) => {
|
||||||
|
line.data.forEach((entity) => {
|
||||||
|
entity.states = this.pruneArray(originalStartTime, entity.states);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cacheData.timeline.forEach((timeline) => {
|
||||||
|
timeline.data = this.pruneArray(originalStartTime, timeline.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecentWithCache(entityId, cacheConfig, localize, language) {
|
||||||
|
const cacheKey = cacheConfig.cacheKey;
|
||||||
|
const endTime = new Date();
|
||||||
|
const originalStartTime = new Date(endTime);
|
||||||
|
originalStartTime.setHours(originalStartTime.getHours() - cacheConfig.hoursToShow);
|
||||||
|
let startTime = originalStartTime;
|
||||||
|
let appendingToCache = false;
|
||||||
|
let cache = stateHistoryCache[cacheKey];
|
||||||
|
if (cache && startTime >= cache.startTime && startTime <= cache.endTime
|
||||||
|
&& cache.language === language) {
|
||||||
|
startTime = cache.endTime;
|
||||||
|
appendingToCache = true;
|
||||||
|
if (endTime <= cache.endTime) {
|
||||||
|
return cache.prom;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cache = stateHistoryCache[cacheKey] = this.getEmptyCache(language);
|
||||||
|
}
|
||||||
|
// Use Promise.all in order to make sure the old and the new fetches have both completed.
|
||||||
|
const prom = Promise.all([cache.prom,
|
||||||
|
this.fetchRecent(entityId, startTime, endTime, appendingToCache)])
|
||||||
|
// Use only data from the new fetch. Old fetch is already stored in cache.data
|
||||||
|
.then(oldAndNew => oldAndNew[1])
|
||||||
|
// Convert data into format state-history-chart-* understands.
|
||||||
|
.then(stateHistory => computeHistory(stateHistory, localize, language))
|
||||||
|
// Merge old and new.
|
||||||
|
.then((stateHistory) => {
|
||||||
|
this.mergeLine(stateHistory.line, cache.data.line);
|
||||||
|
this.mergeTimeline(stateHistory.timeline, cache.data.timeline);
|
||||||
|
if (appendingToCache) {
|
||||||
|
this.pruneStartTime(originalStartTime, cache.data);
|
||||||
|
}
|
||||||
|
return cache.data;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
console.error(err);
|
||||||
|
stateHistoryCache[cacheKey] = undefined;
|
||||||
|
});
|
||||||
|
cache.prom = prom;
|
||||||
|
cache.startTime = originalStartTime;
|
||||||
|
cache.endTime = endTime;
|
||||||
|
return prom;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecent(entityId, startTime, endTime, localize, language) {
|
||||||
|
const cacheKey = entityId;
|
||||||
|
const cache = RECENT_CACHE[cacheKey];
|
||||||
|
|
||||||
|
if (cache && Date.now() - cache.created < RECENT_THRESHOLD && cache.language === language) {
|
||||||
|
return cache.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prom = this.fetchRecent(entityId, startTime, endTime).then(
|
||||||
|
stateHistory => computeHistory(stateHistory, localize, language),
|
||||||
|
() => {
|
||||||
|
RECENT_CACHE[entityId] = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
RECENT_CACHE[cacheKey] = {
|
||||||
|
created: Date.now(),
|
||||||
|
language: language,
|
||||||
|
data: prom,
|
||||||
|
};
|
||||||
|
return prom;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRecent(entityId, startTime, endTime, skipInitialState = false) {
|
||||||
|
let url = 'history/period';
|
||||||
|
if (startTime) {
|
||||||
|
url += '/' + startTime.toISOString();
|
||||||
|
}
|
||||||
|
url += '?filter_entity_id=' + entityId;
|
||||||
|
if (endTime) {
|
||||||
|
url += '&end_time=' + endTime.toISOString();
|
||||||
|
}
|
||||||
|
if (skipInitialState) {
|
||||||
|
url += '&skip_initial_state';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.hass.callApi('GET', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDate(startTime, endTime, localize, language) {
|
||||||
|
const filter = startTime.toISOString() + '?end_time=' + endTime.toISOString();
|
||||||
|
|
||||||
|
const prom = this.hass.callApi('GET', 'history/period/' + filter).then(
|
||||||
|
stateHistory => computeHistory(stateHistory, localize, language),
|
||||||
|
() => null
|
||||||
|
);
|
||||||
|
|
||||||
|
return prom;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
customElements.define('ha-state-history-data', HaStateHistoryData);
|
||||||
|
@ -12,106 +12,104 @@ import featureClassNames from '../../../common/entity/feature_class_names';
|
|||||||
|
|
||||||
import LocalizeMixin from '../../../mixins/localize-mixin.js';
|
import LocalizeMixin from '../../../mixins/localize-mixin.js';
|
||||||
|
|
||||||
{
|
const FEATURE_CLASS_NAMES = {
|
||||||
const FEATURE_CLASS_NAMES = {
|
128: 'has-set_tilt_position',
|
||||||
128: 'has-set_tilt_position',
|
};
|
||||||
};
|
class MoreInfoCover extends LocalizeMixin(PolymerElement) {
|
||||||
class MoreInfoCover extends LocalizeMixin(PolymerElement) {
|
static get template() {
|
||||||
static get template() {
|
return html`
|
||||||
return html`
|
<style include="iron-flex"></style>
|
||||||
<style include="iron-flex"></style>
|
<style>
|
||||||
<style>
|
.current_position, .tilt {
|
||||||
.current_position, .tilt {
|
max-height: 0px;
|
||||||
max-height: 0px;
|
overflow: hidden;
|
||||||
overflow: hidden;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.has-current_position .current_position,
|
.has-current_position .current_position,
|
||||||
.has-set_tilt_position .tilt,
|
.has-set_tilt_position .tilt,
|
||||||
.has-current_tilt_position .tilt
|
.has-current_tilt_position .tilt
|
||||||
{
|
{
|
||||||
max-height: 208px;
|
max-height: 208px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[invisible] {
|
[invisible] {
|
||||||
visibility: hidden !important;
|
visibility: hidden !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class\$="[[computeClassNames(stateObj)]]">
|
<div class\$="[[computeClassNames(stateObj)]]">
|
||||||
|
|
||||||
<div class="current_position">
|
|
||||||
<ha-labeled-slider
|
|
||||||
caption="[[localize('ui.card.cover.position')]]" pin=""
|
|
||||||
value="{{coverPositionSliderValue}}"
|
|
||||||
disabled="[[!entityObj.supportsSetPosition]]"
|
|
||||||
on-change="coverPositionSliderChanged"
|
|
||||||
></ha-labeled-slider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tilt">
|
|
||||||
<ha-labeled-slider
|
|
||||||
caption="[[localize('ui.card.cover.tilt_position')]]" pin="" extra=""
|
|
||||||
value="{{coverTiltPositionSliderValue}}"
|
|
||||||
disabled="[[!entityObj.supportsSetTiltPosition]]"
|
|
||||||
on-change="coverTiltPositionSliderChanged">
|
|
||||||
|
|
||||||
<ha-cover-tilt-controls
|
|
||||||
slot="extra" hidden\$="[[entityObj.isTiltOnly]]"
|
|
||||||
hass="[[hass]]" state-obj="[[stateObj]]"
|
|
||||||
></ha-cover-tilt-controls>
|
|
||||||
|
|
||||||
</ha-labeled-slider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="current_position">
|
||||||
|
<ha-labeled-slider
|
||||||
|
caption="[[localize('ui.card.cover.position')]]" pin=""
|
||||||
|
value="{{coverPositionSliderValue}}"
|
||||||
|
disabled="[[!entityObj.supportsSetPosition]]"
|
||||||
|
on-change="coverPositionSliderChanged"
|
||||||
|
></ha-labeled-slider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tilt">
|
||||||
|
<ha-labeled-slider
|
||||||
|
caption="[[localize('ui.card.cover.tilt_position')]]" pin="" extra=""
|
||||||
|
value="{{coverTiltPositionSliderValue}}"
|
||||||
|
disabled="[[!entityObj.supportsSetTiltPosition]]"
|
||||||
|
on-change="coverTiltPositionSliderChanged">
|
||||||
|
|
||||||
|
<ha-cover-tilt-controls
|
||||||
|
slot="extra" hidden\$="[[entityObj.isTiltOnly]]"
|
||||||
|
hass="[[hass]]" state-obj="[[stateObj]]"
|
||||||
|
></ha-cover-tilt-controls>
|
||||||
|
|
||||||
|
</ha-labeled-slider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
hass: Object,
|
hass: Object,
|
||||||
stateObj: {
|
stateObj: {
|
||||||
type: Object,
|
type: Object,
|
||||||
observer: 'stateObjChanged',
|
observer: 'stateObjChanged',
|
||||||
},
|
},
|
||||||
entityObj: {
|
entityObj: {
|
||||||
type: Object,
|
type: Object,
|
||||||
computed: 'computeEntityObj(hass, stateObj)',
|
computed: 'computeEntityObj(hass, stateObj)',
|
||||||
},
|
},
|
||||||
coverPositionSliderValue: Number,
|
coverPositionSliderValue: Number,
|
||||||
coverTiltPositionSliderValue: Number
|
coverTiltPositionSliderValue: Number
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
computeEntityObj(hass, stateObj) {
|
computeEntityObj(hass, stateObj) {
|
||||||
return new CoverEntity(hass, stateObj);
|
return new CoverEntity(hass, stateObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
stateObjChanged(newVal) {
|
stateObjChanged(newVal) {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
coverPositionSliderValue: newVal.attributes.current_position,
|
coverPositionSliderValue: newVal.attributes.current_position,
|
||||||
coverTiltPositionSliderValue: newVal.attributes.current_tilt_position,
|
coverTiltPositionSliderValue: newVal.attributes.current_tilt_position,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
computeClassNames(stateObj) {
|
|
||||||
var classes = [
|
|
||||||
attributeClassNames(stateObj, ['current_position', 'current_tilt_position']),
|
|
||||||
featureClassNames(stateObj, FEATURE_CLASS_NAMES),
|
|
||||||
];
|
|
||||||
return classes.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
coverPositionSliderChanged(ev) {
|
|
||||||
this.entityObj.setCoverPosition(ev.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
coverTiltPositionSliderChanged(ev) {
|
|
||||||
this.entityObj.setCoverTiltPosition(ev.target.value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('more-info-cover', MoreInfoCover);
|
computeClassNames(stateObj) {
|
||||||
|
var classes = [
|
||||||
|
attributeClassNames(stateObj, ['current_position', 'current_tilt_position']),
|
||||||
|
featureClassNames(stateObj, FEATURE_CLASS_NAMES),
|
||||||
|
];
|
||||||
|
return classes.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
coverPositionSliderChanged(ev) {
|
||||||
|
this.entityObj.setCoverPosition(ev.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
coverTiltPositionSliderChanged(ev) {
|
||||||
|
this.entityObj.setCoverTiltPosition(ev.target.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define('more-info-cover', MoreInfoCover);
|
||||||
|
@ -14,253 +14,251 @@ import featureClassNames from '../../../common/entity/feature_class_names';
|
|||||||
import EventsMixin from '../../../mixins/events-mixin.js';
|
import EventsMixin from '../../../mixins/events-mixin.js';
|
||||||
import LocalizeMixin from '../../../mixins/localize-mixin.js';
|
import LocalizeMixin from '../../../mixins/localize-mixin.js';
|
||||||
|
|
||||||
{
|
const FEATURE_CLASS_NAMES = {
|
||||||
const FEATURE_CLASS_NAMES = {
|
1: 'has-brightness',
|
||||||
1: 'has-brightness',
|
2: 'has-color_temp',
|
||||||
2: 'has-color_temp',
|
4: 'has-effect_list',
|
||||||
4: 'has-effect_list',
|
16: 'has-color',
|
||||||
16: 'has-color',
|
128: 'has-white_value',
|
||||||
128: 'has-white_value',
|
};
|
||||||
};
|
/*
|
||||||
/*
|
* @appliesMixin EventsMixin
|
||||||
* @appliesMixin EventsMixin
|
*/
|
||||||
*/
|
class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||||
class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
static get template() {
|
||||||
static get template() {
|
return html`
|
||||||
return html`
|
<style include="iron-flex"></style>
|
||||||
<style include="iron-flex"></style>
|
<style>
|
||||||
<style>
|
.effect_list {
|
||||||
.effect_list {
|
padding-bottom: 16px;
|
||||||
padding-bottom: 16px;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.effect_list, .brightness, .color_temp, .white_value {
|
.effect_list, .brightness, .color_temp, .white_value {
|
||||||
max-height: 0px;
|
max-height: 0px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: max-height .5s ease-in;
|
transition: max-height .5s ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color_temp {
|
.color_temp {
|
||||||
--ha-slider-background: -webkit-linear-gradient(right, rgb(255, 160, 0) 0%, white 50%, rgb(166, 209, 255) 100%);
|
--ha-slider-background: -webkit-linear-gradient(right, rgb(255, 160, 0) 0%, white 50%, rgb(166, 209, 255) 100%);
|
||||||
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
|
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
|
||||||
--paper-slider-knob-start-border-color: var(--primary-color);
|
--paper-slider-knob-start-border-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-color-picker {
|
ha-color-picker {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
max-height: 0px;
|
max-height: 0px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: max-height .5s ease-in;
|
transition: max-height .5s ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-effect_list.is-on .effect_list,
|
.has-effect_list.is-on .effect_list,
|
||||||
.has-brightness .brightness,
|
.has-brightness .brightness,
|
||||||
.has-color_temp.is-on .color_temp,
|
.has-color_temp.is-on .color_temp,
|
||||||
.has-white_value.is-on .white_value {
|
.has-white_value.is-on .white_value {
|
||||||
max-height: 84px;
|
max-height: 84px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-color.is-on ha-color-picker {
|
.has-color.is-on ha-color-picker {
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
--ha-color-picker-wheel-borderwidth: 5;
|
--ha-color-picker-wheel-borderwidth: 5;
|
||||||
--ha-color-picker-wheel-bordercolor: white;
|
--ha-color-picker-wheel-bordercolor: white;
|
||||||
--ha-color-picker-wheel-shadow: none;
|
--ha-color-picker-wheel-shadow: none;
|
||||||
--ha-color-picker-marker-borderwidth: 2;
|
--ha-color-picker-marker-borderwidth: 2;
|
||||||
--ha-color-picker-marker-bordercolor: white;
|
--ha-color-picker-marker-bordercolor: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-unavailable .control {
|
.is-unavailable .control {
|
||||||
max-height: 0px;
|
max-height: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
paper-item {
|
paper-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class\$="[[computeClassNames(stateObj)]]">
|
<div class\$="[[computeClassNames(stateObj)]]">
|
||||||
|
|
||||||
<div class="control brightness">
|
<div class="control brightness">
|
||||||
<ha-labeled-slider caption="[[localize('ui.card.light.brightness')]]" icon="hass:brightness-5" max="255" value="{{brightnessSliderValue}}" on-change="brightnessSliderChanged"></ha-labeled-slider>
|
<ha-labeled-slider caption="[[localize('ui.card.light.brightness')]]" icon="hass:brightness-5" max="255" value="{{brightnessSliderValue}}" on-change="brightnessSliderChanged"></ha-labeled-slider>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control color_temp">
|
|
||||||
<ha-labeled-slider caption="[[localize('ui.card.light.color_temperature')]]" icon="hass:thermometer" min="[[stateObj.attributes.min_mireds]]" max="[[stateObj.attributes.max_mireds]]" value="{{ctSliderValue}}" on-change="ctSliderChanged"></ha-labeled-slider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control white_value">
|
|
||||||
<ha-labeled-slider caption="[[localize('ui.card.light.white_value')]]" icon="hass:file-word-box" max="255" value="{{wvSliderValue}}" on-change="wvSliderChanged"></ha-labeled-slider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ha-color-picker class="control color" on-colorselected="colorPicked" desired-hs-color="{{colorPickerColor}}" throttle="500" hue-segments="24" saturation-segments="8">
|
|
||||||
</ha-color-picker>
|
|
||||||
|
|
||||||
<div class="control effect_list">
|
|
||||||
<paper-dropdown-menu label-float="" dynamic-align="" label="[[localize('ui.card.light.effect')]]">
|
|
||||||
<paper-listbox slot="dropdown-content" selected="{{effectIndex}}">
|
|
||||||
<template is="dom-repeat" items="[[stateObj.attributes.effect_list]]">
|
|
||||||
<paper-item>[[item]]</paper-item>
|
|
||||||
</template>
|
|
||||||
</paper-listbox>
|
|
||||||
</paper-dropdown-menu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ha-attributes state-obj="[[stateObj]]" extra-filters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,xy_color,min_mireds,max_mireds"></ha-attributes>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="control color_temp">
|
||||||
|
<ha-labeled-slider caption="[[localize('ui.card.light.color_temperature')]]" icon="hass:thermometer" min="[[stateObj.attributes.min_mireds]]" max="[[stateObj.attributes.max_mireds]]" value="{{ctSliderValue}}" on-change="ctSliderChanged"></ha-labeled-slider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control white_value">
|
||||||
|
<ha-labeled-slider caption="[[localize('ui.card.light.white_value')]]" icon="hass:file-word-box" max="255" value="{{wvSliderValue}}" on-change="wvSliderChanged"></ha-labeled-slider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ha-color-picker class="control color" on-colorselected="colorPicked" desired-hs-color="{{colorPickerColor}}" throttle="500" hue-segments="24" saturation-segments="8">
|
||||||
|
</ha-color-picker>
|
||||||
|
|
||||||
|
<div class="control effect_list">
|
||||||
|
<paper-dropdown-menu label-float="" dynamic-align="" label="[[localize('ui.card.light.effect')]]">
|
||||||
|
<paper-listbox slot="dropdown-content" selected="{{effectIndex}}">
|
||||||
|
<template is="dom-repeat" items="[[stateObj.attributes.effect_list]]">
|
||||||
|
<paper-item>[[item]]</paper-item>
|
||||||
|
</template>
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ha-attributes state-obj="[[stateObj]]" extra-filters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,xy_color,min_mireds,max_mireds"></ha-attributes>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
hass: {
|
hass: {
|
||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
|
|
||||||
stateObj: {
|
stateObj: {
|
||||||
type: Object,
|
type: Object,
|
||||||
observer: 'stateObjChanged',
|
observer: 'stateObjChanged',
|
||||||
},
|
},
|
||||||
|
|
||||||
effectIndex: {
|
effectIndex: {
|
||||||
type: Number,
|
type: Number,
|
||||||
value: -1,
|
value: -1,
|
||||||
observer: 'effectChanged',
|
observer: 'effectChanged',
|
||||||
},
|
},
|
||||||
|
|
||||||
brightnessSliderValue: {
|
brightnessSliderValue: {
|
||||||
type: Number,
|
type: Number,
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
ctSliderValue: {
|
ctSliderValue: {
|
||||||
type: Number,
|
type: Number,
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
wvSliderValue: {
|
wvSliderValue: {
|
||||||
type: Number,
|
type: Number,
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
colorPickerColor: {
|
colorPickerColor: {
|
||||||
type: Object,
|
type: Object,
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
stateObjChanged(newVal, oldVal) {
|
|
||||||
const props = {
|
|
||||||
brightnessSliderValue: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
if (newVal && newVal.state === 'on') {
|
|
||||||
props.brightnessSliderValue = newVal.attributes.brightness;
|
|
||||||
props.ctSliderValue = newVal.attributes.color_temp;
|
|
||||||
props.wvSliderValue = newVal.attributes.white_value;
|
|
||||||
if (newVal.attributes.hs_color) {
|
|
||||||
props.colorPickerColor = {
|
|
||||||
h: newVal.attributes.hs_color[0],
|
|
||||||
s: newVal.attributes.hs_color[1] / 100,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (newVal.attributes.effect_list) {
|
|
||||||
props.effectIndex = newVal.attributes.effect_list.indexOf(newVal.attributes.effect);
|
|
||||||
} else {
|
|
||||||
props.effectIndex = -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.setProperties(props);
|
stateObjChanged(newVal, oldVal) {
|
||||||
|
const props = {
|
||||||
|
brightnessSliderValue: 0
|
||||||
|
};
|
||||||
|
|
||||||
if (oldVal) {
|
if (newVal && newVal.state === 'on') {
|
||||||
setTimeout(() => {
|
props.brightnessSliderValue = newVal.attributes.brightness;
|
||||||
this.fire('iron-resize');
|
props.ctSliderValue = newVal.attributes.color_temp;
|
||||||
}, 500);
|
props.wvSliderValue = newVal.attributes.white_value;
|
||||||
|
if (newVal.attributes.hs_color) {
|
||||||
|
props.colorPickerColor = {
|
||||||
|
h: newVal.attributes.hs_color[0],
|
||||||
|
s: newVal.attributes.hs_color[1] / 100,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
if (newVal.attributes.effect_list) {
|
||||||
|
props.effectIndex = newVal.attributes.effect_list.indexOf(newVal.attributes.effect);
|
||||||
computeClassNames(stateObj) {
|
|
||||||
const classes = [featureClassNames(stateObj, FEATURE_CLASS_NAMES)];
|
|
||||||
if (stateObj && stateObj.state === 'on') {
|
|
||||||
classes.push('is-on');
|
|
||||||
}
|
|
||||||
if (stateObj && stateObj.state === 'unavailable') {
|
|
||||||
classes.push('is-unavailable');
|
|
||||||
}
|
|
||||||
return classes.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
effectChanged(effectIndex) {
|
|
||||||
var effectInput;
|
|
||||||
// Selected Option will transition to '' before transitioning to new value
|
|
||||||
if (effectIndex === '' || effectIndex === -1) return;
|
|
||||||
|
|
||||||
effectInput = this.stateObj.attributes.effect_list[effectIndex];
|
|
||||||
if (effectInput === this.stateObj.attributes.effect) return;
|
|
||||||
|
|
||||||
this.hass.callService('light', 'turn_on', {
|
|
||||||
entity_id: this.stateObj.entity_id,
|
|
||||||
effect: effectInput,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
brightnessSliderChanged(ev) {
|
|
||||||
var bri = parseInt(ev.target.value, 10);
|
|
||||||
|
|
||||||
if (isNaN(bri)) return;
|
|
||||||
|
|
||||||
if (bri === 0) {
|
|
||||||
this.hass.callService('light', 'turn_off', {
|
|
||||||
entity_id: this.stateObj.entity_id,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.hass.callService('light', 'turn_on', {
|
props.effectIndex = -1;
|
||||||
entity_id: this.stateObj.entity_id,
|
|
||||||
brightness: bri,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctSliderChanged(ev) {
|
this.setProperties(props);
|
||||||
var ct = parseInt(ev.target.value, 10);
|
|
||||||
|
|
||||||
if (isNaN(ct)) return;
|
if (oldVal) {
|
||||||
|
setTimeout(() => {
|
||||||
this.hass.callService('light', 'turn_on', {
|
this.fire('iron-resize');
|
||||||
entity_id: this.stateObj.entity_id,
|
}, 500);
|
||||||
color_temp: ct,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
wvSliderChanged(ev) {
|
|
||||||
var wv = parseInt(ev.target.value, 10);
|
|
||||||
|
|
||||||
if (isNaN(wv)) return;
|
|
||||||
|
|
||||||
this.hass.callService('light', 'turn_on', {
|
|
||||||
entity_id: this.stateObj.entity_id,
|
|
||||||
white_value: wv,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceChangeColor(hass, entityId, color) {
|
|
||||||
hass.callService('light', 'turn_on', {
|
|
||||||
entity_id: entityId,
|
|
||||||
hs_color: [color.h, color.s * 100],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a new color has been picked.
|
|
||||||
* should be throttled with the 'throttle=' attribute of the color picker
|
|
||||||
*/
|
|
||||||
colorPicked(ev) {
|
|
||||||
this.serviceChangeColor(this.hass, this.stateObj.entity_id, ev.detail.hs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('more-info-light', MoreInfoLight);
|
computeClassNames(stateObj) {
|
||||||
|
const classes = [featureClassNames(stateObj, FEATURE_CLASS_NAMES)];
|
||||||
|
if (stateObj && stateObj.state === 'on') {
|
||||||
|
classes.push('is-on');
|
||||||
|
}
|
||||||
|
if (stateObj && stateObj.state === 'unavailable') {
|
||||||
|
classes.push('is-unavailable');
|
||||||
|
}
|
||||||
|
return classes.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
effectChanged(effectIndex) {
|
||||||
|
var effectInput;
|
||||||
|
// Selected Option will transition to '' before transitioning to new value
|
||||||
|
if (effectIndex === '' || effectIndex === -1) return;
|
||||||
|
|
||||||
|
effectInput = this.stateObj.attributes.effect_list[effectIndex];
|
||||||
|
if (effectInput === this.stateObj.attributes.effect) return;
|
||||||
|
|
||||||
|
this.hass.callService('light', 'turn_on', {
|
||||||
|
entity_id: this.stateObj.entity_id,
|
||||||
|
effect: effectInput,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
brightnessSliderChanged(ev) {
|
||||||
|
var bri = parseInt(ev.target.value, 10);
|
||||||
|
|
||||||
|
if (isNaN(bri)) return;
|
||||||
|
|
||||||
|
if (bri === 0) {
|
||||||
|
this.hass.callService('light', 'turn_off', {
|
||||||
|
entity_id: this.stateObj.entity_id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.hass.callService('light', 'turn_on', {
|
||||||
|
entity_id: this.stateObj.entity_id,
|
||||||
|
brightness: bri,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctSliderChanged(ev) {
|
||||||
|
var ct = parseInt(ev.target.value, 10);
|
||||||
|
|
||||||
|
if (isNaN(ct)) return;
|
||||||
|
|
||||||
|
this.hass.callService('light', 'turn_on', {
|
||||||
|
entity_id: this.stateObj.entity_id,
|
||||||
|
color_temp: ct,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
wvSliderChanged(ev) {
|
||||||
|
var wv = parseInt(ev.target.value, 10);
|
||||||
|
|
||||||
|
if (isNaN(wv)) return;
|
||||||
|
|
||||||
|
this.hass.callService('light', 'turn_on', {
|
||||||
|
entity_id: this.stateObj.entity_id,
|
||||||
|
white_value: wv,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceChangeColor(hass, entityId, color) {
|
||||||
|
hass.callService('light', 'turn_on', {
|
||||||
|
entity_id: entityId,
|
||||||
|
hs_color: [color.h, color.s * 100],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a new color has been picked.
|
||||||
|
* should be throttled with the 'throttle=' attribute of the color picker
|
||||||
|
*/
|
||||||
|
colorPicked(ev) {
|
||||||
|
this.serviceChangeColor(this.hass, this.stateObj.entity_id, ev.detail.hs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define('more-info-light', MoreInfoLight);
|
||||||
|
@ -16,277 +16,275 @@ import isComponentLoaded from '../../../common/config/is_component_loaded.js';
|
|||||||
import EventsMixin from '../../../mixins/events-mixin.js';
|
import EventsMixin from '../../../mixins/events-mixin.js';
|
||||||
import LocalizeMixin from '../../../mixins/localize-mixin.js';
|
import LocalizeMixin from '../../../mixins/localize-mixin.js';
|
||||||
|
|
||||||
{
|
/*
|
||||||
/*
|
* @appliesMixin EventsMixin
|
||||||
* @appliesMixin EventsMixin
|
*/
|
||||||
*/
|
class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||||
class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
static get template() {
|
||||||
static get template() {
|
return html`
|
||||||
return html`
|
<style include="iron-flex iron-flex-alignment"></style>
|
||||||
<style include="iron-flex iron-flex-alignment"></style>
|
<style>
|
||||||
<style>
|
.media-state {
|
||||||
.media-state {
|
text-transform: capitalize;
|
||||||
text-transform: capitalize;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
paper-icon-button[highlight] {
|
paper-icon-button[highlight] {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume {
|
.volume {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
||||||
max-height: 0px;
|
max-height: 0px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: max-height .5s ease-in;
|
transition: max-height .5s ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-volume_level .volume {
|
.has-volume_level .volume {
|
||||||
max-height: 40px;
|
max-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
iron-icon.source-input {
|
iron-icon.source-input {
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
paper-dropdown-menu.source-input {
|
paper-dropdown-menu.source-input {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[hidden] {
|
[hidden] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
paper-item {
|
paper-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class\$="[[computeClassNames(stateObj)]]">
|
<div class\$="[[computeClassNames(stateObj)]]">
|
||||||
<div class="layout horizontal">
|
<div class="layout horizontal">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<paper-icon-button icon="hass:power" highlight\$="[[playerObj.isOff]]" on-click="handleTogglePower" hidden\$="[[computeHidePowerButton(playerObj)]]"></paper-icon-button>
|
<paper-icon-button icon="hass:power" highlight\$="[[playerObj.isOff]]" on-click="handleTogglePower" hidden\$="[[computeHidePowerButton(playerObj)]]"></paper-icon-button>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<template is="dom-if" if="[[computeShowPlaybackControls(playerObj)]]">
|
|
||||||
<paper-icon-button icon="hass:skip-previous" on-click="handlePrevious" hidden\$="[[!playerObj.supportsPreviousTrack]]"></paper-icon-button>
|
|
||||||
<paper-icon-button icon="[[computePlaybackControlIcon(playerObj)]]" on-click="handlePlaybackControl" hidden\$="[[!computePlaybackControlIcon(playerObj)]]" highlight=""></paper-icon-button>
|
|
||||||
<paper-icon-button icon="hass:skip-next" on-click="handleNext" hidden\$="[[!playerObj.supportsNextTrack]]"></paper-icon-button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- VOLUME -->
|
<div>
|
||||||
<div class="volume_buttons center horizontal layout" hidden\$="[[computeHideVolumeButtons(playerObj)]]">
|
<template is="dom-if" if="[[computeShowPlaybackControls(playerObj)]]">
|
||||||
<paper-icon-button on-click="handleVolumeTap" icon="hass:volume-off"></paper-icon-button>
|
<paper-icon-button icon="hass:skip-previous" on-click="handlePrevious" hidden\$="[[!playerObj.supportsPreviousTrack]]"></paper-icon-button>
|
||||||
<paper-icon-button id="volumeDown" disabled\$="[[playerObj.isMuted]]" on-mousedown="handleVolumeDown" on-touchstart="handleVolumeDown" icon="hass:volume-medium"></paper-icon-button>
|
<paper-icon-button icon="[[computePlaybackControlIcon(playerObj)]]" on-click="handlePlaybackControl" hidden\$="[[!computePlaybackControlIcon(playerObj)]]" highlight=""></paper-icon-button>
|
||||||
<paper-icon-button id="volumeUp" disabled\$="[[playerObj.isMuted]]" on-mousedown="handleVolumeUp" on-touchstart="handleVolumeUp" icon="hass:volume-high"></paper-icon-button>
|
<paper-icon-button icon="hass:skip-next" on-click="handleNext" hidden\$="[[!playerObj.supportsNextTrack]]"></paper-icon-button>
|
||||||
</div>
|
</template>
|
||||||
<div class="volume center horizontal layout" hidden\$="[[!playerObj.supportsVolumeSet]]">
|
|
||||||
<paper-icon-button on-click="handleVolumeTap" hidden\$="[[playerObj.supportsVolumeButtons]]" icon="[[computeMuteVolumeIcon(playerObj)]]"></paper-icon-button>
|
|
||||||
<ha-paper-slider disabled\$="[[playerObj.isMuted]]" min="0" max="100" value="[[playerObj.volumeSliderValue]]" on-change="volumeSliderChanged" class="flex" ignore-bar-touch="">
|
|
||||||
</ha-paper-slider>
|
|
||||||
</div>
|
|
||||||
<!-- SOURCE PICKER -->
|
|
||||||
<div class="controls layout horizontal justified" hidden\$="[[computeHideSelectSource(playerObj)]]">
|
|
||||||
<iron-icon class="source-input" icon="hass:login-variant"></iron-icon>
|
|
||||||
<paper-dropdown-menu class="flex source-input" dynamic-align="" label-float="" label="Source">
|
|
||||||
<paper-listbox slot="dropdown-content" selected="{{sourceIndex}}">
|
|
||||||
<template is="dom-repeat" items="[[playerObj.sourceList]]">
|
|
||||||
<paper-item>[[item]]</paper-item>
|
|
||||||
</template>
|
|
||||||
</paper-listbox>
|
|
||||||
</paper-dropdown-menu>
|
|
||||||
</div>
|
|
||||||
<!-- TTS -->
|
|
||||||
<div hidden\$="[[computeHideTTS(ttsLoaded, playerObj)]]" class="layout horizontal end">
|
|
||||||
<paper-input id="ttsInput" label="[[localize('ui.card.media_player.text_to_speak')]]" class="flex" value="{{ttsMessage}}" on-keydown="ttsCheckForEnter"></paper-input>
|
|
||||||
<paper-icon-button icon="hass:send" on-click="sendTTS"></paper-icon-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- VOLUME -->
|
||||||
|
<div class="volume_buttons center horizontal layout" hidden\$="[[computeHideVolumeButtons(playerObj)]]">
|
||||||
|
<paper-icon-button on-click="handleVolumeTap" icon="hass:volume-off"></paper-icon-button>
|
||||||
|
<paper-icon-button id="volumeDown" disabled\$="[[playerObj.isMuted]]" on-mousedown="handleVolumeDown" on-touchstart="handleVolumeDown" icon="hass:volume-medium"></paper-icon-button>
|
||||||
|
<paper-icon-button id="volumeUp" disabled\$="[[playerObj.isMuted]]" on-mousedown="handleVolumeUp" on-touchstart="handleVolumeUp" icon="hass:volume-high"></paper-icon-button>
|
||||||
|
</div>
|
||||||
|
<div class="volume center horizontal layout" hidden\$="[[!playerObj.supportsVolumeSet]]">
|
||||||
|
<paper-icon-button on-click="handleVolumeTap" hidden\$="[[playerObj.supportsVolumeButtons]]" icon="[[computeMuteVolumeIcon(playerObj)]]"></paper-icon-button>
|
||||||
|
<ha-paper-slider disabled\$="[[playerObj.isMuted]]" min="0" max="100" value="[[playerObj.volumeSliderValue]]" on-change="volumeSliderChanged" class="flex" ignore-bar-touch="">
|
||||||
|
</ha-paper-slider>
|
||||||
|
</div>
|
||||||
|
<!-- SOURCE PICKER -->
|
||||||
|
<div class="controls layout horizontal justified" hidden\$="[[computeHideSelectSource(playerObj)]]">
|
||||||
|
<iron-icon class="source-input" icon="hass:login-variant"></iron-icon>
|
||||||
|
<paper-dropdown-menu class="flex source-input" dynamic-align="" label-float="" label="Source">
|
||||||
|
<paper-listbox slot="dropdown-content" selected="{{sourceIndex}}">
|
||||||
|
<template is="dom-repeat" items="[[playerObj.sourceList]]">
|
||||||
|
<paper-item>[[item]]</paper-item>
|
||||||
|
</template>
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu>
|
||||||
|
</div>
|
||||||
|
<!-- TTS -->
|
||||||
|
<div hidden\$="[[computeHideTTS(ttsLoaded, playerObj)]]" class="layout horizontal end">
|
||||||
|
<paper-input id="ttsInput" label="[[localize('ui.card.media_player.text_to_speak')]]" class="flex" value="{{ttsMessage}}" on-keydown="ttsCheckForEnter"></paper-input>
|
||||||
|
<paper-icon-button icon="hass:send" on-click="sendTTS"></paper-icon-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: Object,
|
||||||
|
stateObj: Object,
|
||||||
|
playerObj: {
|
||||||
|
type: Object,
|
||||||
|
computed: 'computePlayerObj(hass, stateObj)',
|
||||||
|
observer: 'playerObjChanged',
|
||||||
|
},
|
||||||
|
|
||||||
|
sourceIndex: {
|
||||||
|
type: Number,
|
||||||
|
value: 0,
|
||||||
|
observer: 'handleSourceChanged',
|
||||||
|
},
|
||||||
|
|
||||||
|
ttsLoaded: {
|
||||||
|
type: Boolean,
|
||||||
|
computed: 'computeTTSLoaded(hass)',
|
||||||
|
},
|
||||||
|
|
||||||
|
ttsMessage: {
|
||||||
|
type: String,
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
computePlayerObj(hass, stateObj) {
|
||||||
|
return new HassMediaPlayerEntity(hass, stateObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
playerObjChanged(newVal, oldVal) {
|
||||||
|
if (newVal && newVal.sourceList !== undefined) {
|
||||||
|
this.sourceIndex = newVal.sourceList.indexOf(newVal.source);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get properties() {
|
if (oldVal) {
|
||||||
return {
|
setTimeout(() => {
|
||||||
hass: Object,
|
this.fire('iron-resize');
|
||||||
stateObj: Object,
|
}, 500);
|
||||||
playerObj: {
|
|
||||||
type: Object,
|
|
||||||
computed: 'computePlayerObj(hass, stateObj)',
|
|
||||||
observer: 'playerObjChanged',
|
|
||||||
},
|
|
||||||
|
|
||||||
sourceIndex: {
|
|
||||||
type: Number,
|
|
||||||
value: 0,
|
|
||||||
observer: 'handleSourceChanged',
|
|
||||||
},
|
|
||||||
|
|
||||||
ttsLoaded: {
|
|
||||||
type: Boolean,
|
|
||||||
computed: 'computeTTSLoaded(hass)',
|
|
||||||
},
|
|
||||||
|
|
||||||
ttsMessage: {
|
|
||||||
type: String,
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
computePlayerObj(hass, stateObj) {
|
|
||||||
return new HassMediaPlayerEntity(hass, stateObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
playerObjChanged(newVal, oldVal) {
|
|
||||||
if (newVal && newVal.sourceList !== undefined) {
|
|
||||||
this.sourceIndex = newVal.sourceList.indexOf(newVal.source);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVal) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.fire('iron-resize');
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
computeClassNames(stateObj) {
|
|
||||||
return attributeClassNames(stateObj, ['volume_level']);
|
|
||||||
}
|
|
||||||
|
|
||||||
computeMuteVolumeIcon(playerObj) {
|
|
||||||
return playerObj.isMuted ? 'hass:volume-off' : 'hass:volume-high';
|
|
||||||
}
|
|
||||||
|
|
||||||
computeHideVolumeButtons(playerObj) {
|
|
||||||
return !playerObj.supportsVolumeButtons || playerObj.isOff;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeShowPlaybackControls(playerObj) {
|
|
||||||
return !playerObj.isOff && playerObj.hasMediaControl;
|
|
||||||
}
|
|
||||||
|
|
||||||
computePlaybackControlIcon(playerObj) {
|
|
||||||
if (playerObj.isPlaying) {
|
|
||||||
return playerObj.supportsPause ? 'hass:pause' : 'hass:stop';
|
|
||||||
}
|
|
||||||
return playerObj.supportsPlay ? 'hass:play' : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeHidePowerButton(playerObj) {
|
|
||||||
return playerObj.isOff ? !playerObj.supportsTurnOn : !playerObj.supportsTurnOff;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeHideSelectSource(playerObj) {
|
|
||||||
return playerObj.isOff || !playerObj.supportsSelectSource || !playerObj.sourceList;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeHideTTS(ttsLoaded, playerObj) {
|
|
||||||
return !ttsLoaded || !playerObj.supportsPlayMedia;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeTTSLoaded(hass) {
|
|
||||||
return isComponentLoaded(hass, 'tts');
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTogglePower() {
|
|
||||||
this.playerObj.togglePower();
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePrevious() {
|
|
||||||
this.playerObj.previousTrack();
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePlaybackControl() {
|
|
||||||
this.playerObj.mediaPlayPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNext() {
|
|
||||||
this.playerObj.nextTrack();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSourceChanged(sourceIndex, sourceIndexOld) {
|
|
||||||
// Selected Option will transition to '' before transitioning to new value
|
|
||||||
if (!this.playerObj
|
|
||||||
|| !this.playerObj.supportsSelectSource
|
|
||||||
|| this.playerObj.sourceList === undefined
|
|
||||||
|| sourceIndex < 0
|
|
||||||
|| sourceIndex >= this.playerObj.sourceList
|
|
||||||
|| sourceIndexOld === undefined
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceInput = this.playerObj.sourceList[sourceIndex];
|
|
||||||
|
|
||||||
if (sourceInput === this.playerObj.source) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.playerObj.selectSource(sourceInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleVolumeTap() {
|
|
||||||
if (!this.playerObj.supportsVolumeMute) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.playerObj.volumeMute(!this.playerObj.isMuted);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleVolumeUp() {
|
|
||||||
const obj = this.$.volumeUp;
|
|
||||||
this.handleVolumeWorker('volume_up', obj, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleVolumeDown() {
|
|
||||||
const obj = this.$.volumeDown;
|
|
||||||
this.handleVolumeWorker('volume_down', obj, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleVolumeWorker(service, obj, force) {
|
|
||||||
if (force || (obj !== undefined && obj.pointerDown)) {
|
|
||||||
this.playerObj.callService(service);
|
|
||||||
setTimeout(() => this.handleVolumeWorker(service, obj, false), 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
volumeSliderChanged(ev) {
|
|
||||||
const volPercentage = parseFloat(ev.target.value);
|
|
||||||
const volume = volPercentage > 0 ? volPercentage / 100 : 0;
|
|
||||||
this.playerObj.setVolume(volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
ttsCheckForEnter(ev) {
|
|
||||||
if (ev.keyCode === 13) this.sendTTS();
|
|
||||||
}
|
|
||||||
|
|
||||||
sendTTS() {
|
|
||||||
const services = this.hass.config.services.tts;
|
|
||||||
const serviceKeys = Object.keys(services).sort();
|
|
||||||
let service;
|
|
||||||
let i;
|
|
||||||
|
|
||||||
for (i = 0; i < serviceKeys.length; i++) {
|
|
||||||
if (serviceKeys[i].indexOf('_say') !== -1) {
|
|
||||||
service = serviceKeys[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!service) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hass.callService('tts', service, {
|
|
||||||
entity_id: this.stateObj.entity_id,
|
|
||||||
message: this.ttsMessage,
|
|
||||||
});
|
|
||||||
this.ttsMessage = '';
|
|
||||||
this.$.ttsInput.focus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('more-info-media_player', MoreInfoMediaPlayer);
|
computeClassNames(stateObj) {
|
||||||
|
return attributeClassNames(stateObj, ['volume_level']);
|
||||||
|
}
|
||||||
|
|
||||||
|
computeMuteVolumeIcon(playerObj) {
|
||||||
|
return playerObj.isMuted ? 'hass:volume-off' : 'hass:volume-high';
|
||||||
|
}
|
||||||
|
|
||||||
|
computeHideVolumeButtons(playerObj) {
|
||||||
|
return !playerObj.supportsVolumeButtons || playerObj.isOff;
|
||||||
|
}
|
||||||
|
|
||||||
|
computeShowPlaybackControls(playerObj) {
|
||||||
|
return !playerObj.isOff && playerObj.hasMediaControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
computePlaybackControlIcon(playerObj) {
|
||||||
|
if (playerObj.isPlaying) {
|
||||||
|
return playerObj.supportsPause ? 'hass:pause' : 'hass:stop';
|
||||||
|
}
|
||||||
|
return playerObj.supportsPlay ? 'hass:play' : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
computeHidePowerButton(playerObj) {
|
||||||
|
return playerObj.isOff ? !playerObj.supportsTurnOn : !playerObj.supportsTurnOff;
|
||||||
|
}
|
||||||
|
|
||||||
|
computeHideSelectSource(playerObj) {
|
||||||
|
return playerObj.isOff || !playerObj.supportsSelectSource || !playerObj.sourceList;
|
||||||
|
}
|
||||||
|
|
||||||
|
computeHideTTS(ttsLoaded, playerObj) {
|
||||||
|
return !ttsLoaded || !playerObj.supportsPlayMedia;
|
||||||
|
}
|
||||||
|
|
||||||
|
computeTTSLoaded(hass) {
|
||||||
|
return isComponentLoaded(hass, 'tts');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTogglePower() {
|
||||||
|
this.playerObj.togglePower();
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePrevious() {
|
||||||
|
this.playerObj.previousTrack();
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePlaybackControl() {
|
||||||
|
this.playerObj.mediaPlayPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNext() {
|
||||||
|
this.playerObj.nextTrack();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSourceChanged(sourceIndex, sourceIndexOld) {
|
||||||
|
// Selected Option will transition to '' before transitioning to new value
|
||||||
|
if (!this.playerObj
|
||||||
|
|| !this.playerObj.supportsSelectSource
|
||||||
|
|| this.playerObj.sourceList === undefined
|
||||||
|
|| sourceIndex < 0
|
||||||
|
|| sourceIndex >= this.playerObj.sourceList
|
||||||
|
|| sourceIndexOld === undefined
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceInput = this.playerObj.sourceList[sourceIndex];
|
||||||
|
|
||||||
|
if (sourceInput === this.playerObj.source) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playerObj.selectSource(sourceInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleVolumeTap() {
|
||||||
|
if (!this.playerObj.supportsVolumeMute) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.playerObj.volumeMute(!this.playerObj.isMuted);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleVolumeUp() {
|
||||||
|
const obj = this.$.volumeUp;
|
||||||
|
this.handleVolumeWorker('volume_up', obj, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleVolumeDown() {
|
||||||
|
const obj = this.$.volumeDown;
|
||||||
|
this.handleVolumeWorker('volume_down', obj, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleVolumeWorker(service, obj, force) {
|
||||||
|
if (force || (obj !== undefined && obj.pointerDown)) {
|
||||||
|
this.playerObj.callService(service);
|
||||||
|
setTimeout(() => this.handleVolumeWorker(service, obj, false), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volumeSliderChanged(ev) {
|
||||||
|
const volPercentage = parseFloat(ev.target.value);
|
||||||
|
const volume = volPercentage > 0 ? volPercentage / 100 : 0;
|
||||||
|
this.playerObj.setVolume(volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
ttsCheckForEnter(ev) {
|
||||||
|
if (ev.keyCode === 13) this.sendTTS();
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTTS() {
|
||||||
|
const services = this.hass.config.services.tts;
|
||||||
|
const serviceKeys = Object.keys(services).sort();
|
||||||
|
let service;
|
||||||
|
let i;
|
||||||
|
|
||||||
|
for (i = 0; i < serviceKeys.length; i++) {
|
||||||
|
if (serviceKeys[i].indexOf('_say') !== -1) {
|
||||||
|
service = serviceKeys[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hass.callService('tts', service, {
|
||||||
|
entity_id: this.stateObj.entity_id,
|
||||||
|
message: this.ttsMessage,
|
||||||
|
});
|
||||||
|
this.ttsMessage = '';
|
||||||
|
this.$.ttsInput.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define('more-info-media_player', MoreInfoMediaPlayer);
|
||||||
|
@ -17,147 +17,145 @@ import isComponentLoaded from '../../common/config/is_component_loaded.js';
|
|||||||
import { DOMAINS_MORE_INFO_NO_HISTORY } from '../../common/const.js';
|
import { DOMAINS_MORE_INFO_NO_HISTORY } from '../../common/const.js';
|
||||||
import EventsMixin from '../../mixins/events-mixin.js';
|
import EventsMixin from '../../mixins/events-mixin.js';
|
||||||
|
|
||||||
{
|
const DOMAINS_NO_INFO = [
|
||||||
const DOMAINS_NO_INFO = [
|
'camera',
|
||||||
'camera',
|
'configurator',
|
||||||
'configurator',
|
'history_graph',
|
||||||
'history_graph',
|
];
|
||||||
];
|
/*
|
||||||
/*
|
* @appliesMixin EventsMixin
|
||||||
* @appliesMixin EventsMixin
|
*/
|
||||||
*/
|
class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||||
class MoreInfoControls extends EventsMixin(PolymerElement) {
|
static get template() {
|
||||||
static get template() {
|
return html`
|
||||||
return html`
|
<style include="ha-style-dialog">
|
||||||
<style include="ha-style-dialog">
|
app-toolbar {
|
||||||
app-toolbar {
|
color: var(--more-info-header-color);
|
||||||
color: var(--more-info-header-color);
|
background-color: var(--more-info-header-background);
|
||||||
background-color: var(--more-info-header-background);
|
}
|
||||||
|
|
||||||
|
app-toolbar [main-title] {
|
||||||
|
@apply --ha-more-info-app-toolbar-title;
|
||||||
|
}
|
||||||
|
|
||||||
|
state-card-content {
|
||||||
|
display: block;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
state-history-charts {
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 451px) and (min-height: 501px) {
|
||||||
|
.main-title {
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app-toolbar [main-title] {
|
:host([domain=camera]) paper-dialog-scrollable {
|
||||||
@apply --ha-more-info-app-toolbar-title;
|
margin: 0 -24px -5px;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
state-card-content {
|
<app-toolbar>
|
||||||
display: block;
|
<paper-icon-button icon="hass:close" dialog-dismiss=""></paper-icon-button>
|
||||||
padding: 16px;
|
<div class="main-title" main-title="" on-click="enlarge">[[_computeStateName(stateObj)]]</div>
|
||||||
}
|
<template is="dom-if" if="[[canConfigure]]">
|
||||||
|
<paper-icon-button icon="hass:settings" on-click="_gotoSettings"></paper-icon-button>
|
||||||
state-history-charts {
|
|
||||||
max-width: 100%;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (min-width: 451px) and (min-height: 501px) {
|
|
||||||
.main-title {
|
|
||||||
pointer-events: auto;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([domain=camera]) paper-dialog-scrollable {
|
|
||||||
margin: 0 -24px -5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<app-toolbar>
|
|
||||||
<paper-icon-button icon="hass:close" dialog-dismiss=""></paper-icon-button>
|
|
||||||
<div class="main-title" main-title="" on-click="enlarge">[[_computeStateName(stateObj)]]</div>
|
|
||||||
<template is="dom-if" if="[[canConfigure]]">
|
|
||||||
<paper-icon-button icon="hass:settings" on-click="_gotoSettings"></paper-icon-button>
|
|
||||||
</template>
|
|
||||||
</app-toolbar>
|
|
||||||
|
|
||||||
<template is="dom-if" if="[[_computeShowStateInfo(stateObj)]]" restamp="">
|
|
||||||
<state-card-content state-obj="[[stateObj]]" hass="[[hass]]" in-dialog=""></state-card-content>
|
|
||||||
</template>
|
</template>
|
||||||
<paper-dialog-scrollable dialog-element="[[dialogElement]]">
|
</app-toolbar>
|
||||||
<template is="dom-if" if="[[_computeShowHistoryComponent(hass, stateObj)]]" restamp="">
|
|
||||||
<ha-state-history-data hass="[[hass]]" filter-type="recent-entity" entity-id="[[stateObj.entity_id]]" data="{{_stateHistory}}" is-loading="{{_stateHistoryLoading}}" cache-config="[[_cacheConfig]]"></ha-state-history-data>
|
<template is="dom-if" if="[[_computeShowStateInfo(stateObj)]]" restamp="">
|
||||||
<state-history-charts hass="[[hass]]" history-data="[[_stateHistory]]" is-loading-data="[[_stateHistoryLoading]]" up-to-now no-single="[[large]]"></state-history-charts>
|
<state-card-content state-obj="[[stateObj]]" hass="[[hass]]" in-dialog=""></state-card-content>
|
||||||
</template>
|
</template>
|
||||||
<more-info-content state-obj="[[stateObj]]" hass="[[hass]]"></more-info-content>
|
<paper-dialog-scrollable dialog-element="[[dialogElement]]">
|
||||||
</paper-dialog-scrollable>
|
<template is="dom-if" if="[[_computeShowHistoryComponent(hass, stateObj)]]" restamp="">
|
||||||
|
<ha-state-history-data hass="[[hass]]" filter-type="recent-entity" entity-id="[[stateObj.entity_id]]" data="{{_stateHistory}}" is-loading="{{_stateHistoryLoading}}" cache-config="[[_cacheConfig]]"></ha-state-history-data>
|
||||||
|
<state-history-charts hass="[[hass]]" history-data="[[_stateHistory]]" is-loading-data="[[_stateHistoryLoading]]" up-to-now no-single="[[large]]"></state-history-charts>
|
||||||
|
</template>
|
||||||
|
<more-info-content state-obj="[[stateObj]]" hass="[[hass]]"></more-info-content>
|
||||||
|
</paper-dialog-scrollable>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
hass: Object,
|
hass: Object,
|
||||||
|
|
||||||
stateObj: {
|
stateObj: {
|
||||||
type: Object,
|
type: Object,
|
||||||
observer: '_stateObjChanged',
|
observer: '_stateObjChanged',
|
||||||
|
},
|
||||||
|
|
||||||
|
dialogElement: Object,
|
||||||
|
canConfigure: Boolean,
|
||||||
|
|
||||||
|
domain: {
|
||||||
|
type: String,
|
||||||
|
reflectToAttribute: true,
|
||||||
|
computed: '_computeDomain(stateObj)',
|
||||||
|
},
|
||||||
|
|
||||||
|
_stateHistory: Object,
|
||||||
|
_stateHistoryLoading: Boolean,
|
||||||
|
|
||||||
|
large: {
|
||||||
|
type: Boolean,
|
||||||
|
value: false,
|
||||||
|
notify: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
_cacheConfig: {
|
||||||
|
type: Object,
|
||||||
|
value: {
|
||||||
|
refresh: 60,
|
||||||
|
cacheKey: null,
|
||||||
|
hoursToShow: 24,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
dialogElement: Object,
|
enlarge() {
|
||||||
canConfigure: Boolean,
|
this.large = !this.large;
|
||||||
|
}
|
||||||
|
|
||||||
domain: {
|
_computeShowStateInfo(stateObj) {
|
||||||
type: String,
|
return !stateObj || !DOMAINS_NO_INFO.includes(computeStateDomain(stateObj));
|
||||||
reflectToAttribute: true,
|
}
|
||||||
computed: '_computeDomain(stateObj)',
|
|
||||||
},
|
|
||||||
|
|
||||||
_stateHistory: Object,
|
_computeShowHistoryComponent(hass, stateObj) {
|
||||||
_stateHistoryLoading: Boolean,
|
return hass && stateObj &&
|
||||||
|
isComponentLoaded(hass, 'history') &&
|
||||||
|
!DOMAINS_MORE_INFO_NO_HISTORY.includes(computeStateDomain(stateObj));
|
||||||
|
}
|
||||||
|
|
||||||
large: {
|
_computeDomain(stateObj) {
|
||||||
type: Boolean,
|
return stateObj ? computeStateDomain(stateObj) : '';
|
||||||
value: false,
|
}
|
||||||
notify: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
_cacheConfig: {
|
_computeStateName(stateObj) {
|
||||||
type: Object,
|
return stateObj ? computeStateName(stateObj) : '';
|
||||||
value: {
|
}
|
||||||
refresh: 60,
|
|
||||||
cacheKey: null,
|
_stateObjChanged(newVal) {
|
||||||
hoursToShow: 24,
|
if (!newVal) {
|
||||||
},
|
return;
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enlarge() {
|
if (this._cacheConfig.cacheKey !== `more_info.${newVal.entity_id}`) {
|
||||||
this.large = !this.large;
|
this._cacheConfig = Object.assign(
|
||||||
}
|
{}, this._cacheConfig,
|
||||||
|
{ cacheKey: `more_info.${newVal.entity_id}` }
|
||||||
_computeShowStateInfo(stateObj) {
|
);
|
||||||
return !stateObj || !DOMAINS_NO_INFO.includes(computeStateDomain(stateObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeShowHistoryComponent(hass, stateObj) {
|
|
||||||
return hass && stateObj &&
|
|
||||||
isComponentLoaded(hass, 'history') &&
|
|
||||||
!DOMAINS_MORE_INFO_NO_HISTORY.includes(computeStateDomain(stateObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeDomain(stateObj) {
|
|
||||||
return stateObj ? computeStateDomain(stateObj) : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeStateName(stateObj) {
|
|
||||||
return stateObj ? computeStateName(stateObj) : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
_stateObjChanged(newVal) {
|
|
||||||
if (!newVal) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._cacheConfig.cacheKey !== `more_info.${newVal.entity_id}`) {
|
|
||||||
this._cacheConfig = Object.assign(
|
|
||||||
{}, this._cacheConfig,
|
|
||||||
{ cacheKey: `more_info.${newVal.entity_id}` }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_gotoSettings() {
|
|
||||||
this.fire('more-info-page', { page: 'settings' });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customElements.define('more-info-controls', MoreInfoControls);
|
|
||||||
|
_gotoSettings() {
|
||||||
|
this.fire('more-info-page', { page: 'settings' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
customElements.define('more-info-controls', MoreInfoControls);
|
||||||
|
@ -17,117 +17,115 @@ import(/* webpackChunkName: "ha-sidebar" */ '../components/ha-sidebar.js');
|
|||||||
import(/* webpackChunkName: "more-info-dialog" */ '../dialogs/ha-more-info-dialog.js');
|
import(/* webpackChunkName: "more-info-dialog" */ '../dialogs/ha-more-info-dialog.js');
|
||||||
import(/* webpackChunkName: "voice-command-dialog" */ '../dialogs/ha-voice-command-dialog.js');
|
import(/* webpackChunkName: "voice-command-dialog" */ '../dialogs/ha-voice-command-dialog.js');
|
||||||
|
|
||||||
{
|
const NON_SWIPABLE_PANELS = ['kiosk', 'map'];
|
||||||
const NON_SWIPABLE_PANELS = ['kiosk', 'map'];
|
|
||||||
|
|
||||||
class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
||||||
static get template() {
|
static get template() {
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
:host {
|
:host {
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
/* remove the grey tap highlights in iOS on the fullscreen touch targets */
|
/* remove the grey tap highlights in iOS on the fullscreen touch targets */
|
||||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||||
}
|
}
|
||||||
iron-pages, ha-sidebar {
|
iron-pages, ha-sidebar {
|
||||||
/* allow a light tap highlight on the actual interface elements */
|
/* allow a light tap highlight on the actual interface elements */
|
||||||
-webkit-tap-highlight-color: rgba(0,0,0,0.1);
|
-webkit-tap-highlight-color: rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
iron-pages {
|
iron-pages {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<ha-more-info-dialog hass="[[hass]]"></ha-more-info-dialog>
|
<ha-more-info-dialog hass="[[hass]]"></ha-more-info-dialog>
|
||||||
<ha-url-sync hass="[[hass]]"></ha-url-sync>
|
<ha-url-sync hass="[[hass]]"></ha-url-sync>
|
||||||
<app-route route="{{route}}" pattern="/states" tail="{{statesRouteTail}}"></app-route>
|
<app-route route="{{route}}" pattern="/states" tail="{{statesRouteTail}}"></app-route>
|
||||||
<ha-voice-command-dialog hass="[[hass]]" id="voiceDialog"></ha-voice-command-dialog>
|
<ha-voice-command-dialog hass="[[hass]]" id="voiceDialog"></ha-voice-command-dialog>
|
||||||
<iron-media-query query="(max-width: 870px)" query-matches="{{narrow}}">
|
<iron-media-query query="(max-width: 870px)" query-matches="{{narrow}}">
|
||||||
</iron-media-query>
|
</iron-media-query>
|
||||||
|
|
||||||
<app-drawer-layout fullbleed="" force-narrow="[[computeForceNarrow(narrow, dockedSidebar)]]" responsive-width="0">
|
<app-drawer-layout fullbleed="" force-narrow="[[computeForceNarrow(narrow, dockedSidebar)]]" responsive-width="0">
|
||||||
<app-drawer id="drawer" slot="drawer" disable-swipe="[[_computeDisableSwipe(hass)]]" swipe-open="[[!_computeDisableSwipe(hass)]]" persistent="[[dockedSidebar]]">
|
<app-drawer id="drawer" slot="drawer" disable-swipe="[[_computeDisableSwipe(hass)]]" swipe-open="[[!_computeDisableSwipe(hass)]]" persistent="[[dockedSidebar]]">
|
||||||
<ha-sidebar narrow="[[narrow]]" hass="[[hass]]"></ha-sidebar>
|
<ha-sidebar narrow="[[narrow]]" hass="[[hass]]"></ha-sidebar>
|
||||||
</app-drawer>
|
</app-drawer>
|
||||||
|
|
||||||
<iron-pages attr-for-selected="id" fallback-selection="panel-resolver" selected="[[hass.panelUrl]]" selected-attribute="panel-visible">
|
<iron-pages attr-for-selected="id" fallback-selection="panel-resolver" selected="[[hass.panelUrl]]" selected-attribute="panel-visible">
|
||||||
<partial-cards id="states" narrow="[[narrow]]" hass="[[hass]]" show-menu="[[dockedSidebar]]" route="[[statesRouteTail]]" show-tabs=""></partial-cards>
|
<partial-cards id="states" narrow="[[narrow]]" hass="[[hass]]" show-menu="[[dockedSidebar]]" route="[[statesRouteTail]]" show-tabs=""></partial-cards>
|
||||||
|
|
||||||
<partial-panel-resolver id="panel-resolver" narrow="[[narrow]]" hass="[[hass]]" route="[[route]]" show-menu="[[dockedSidebar]]"></partial-panel-resolver>
|
<partial-panel-resolver id="panel-resolver" narrow="[[narrow]]" hass="[[hass]]" route="[[route]]" show-menu="[[dockedSidebar]]"></partial-panel-resolver>
|
||||||
|
|
||||||
</iron-pages>
|
</iron-pages>
|
||||||
</app-drawer-layout>
|
</app-drawer-layout>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
hass: Object,
|
hass: Object,
|
||||||
narrow: Boolean,
|
narrow: Boolean,
|
||||||
route: {
|
route: {
|
||||||
type: Object,
|
type: Object,
|
||||||
observer: '_routeChanged',
|
observer: '_routeChanged',
|
||||||
},
|
},
|
||||||
statesRouteTail: Object,
|
statesRouteTail: Object,
|
||||||
dockedSidebar: {
|
dockedSidebar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
computed: 'computeDockedSidebar(hass)',
|
computed: 'computeDockedSidebar(hass)',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
ready() {
|
||||||
super.ready();
|
super.ready();
|
||||||
this.addEventListener('hass-open-menu', () => this.handleOpenMenu());
|
this.addEventListener('hass-open-menu', () => this.handleOpenMenu());
|
||||||
this.addEventListener('hass-close-menu', () => this.handleCloseMenu());
|
this.addEventListener('hass-close-menu', () => this.handleCloseMenu());
|
||||||
this.addEventListener('hass-start-voice', ev => this.handleStartVoice(ev));
|
this.addEventListener('hass-start-voice', ev => this.handleStartVoice(ev));
|
||||||
}
|
}
|
||||||
|
|
||||||
_routeChanged() {
|
_routeChanged() {
|
||||||
if (this.narrow) {
|
if (this.narrow) {
|
||||||
this.$.drawer.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleStartVoice(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this.$.voiceDialog.opened = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOpenMenu() {
|
|
||||||
if (this.narrow) {
|
|
||||||
this.$.drawer.open();
|
|
||||||
} else {
|
|
||||||
this.fire('hass-dock-sidebar', { dock: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCloseMenu() {
|
|
||||||
this.$.drawer.close();
|
this.$.drawer.close();
|
||||||
if (this.dockedSidebar) {
|
|
||||||
this.fire('hass-dock-sidebar', { dock: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
window.removeInitMsg();
|
|
||||||
if (document.location.pathname === '/') {
|
|
||||||
this.navigate('/states', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
computeForceNarrow(narrow, dockedSidebar) {
|
|
||||||
return narrow || !dockedSidebar;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeDockedSidebar(hass) {
|
|
||||||
return hass.dockedSidebar;
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeDisableSwipe(hass) {
|
|
||||||
return NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('home-assistant-main', HomeAssistantMain);
|
handleStartVoice(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.$.voiceDialog.opened = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOpenMenu() {
|
||||||
|
if (this.narrow) {
|
||||||
|
this.$.drawer.open();
|
||||||
|
} else {
|
||||||
|
this.fire('hass-dock-sidebar', { dock: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseMenu() {
|
||||||
|
this.$.drawer.close();
|
||||||
|
if (this.dockedSidebar) {
|
||||||
|
this.fire('hass-dock-sidebar', { dock: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
window.removeInitMsg();
|
||||||
|
if (document.location.pathname === '/') {
|
||||||
|
this.navigate('/states', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
computeForceNarrow(narrow, dockedSidebar) {
|
||||||
|
return narrow || !dockedSidebar;
|
||||||
|
}
|
||||||
|
|
||||||
|
computeDockedSidebar(hass) {
|
||||||
|
return hass.dockedSidebar;
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeDisableSwipe(hass) {
|
||||||
|
return NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define('home-assistant-main', HomeAssistantMain);
|
||||||
|
@ -24,330 +24,328 @@ import computeLocationName from '../common/config/location_name.js';
|
|||||||
import NavigateMixin from '../mixins/navigate-mixin.js';
|
import NavigateMixin from '../mixins/navigate-mixin.js';
|
||||||
import EventsMixin from '../mixins/events-mixin.js';
|
import EventsMixin from '../mixins/events-mixin.js';
|
||||||
|
|
||||||
{
|
const DEFAULT_VIEW_ENTITY_ID = 'group.default_view';
|
||||||
const DEFAULT_VIEW_ENTITY_ID = 'group.default_view';
|
const ALWAYS_SHOW_DOMAIN = ['persistent_notification', 'configurator'];
|
||||||
const ALWAYS_SHOW_DOMAIN = ['persistent_notification', 'configurator'];
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @appliesMixin EventsMixin
|
* @appliesMixin EventsMixin
|
||||||
* @appliesMixin NavigateMixin
|
* @appliesMixin NavigateMixin
|
||||||
*/
|
*/
|
||||||
class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
|
class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
|
||||||
static get template() {
|
static get template() {
|
||||||
return html`
|
return html`
|
||||||
<style include="iron-flex iron-positioning ha-style">
|
<style include="iron-flex iron-positioning ha-style">
|
||||||
:host {
|
:host {
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-app-layout {
|
ha-app-layout {
|
||||||
background-color: var(--secondary-background-color, #E5E5E5);
|
background-color: var(--secondary-background-color, #E5E5E5);
|
||||||
}
|
}
|
||||||
|
|
||||||
paper-tabs {
|
paper-tabs {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
--paper-tabs-selection-bar-color: var(--text-primary-color, #FFF);
|
--paper-tabs-selection-bar-color: var(--text-primary-color, #FFF);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<app-route route="{{route}}" pattern="/:view" data="{{routeData}}" active="{{routeMatch}}"></app-route>
|
<app-route route="{{route}}" pattern="/:view" data="{{routeData}}" active="{{routeMatch}}"></app-route>
|
||||||
<ha-app-layout has-scrolling-region="" id="layout">
|
<ha-app-layout has-scrolling-region="" id="layout">
|
||||||
<app-header effects="waterfall" condenses="" fixed="" slot="header">
|
<app-header effects="waterfall" condenses="" fixed="" slot="header">
|
||||||
<app-toolbar>
|
<app-toolbar>
|
||||||
<ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button>
|
<ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button>
|
||||||
<div main-title="">[[computeTitle(views, defaultView, locationName)]]</div>
|
<div main-title="">[[computeTitle(views, defaultView, locationName)]]</div>
|
||||||
<ha-start-voice-button hass="[[hass]]"></ha-start-voice-button>
|
<ha-start-voice-button hass="[[hass]]"></ha-start-voice-button>
|
||||||
</app-toolbar>
|
</app-toolbar>
|
||||||
|
|
||||||
<div sticky="" hidden\$="[[areTabsHidden(views, showTabs)]]">
|
<div sticky="" hidden\$="[[areTabsHidden(views, showTabs)]]">
|
||||||
<paper-tabs scrollable="" selected="[[currentView]]" attr-for-selected="data-entity" on-iron-activate="handleViewSelected">
|
<paper-tabs scrollable="" selected="[[currentView]]" attr-for-selected="data-entity" on-iron-activate="handleViewSelected">
|
||||||
<paper-tab data-entity="" on-click="scrollToTop">
|
<paper-tab data-entity="" on-click="scrollToTop">
|
||||||
<template is="dom-if" if="[[!defaultView]]">
|
<template is="dom-if" if="[[!defaultView]]">
|
||||||
Home
|
Home
|
||||||
|
</template>
|
||||||
|
<template is="dom-if" if="[[defaultView]]">
|
||||||
|
<template is="dom-if" if="[[defaultView.attributes.icon]]">
|
||||||
|
<iron-icon title\$="[[_computeStateName(defaultView)]]" icon="[[defaultView.attributes.icon]]"></iron-icon>
|
||||||
</template>
|
</template>
|
||||||
<template is="dom-if" if="[[defaultView]]">
|
<template is="dom-if" if="[[!defaultView.attributes.icon]]">
|
||||||
<template is="dom-if" if="[[defaultView.attributes.icon]]">
|
[[_computeStateName(defaultView)]]
|
||||||
<iron-icon title\$="[[_computeStateName(defaultView)]]" icon="[[defaultView.attributes.icon]]"></iron-icon>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template is="dom-if" if="[[!defaultView.attributes.icon]]">
|
</paper-tab>
|
||||||
[[_computeStateName(defaultView)]]
|
<template is="dom-repeat" items="[[views]]">
|
||||||
</template>
|
<paper-tab data-entity\$="[[item.entity_id]]" on-click="scrollToTop">
|
||||||
|
<template is="dom-if" if="[[item.attributes.icon]]">
|
||||||
|
<iron-icon title\$="[[_computeStateName(item)]]" icon="[[item.attributes.icon]]"></iron-icon>
|
||||||
|
</template>
|
||||||
|
<template is="dom-if" if="[[!item.attributes.icon]]">
|
||||||
|
[[_computeStateName(item)]]
|
||||||
</template>
|
</template>
|
||||||
</paper-tab>
|
</paper-tab>
|
||||||
<template is="dom-repeat" items="[[views]]">
|
</template>
|
||||||
<paper-tab data-entity\$="[[item.entity_id]]" on-click="scrollToTop">
|
</paper-tabs>
|
||||||
<template is="dom-if" if="[[item.attributes.icon]]">
|
</div>
|
||||||
<iron-icon title\$="[[_computeStateName(item)]]" icon="[[item.attributes.icon]]"></iron-icon>
|
</app-header>
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[!item.attributes.icon]]">
|
|
||||||
[[_computeStateName(item)]]
|
|
||||||
</template>
|
|
||||||
</paper-tab>
|
|
||||||
</template>
|
|
||||||
</paper-tabs>
|
|
||||||
</div>
|
|
||||||
</app-header>
|
|
||||||
|
|
||||||
<iron-pages attr-for-selected="data-view" selected="[[currentView]]" selected-attribute="view-visible">
|
<iron-pages attr-for-selected="data-view" selected="[[currentView]]" selected-attribute="view-visible">
|
||||||
<ha-cards data-view="" states="[[viewStates]]" columns="[[_columns]]" hass="[[hass]]" panel-visible="[[panelVisible]]" ordered-group-entities="[[orderedGroupEntities]]"></ha-cards>
|
<ha-cards data-view="" states="[[viewStates]]" columns="[[_columns]]" hass="[[hass]]" panel-visible="[[panelVisible]]" ordered-group-entities="[[orderedGroupEntities]]"></ha-cards>
|
||||||
|
|
||||||
<template is="dom-repeat" items="[[views]]">
|
<template is="dom-repeat" items="[[views]]">
|
||||||
<ha-cards data-view\$="[[item.entity_id]]" states="[[viewStates]]" columns="[[_columns]]" hass="[[hass]]" panel-visible="[[panelVisible]]" ordered-group-entities="[[orderedGroupEntities]]"></ha-cards>
|
<ha-cards data-view\$="[[item.entity_id]]" states="[[viewStates]]" columns="[[_columns]]" hass="[[hass]]" panel-visible="[[panelVisible]]" ordered-group-entities="[[orderedGroupEntities]]"></ha-cards>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</iron-pages>
|
</iron-pages>
|
||||||
</ha-app-layout>
|
</ha-app-layout>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
|
||||||
hass: {
|
hass: {
|
||||||
type: Object,
|
type: Object,
|
||||||
value: null,
|
value: null,
|
||||||
observer: 'hassChanged'
|
observer: 'hassChanged'
|
||||||
},
|
},
|
||||||
|
|
||||||
narrow: {
|
narrow: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
value: false,
|
value: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
showMenu: {
|
showMenu: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
observer: 'handleWindowChange',
|
observer: 'handleWindowChange',
|
||||||
},
|
},
|
||||||
|
|
||||||
panelVisible: {
|
panelVisible: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
value: false,
|
value: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
route: Object,
|
route: Object,
|
||||||
routeData: Object,
|
routeData: Object,
|
||||||
routeMatch: Boolean,
|
routeMatch: Boolean,
|
||||||
|
|
||||||
_columns: {
|
_columns: {
|
||||||
type: Number,
|
type: Number,
|
||||||
value: 1,
|
value: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
locationName: {
|
locationName: {
|
||||||
type: String,
|
type: String,
|
||||||
value: '',
|
value: '',
|
||||||
computed: '_computeLocationName(hass)',
|
computed: '_computeLocationName(hass)',
|
||||||
},
|
},
|
||||||
|
|
||||||
currentView: {
|
currentView: {
|
||||||
type: String,
|
type: String,
|
||||||
computed: '_computeCurrentView(hass, routeMatch, routeData)',
|
computed: '_computeCurrentView(hass, routeMatch, routeData)',
|
||||||
},
|
},
|
||||||
|
|
||||||
views: {
|
views: {
|
||||||
type: Array,
|
type: Array,
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultView: {
|
defaultView: {
|
||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
|
|
||||||
viewStates: {
|
viewStates: {
|
||||||
type: Object,
|
type: Object,
|
||||||
computed: 'computeViewStates(currentView, hass, defaultView)',
|
computed: 'computeViewStates(currentView, hass, defaultView)',
|
||||||
},
|
},
|
||||||
|
|
||||||
orderedGroupEntities: {
|
orderedGroupEntities: {
|
||||||
type: Array,
|
type: Array,
|
||||||
computed: 'computeOrderedGroupEntities(currentView, hass, defaultView)',
|
computed: 'computeOrderedGroupEntities(currentView, hass, defaultView)',
|
||||||
},
|
},
|
||||||
|
|
||||||
showTabs: {
|
showTabs: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
value: false,
|
value: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
ready() {
|
||||||
this.handleWindowChange = this.handleWindowChange.bind(this);
|
this.handleWindowChange = this.handleWindowChange.bind(this);
|
||||||
this.mqls = [300, 600, 900, 1200].map(width => matchMedia(`(min-width: ${width}px)`));
|
this.mqls = [300, 600, 900, 1200].map(width => matchMedia(`(min-width: ${width}px)`));
|
||||||
super.ready();
|
super.ready();
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.mqls.forEach(mql => mql.addListener(this.handleWindowChange));
|
this.mqls.forEach(mql => mql.addListener(this.handleWindowChange));
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.mqls.forEach(mql => mql.removeListener(this.handleWindowChange));
|
this.mqls.forEach(mql => mql.removeListener(this.handleWindowChange));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWindowChange() {
|
handleWindowChange() {
|
||||||
const matchColumns = this.mqls.reduce((cols, mql) => cols + mql.matches, 0);
|
const matchColumns = this.mqls.reduce((cols, mql) => cols + mql.matches, 0);
|
||||||
// Do -1 column if the menu is docked and open
|
// Do -1 column if the menu is docked and open
|
||||||
this._columns = Math.max(1, matchColumns - (!this.narrow && this.showMenu));
|
this._columns = Math.max(1, matchColumns - (!this.narrow && this.showMenu));
|
||||||
}
|
}
|
||||||
|
|
||||||
areTabsHidden(views, showTabs) {
|
areTabsHidden(views, showTabs) {
|
||||||
return !views || !views.length || !showTabs;
|
return !views || !views.length || !showTabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll to a specific y coordinate.
|
* Scroll to a specific y coordinate.
|
||||||
*
|
*
|
||||||
* Copied from paper-scroll-header-panel.
|
* Copied from paper-scroll-header-panel.
|
||||||
*
|
*
|
||||||
* @method scroll
|
* @method scroll
|
||||||
* @param {number} top The coordinate to scroll to, along the y-axis.
|
* @param {number} top The coordinate to scroll to, along the y-axis.
|
||||||
* @param {boolean} smooth true if the scroll position should be smoothly adjusted.
|
* @param {boolean} smooth true if the scroll position should be smoothly adjusted.
|
||||||
*/
|
*/
|
||||||
scrollToTop() {
|
scrollToTop() {
|
||||||
// the scroll event will trigger _updateScrollState directly,
|
// the scroll event will trigger _updateScrollState directly,
|
||||||
// However, _updateScrollState relies on the previous `scrollTop` to update the states.
|
// However, _updateScrollState relies on the previous `scrollTop` to update the states.
|
||||||
// Calling _updateScrollState will ensure that the states are synced correctly.
|
// Calling _updateScrollState will ensure that the states are synced correctly.
|
||||||
var top = 0;
|
var top = 0;
|
||||||
var scroller = this.$.layout.header.scrollTarget;
|
var scroller = this.$.layout.header.scrollTarget;
|
||||||
var easingFn = function easeOutQuad(t, b, c, d) {
|
var easingFn = function easeOutQuad(t, b, c, d) {
|
||||||
/* eslint-disable no-param-reassign, space-infix-ops, no-mixed-operators */
|
/* eslint-disable no-param-reassign, space-infix-ops, no-mixed-operators */
|
||||||
t /= d;
|
t /= d;
|
||||||
return -c * t*(t-2) + b;
|
return -c * t*(t-2) + b;
|
||||||
/* eslint-enable no-param-reassign, space-infix-ops, no-mixed-operators */
|
/* eslint-enable no-param-reassign, space-infix-ops, no-mixed-operators */
|
||||||
};
|
};
|
||||||
var animationId = Math.random();
|
var animationId = Math.random();
|
||||||
var duration = 200;
|
var duration = 200;
|
||||||
var startTime = Date.now();
|
var startTime = Date.now();
|
||||||
var currentScrollTop = scroller.scrollTop;
|
var currentScrollTop = scroller.scrollTop;
|
||||||
var deltaScrollTop = top - currentScrollTop;
|
var deltaScrollTop = top - currentScrollTop;
|
||||||
this._currentAnimationId = animationId;
|
this._currentAnimationId = animationId;
|
||||||
(function updateFrame() {
|
(function updateFrame() {
|
||||||
var now = Date.now();
|
var now = Date.now();
|
||||||
var elapsedTime = now - startTime;
|
var elapsedTime = now - startTime;
|
||||||
if (elapsedTime > duration) {
|
if (elapsedTime > duration) {
|
||||||
scroller.scrollTop = top;
|
scroller.scrollTop = top;
|
||||||
} else if (this._currentAnimationId === animationId) {
|
} else if (this._currentAnimationId === animationId) {
|
||||||
scroller.scrollTop = easingFn(elapsedTime, currentScrollTop, deltaScrollTop, duration);
|
scroller.scrollTop = easingFn(elapsedTime, currentScrollTop, deltaScrollTop, duration);
|
||||||
requestAnimationFrame(updateFrame.bind(this));
|
requestAnimationFrame(updateFrame.bind(this));
|
||||||
}
|
|
||||||
}).call(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleViewSelected(ev) {
|
|
||||||
const view = ev.detail.item.getAttribute('data-entity') || null;
|
|
||||||
|
|
||||||
if (view !== this.currentView) {
|
|
||||||
let path = '/states';
|
|
||||||
if (view) {
|
|
||||||
path += '/' + view;
|
|
||||||
}
|
|
||||||
this.navigate(path);
|
|
||||||
}
|
}
|
||||||
}
|
}).call(this);
|
||||||
|
}
|
||||||
|
|
||||||
_computeCurrentView(hass, routeMatch, routeData) {
|
handleViewSelected(ev) {
|
||||||
if (!routeMatch) return '';
|
const view = ev.detail.item.getAttribute('data-entity') || null;
|
||||||
if (!hass.states[routeData.view] || !hass.states[routeData.view].attributes.view) {
|
|
||||||
return '';
|
if (view !== this.currentView) {
|
||||||
|
let path = '/states';
|
||||||
|
if (view) {
|
||||||
|
path += '/' + view;
|
||||||
}
|
}
|
||||||
return routeData.view;
|
this.navigate(path);
|
||||||
}
|
|
||||||
|
|
||||||
computeTitle(views, defaultView, locationName) {
|
|
||||||
return (views && views.length > 0 && !defaultView && locationName === 'Home') || !locationName ? 'Home Assistant' : locationName;
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeStateName(stateObj) {
|
|
||||||
return computeStateName(stateObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeLocationName(hass) {
|
|
||||||
return computeLocationName(hass);
|
|
||||||
}
|
|
||||||
|
|
||||||
hassChanged(hass) {
|
|
||||||
if (!hass) return;
|
|
||||||
const views = extractViews(hass.states);
|
|
||||||
let defaultView = null;
|
|
||||||
// If default view present, it's in first index.
|
|
||||||
if (views.length > 0 && views[0].entity_id === DEFAULT_VIEW_ENTITY_ID) {
|
|
||||||
defaultView = views.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setProperties({ views, defaultView });
|
|
||||||
}
|
|
||||||
|
|
||||||
isView(currentView, defaultView) {
|
|
||||||
return (currentView || defaultView) &&
|
|
||||||
this.hass.states[currentView || DEFAULT_VIEW_ENTITY_ID];
|
|
||||||
}
|
|
||||||
|
|
||||||
_defaultViewFilter(hass, entityId) {
|
|
||||||
// Filter out hidden
|
|
||||||
return !hass.states[entityId].attributes.hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeDefaultViewStates(hass, entityIds) {
|
|
||||||
const states = {};
|
|
||||||
entityIds.filter(this._defaultViewFilter.bind(null, hass)).forEach((entityId) => {
|
|
||||||
states[entityId] = hass.states[entityId];
|
|
||||||
});
|
|
||||||
return states;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Compute the states to show for current view.
|
|
||||||
|
|
||||||
Will make sure we always show entities from ALWAYS_SHOW_DOMAINS domains.
|
|
||||||
*/
|
|
||||||
computeViewStates(currentView, hass, defaultView) {
|
|
||||||
const entityIds = Object.keys(hass.states);
|
|
||||||
|
|
||||||
// If we base off all entities, only have to filter out hidden
|
|
||||||
if (!this.isView(currentView, defaultView)) {
|
|
||||||
return this._computeDefaultViewStates(hass, entityIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
let states;
|
|
||||||
if (currentView) {
|
|
||||||
states = getViewEntities(hass.states, hass.states[currentView]);
|
|
||||||
} else {
|
|
||||||
states = getViewEntities(hass.states, hass.states[DEFAULT_VIEW_ENTITY_ID]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure certain domains are always shown.
|
|
||||||
entityIds.forEach((entityId) => {
|
|
||||||
const state = hass.states[entityId];
|
|
||||||
|
|
||||||
if (ALWAYS_SHOW_DOMAIN.includes(computeStateDomain(state))) {
|
|
||||||
states[entityId] = state;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return states;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Compute the ordered list of groups for this view
|
|
||||||
*/
|
|
||||||
computeOrderedGroupEntities(currentView, hass, defaultView) {
|
|
||||||
if (!this.isView(currentView, defaultView)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var orderedGroupEntities = {};
|
|
||||||
var entitiesList = hass.states[currentView || DEFAULT_VIEW_ENTITY_ID].attributes.entity_id;
|
|
||||||
|
|
||||||
for (var i = 0; i < entitiesList.length; i++) {
|
|
||||||
orderedGroupEntities[entitiesList[i]] = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return orderedGroupEntities;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('partial-cards', PartialCards);
|
_computeCurrentView(hass, routeMatch, routeData) {
|
||||||
|
if (!routeMatch) return '';
|
||||||
|
if (!hass.states[routeData.view] || !hass.states[routeData.view].attributes.view) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return routeData.view;
|
||||||
|
}
|
||||||
|
|
||||||
|
computeTitle(views, defaultView, locationName) {
|
||||||
|
return (views && views.length > 0 && !defaultView && locationName === 'Home') || !locationName ? 'Home Assistant' : locationName;
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeStateName(stateObj) {
|
||||||
|
return computeStateName(stateObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeLocationName(hass) {
|
||||||
|
return computeLocationName(hass);
|
||||||
|
}
|
||||||
|
|
||||||
|
hassChanged(hass) {
|
||||||
|
if (!hass) return;
|
||||||
|
const views = extractViews(hass.states);
|
||||||
|
let defaultView = null;
|
||||||
|
// If default view present, it's in first index.
|
||||||
|
if (views.length > 0 && views[0].entity_id === DEFAULT_VIEW_ENTITY_ID) {
|
||||||
|
defaultView = views.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setProperties({ views, defaultView });
|
||||||
|
}
|
||||||
|
|
||||||
|
isView(currentView, defaultView) {
|
||||||
|
return (currentView || defaultView) &&
|
||||||
|
this.hass.states[currentView || DEFAULT_VIEW_ENTITY_ID];
|
||||||
|
}
|
||||||
|
|
||||||
|
_defaultViewFilter(hass, entityId) {
|
||||||
|
// Filter out hidden
|
||||||
|
return !hass.states[entityId].attributes.hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeDefaultViewStates(hass, entityIds) {
|
||||||
|
const states = {};
|
||||||
|
entityIds.filter(this._defaultViewFilter.bind(null, hass)).forEach((entityId) => {
|
||||||
|
states[entityId] = hass.states[entityId];
|
||||||
|
});
|
||||||
|
return states;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Compute the states to show for current view.
|
||||||
|
|
||||||
|
Will make sure we always show entities from ALWAYS_SHOW_DOMAINS domains.
|
||||||
|
*/
|
||||||
|
computeViewStates(currentView, hass, defaultView) {
|
||||||
|
const entityIds = Object.keys(hass.states);
|
||||||
|
|
||||||
|
// If we base off all entities, only have to filter out hidden
|
||||||
|
if (!this.isView(currentView, defaultView)) {
|
||||||
|
return this._computeDefaultViewStates(hass, entityIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
let states;
|
||||||
|
if (currentView) {
|
||||||
|
states = getViewEntities(hass.states, hass.states[currentView]);
|
||||||
|
} else {
|
||||||
|
states = getViewEntities(hass.states, hass.states[DEFAULT_VIEW_ENTITY_ID]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure certain domains are always shown.
|
||||||
|
entityIds.forEach((entityId) => {
|
||||||
|
const state = hass.states[entityId];
|
||||||
|
|
||||||
|
if (ALWAYS_SHOW_DOMAIN.includes(computeStateDomain(state))) {
|
||||||
|
states[entityId] = state;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return states;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Compute the ordered list of groups for this view
|
||||||
|
*/
|
||||||
|
computeOrderedGroupEntities(currentView, hass, defaultView) {
|
||||||
|
if (!this.isView(currentView, defaultView)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderedGroupEntities = {};
|
||||||
|
var entitiesList = hass.states[currentView || DEFAULT_VIEW_ENTITY_ID].attributes.entity_id;
|
||||||
|
|
||||||
|
for (var i = 0; i < entitiesList.length; i++) {
|
||||||
|
orderedGroupEntities[entitiesList[i]] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderedGroupEntities;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define('partial-cards', PartialCards);
|
||||||
|
@ -12,100 +12,98 @@ import './ha-config-cloud-login.js';
|
|||||||
import './ha-config-cloud-register.js';
|
import './ha-config-cloud-register.js';
|
||||||
import NavigateMixin from '../../../mixins/navigate-mixin.js';
|
import NavigateMixin from '../../../mixins/navigate-mixin.js';
|
||||||
|
|
||||||
{
|
const LOGGED_IN_URLS = [
|
||||||
const LOGGED_IN_URLS = [
|
'/cloud/account',
|
||||||
'/cloud/account',
|
];
|
||||||
];
|
const NOT_LOGGED_IN_URLS = [
|
||||||
const NOT_LOGGED_IN_URLS = [
|
'/cloud/login',
|
||||||
'/cloud/login',
|
'/cloud/register',
|
||||||
'/cloud/register',
|
'/cloud/forgot-password',
|
||||||
'/cloud/forgot-password',
|
];
|
||||||
];
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @appliesMixin NavigateMixin
|
* @appliesMixin NavigateMixin
|
||||||
*/
|
*/
|
||||||
class HaConfigCloud extends NavigateMixin(PolymerElement) {
|
class HaConfigCloud extends NavigateMixin(PolymerElement) {
|
||||||
static get template() {
|
static get template() {
|
||||||
return html`
|
return html`
|
||||||
<app-route route="[[route]]" pattern="/cloud/:page" data="{{_routeData}}" tail="{{_routeTail}}"></app-route>
|
<app-route route="[[route]]" pattern="/cloud/:page" data="{{_routeData}}" tail="{{_routeTail}}"></app-route>
|
||||||
|
|
||||||
<template is="dom-if" if="[[_equals(_routeData.page, "account")]]" restamp="">
|
<template is="dom-if" if="[[_equals(_routeData.page, "account")]]" restamp="">
|
||||||
<ha-config-cloud-account hass="[[hass]]" account="[[account]]" is-wide="[[isWide]]"></ha-config-cloud-account>
|
<ha-config-cloud-account hass="[[hass]]" account="[[account]]" is-wide="[[isWide]]"></ha-config-cloud-account>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template is="dom-if" if="[[_equals(_routeData.page, "login")]]" restamp="">
|
<template is="dom-if" if="[[_equals(_routeData.page, "login")]]" restamp="">
|
||||||
<ha-config-cloud-login page-name="login" hass="[[hass]]" is-wide="[[isWide]]" email="{{_loginEmail}}" flash-message="{{_flashMessage}}"></ha-config-cloud-login>
|
<ha-config-cloud-login page-name="login" hass="[[hass]]" is-wide="[[isWide]]" email="{{_loginEmail}}" flash-message="{{_flashMessage}}"></ha-config-cloud-login>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template is="dom-if" if="[[_equals(_routeData.page, "register")]]" restamp="">
|
<template is="dom-if" if="[[_equals(_routeData.page, "register")]]" restamp="">
|
||||||
<ha-config-cloud-register page-name="register" hass="[[hass]]" is-wide="[[isWide]]" email="{{_loginEmail}}"></ha-config-cloud-register>
|
<ha-config-cloud-register page-name="register" hass="[[hass]]" is-wide="[[isWide]]" email="{{_loginEmail}}"></ha-config-cloud-register>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template is="dom-if" if="[[_equals(_routeData.page, "forgot-password")]]" restamp="">
|
<template is="dom-if" if="[[_equals(_routeData.page, "forgot-password")]]" restamp="">
|
||||||
<ha-config-cloud-forgot-password page-name="forgot-password" hass="[[hass]]" email="{{_loginEmail}}"></ha-config-cloud-forgot-password>
|
<ha-config-cloud-forgot-password page-name="forgot-password" hass="[[hass]]" email="{{_loginEmail}}"></ha-config-cloud-forgot-password>
|
||||||
</template>
|
</template>
|
||||||
`;
|
`;
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
isWide: Boolean,
|
|
||||||
loadingAccount: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false
|
|
||||||
},
|
|
||||||
account: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
_flashMessage: {
|
|
||||||
type: String,
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
|
|
||||||
route: Object,
|
|
||||||
|
|
||||||
_routeData: Object,
|
|
||||||
_routeTail: Object,
|
|
||||||
_loginEmail: String,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static get observers() {
|
|
||||||
return [
|
|
||||||
'_checkRoute(route, account)'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
this.addEventListener('cloud-done', (ev) => {
|
|
||||||
this._flashMessage = ev.detail.flashMessage;
|
|
||||||
this.navigate('/config/cloud/login');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_checkRoute(route) {
|
|
||||||
if (!route || route.path.substr(0, 6) !== '/cloud') return;
|
|
||||||
|
|
||||||
this._debouncer = Debouncer.debounce(
|
|
||||||
this._debouncer,
|
|
||||||
timeOut.after(0),
|
|
||||||
() => {
|
|
||||||
if (!this.account && !NOT_LOGGED_IN_URLS.includes(route.path)) {
|
|
||||||
this.navigate('/config/cloud/login', true);
|
|
||||||
} else if (this.account && !LOGGED_IN_URLS.includes(route.path)) {
|
|
||||||
this.navigate('/config/cloud/account', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_equals(a, b) {
|
|
||||||
return a === b;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('ha-config-cloud', HaConfigCloud);
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: Object,
|
||||||
|
isWide: Boolean,
|
||||||
|
loadingAccount: {
|
||||||
|
type: Boolean,
|
||||||
|
value: false
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
_flashMessage: {
|
||||||
|
type: String,
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
route: Object,
|
||||||
|
|
||||||
|
_routeData: Object,
|
||||||
|
_routeTail: Object,
|
||||||
|
_loginEmail: String,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get observers() {
|
||||||
|
return [
|
||||||
|
'_checkRoute(route, account)'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
ready() {
|
||||||
|
super.ready();
|
||||||
|
this.addEventListener('cloud-done', (ev) => {
|
||||||
|
this._flashMessage = ev.detail.flashMessage;
|
||||||
|
this.navigate('/config/cloud/login');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_checkRoute(route) {
|
||||||
|
if (!route || route.path.substr(0, 6) !== '/cloud') return;
|
||||||
|
|
||||||
|
this._debouncer = Debouncer.debounce(
|
||||||
|
this._debouncer,
|
||||||
|
timeOut.after(0),
|
||||||
|
() => {
|
||||||
|
if (!this.account && !NOT_LOGGED_IN_URLS.includes(route.path)) {
|
||||||
|
this.navigate('/config/cloud/login', true);
|
||||||
|
} else if (this.account && !LOGGED_IN_URLS.includes(route.path)) {
|
||||||
|
this.navigate('/config/cloud/account', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_equals(a, b) {
|
||||||
|
return a === b;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define('ha-config-cloud', HaConfigCloud);
|
||||||
|
@ -13,192 +13,190 @@ import './ha-config-flow.js';
|
|||||||
import EventsMixin from '../../../mixins/events-mixin.js';
|
import EventsMixin from '../../../mixins/events-mixin.js';
|
||||||
import LocalizeMixin from '../../../mixins/localize-mixin.js';
|
import LocalizeMixin from '../../../mixins/localize-mixin.js';
|
||||||
|
|
||||||
{
|
/*
|
||||||
/*
|
* @appliesMixin LocalizeMixin
|
||||||
* @appliesMixin LocalizeMixin
|
* @appliesMixin EventsMixin
|
||||||
* @appliesMixin EventsMixin
|
*/
|
||||||
*/
|
class HaConfigManager extends
|
||||||
class HaConfigManager extends
|
LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||||
LocalizeMixin(EventsMixin(PolymerElement)) {
|
static get template() {
|
||||||
static get template() {
|
return html`
|
||||||
return html`
|
<style include="iron-flex ha-style">
|
||||||
<style include="iron-flex ha-style">
|
paper-button {
|
||||||
paper-button {
|
color: var(--primary-color);
|
||||||
color: var(--primary-color);
|
font-weight: 500;
|
||||||
font-weight: 500;
|
top: 3px;
|
||||||
top: 3px;
|
margin-right: -.57em;
|
||||||
margin-right: -.57em;
|
}
|
||||||
}
|
paper-card:last-child {
|
||||||
paper-card:last-child {
|
margin-top: 12px;
|
||||||
margin-top: 12px;
|
}
|
||||||
}
|
.config-entry-row {
|
||||||
.config-entry-row {
|
display: flex;
|
||||||
display: flex;
|
padding: 0 16px;
|
||||||
padding: 0 16px;
|
}
|
||||||
}
|
</style>
|
||||||
</style>
|
|
||||||
|
|
||||||
<hass-subpage header="Integrations">
|
|
||||||
<template is="dom-if" if="[[_progress.length]]">
|
|
||||||
<ha-config-section is-wide="[[isWide]]">
|
|
||||||
<span slot="header">In Progress</span>
|
|
||||||
<paper-card>
|
|
||||||
<template is="dom-repeat" items="[[_progress]]">
|
|
||||||
<div class="config-entry-row">
|
|
||||||
<paper-item-body>
|
|
||||||
[[_computeIntegrationTitle(localize, item.handler)]]
|
|
||||||
</paper-item-body>
|
|
||||||
<paper-button on-click="_continueFlow">Configure</paper-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</paper-card>
|
|
||||||
</ha-config-section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
<hass-subpage header="Integrations">
|
||||||
|
<template is="dom-if" if="[[_progress.length]]">
|
||||||
<ha-config-section is-wide="[[isWide]]">
|
<ha-config-section is-wide="[[isWide]]">
|
||||||
<span slot="header">Configured</span>
|
<span slot="header">In Progress</span>
|
||||||
<paper-card>
|
<paper-card>
|
||||||
<template is="dom-if" if="[[!_entries.length]]">
|
<template is="dom-repeat" items="[[_progress]]">
|
||||||
<div class="config-entry-row">
|
<div class="config-entry-row">
|
||||||
<paper-item-body>
|
<paper-item-body>
|
||||||
Nothing configured yet
|
[[_computeIntegrationTitle(localize, item.handler)]]
|
||||||
</paper-item-body>
|
</paper-item-body>
|
||||||
</div>
|
<paper-button on-click="_continueFlow">Configure</paper-button>
|
||||||
</template>
|
|
||||||
<template is="dom-repeat" items="[[_entries]]">
|
|
||||||
<div class="config-entry-row">
|
|
||||||
<paper-item-body three-line="">
|
|
||||||
[[item.title]]
|
|
||||||
<div secondary="">Integration: [[_computeIntegrationTitle(localize, item.domain)]]</div>
|
|
||||||
<div secondary="">Added by: [[item.source]]</div>
|
|
||||||
<div secondary="">State: [[item.state]]</div>
|
|
||||||
</paper-item-body>
|
|
||||||
<paper-button on-click="_removeEntry">Remove</paper-button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</paper-card>
|
</paper-card>
|
||||||
</ha-config-section>
|
</ha-config-section>
|
||||||
|
</template>
|
||||||
|
|
||||||
<ha-config-section is-wide="[[isWide]]">
|
<ha-config-section is-wide="[[isWide]]">
|
||||||
<span slot="header">Available</span>
|
<span slot="header">Configured</span>
|
||||||
<paper-card>
|
<paper-card>
|
||||||
<template is="dom-repeat" items="[[_handlers]]">
|
<template is="dom-if" if="[[!_entries.length]]">
|
||||||
<div class="config-entry-row">
|
<div class="config-entry-row">
|
||||||
<paper-item-body>
|
<paper-item-body>
|
||||||
[[_computeIntegrationTitle(localize, item)]]
|
Nothing configured yet
|
||||||
</paper-item-body>
|
</paper-item-body>
|
||||||
<paper-button on-click="_createFlow">Configure</paper-button>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
<template is="dom-repeat" items="[[_entries]]">
|
||||||
</paper-card>
|
<div class="config-entry-row">
|
||||||
</ha-config-section>
|
<paper-item-body three-line="">
|
||||||
</hass-subpage>
|
[[item.title]]
|
||||||
|
<div secondary="">Integration: [[_computeIntegrationTitle(localize, item.domain)]]</div>
|
||||||
|
<div secondary="">Added by: [[item.source]]</div>
|
||||||
|
<div secondary="">State: [[item.state]]</div>
|
||||||
|
</paper-item-body>
|
||||||
|
<paper-button on-click="_removeEntry">Remove</paper-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</paper-card>
|
||||||
|
</ha-config-section>
|
||||||
|
|
||||||
<ha-config-flow hass="[[hass]]" flow-id="[[_flowId]]" step="{{_flowStep}}" on-flow-closed="_flowClose"></ha-config-flow>
|
<ha-config-section is-wide="[[isWide]]">
|
||||||
|
<span slot="header">Available</span>
|
||||||
|
<paper-card>
|
||||||
|
<template is="dom-repeat" items="[[_handlers]]">
|
||||||
|
<div class="config-entry-row">
|
||||||
|
<paper-item-body>
|
||||||
|
[[_computeIntegrationTitle(localize, item)]]
|
||||||
|
</paper-item-body>
|
||||||
|
<paper-button on-click="_createFlow">Configure</paper-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</paper-card>
|
||||||
|
</ha-config-section>
|
||||||
|
</hass-subpage>
|
||||||
|
|
||||||
|
<ha-config-flow hass="[[hass]]" flow-id="[[_flowId]]" step="{{_flowStep}}" on-flow-closed="_flowClose"></ha-config-flow>
|
||||||
`;
|
`;
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
isWide: Boolean,
|
|
||||||
|
|
||||||
_flowId: {
|
|
||||||
type: String,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
* The step of the current selected flow, if available.
|
|
||||||
*/
|
|
||||||
_flowStep: Object,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Existing entries.
|
|
||||||
*/
|
|
||||||
_entries: Array,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current flows that are in progress and have not been started by a user.
|
|
||||||
* For example, can be discovered devices that require more config.
|
|
||||||
*/
|
|
||||||
_progress: Array,
|
|
||||||
|
|
||||||
_handlers: Array,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
this._loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
_createFlow(ev) {
|
|
||||||
this.hass.callApi('post', 'config/config_entries/flow', { handler: ev.model.item })
|
|
||||||
.then((flow) => {
|
|
||||||
this._userCreatedFlow = true;
|
|
||||||
this.setProperties({
|
|
||||||
_flowStep: flow,
|
|
||||||
_flowId: flow.flow_id,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_continueFlow(ev) {
|
|
||||||
this._userCreatedFlow = false;
|
|
||||||
this.setProperties({
|
|
||||||
_flowId: ev.model.item.flow_id,
|
|
||||||
_flowStep: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_removeEntry(ev) {
|
|
||||||
if (!confirm('Are you sure you want to delete this integration?')) return;
|
|
||||||
|
|
||||||
const entryId = ev.model.item.entry_id;
|
|
||||||
|
|
||||||
this.hass.callApi('delete', `config/config_entries/entry/${entryId}`)
|
|
||||||
.then((result) => {
|
|
||||||
this._entries = this._entries.filter(entry => entry.entry_id !== entryId);
|
|
||||||
if (result.require_restart) {
|
|
||||||
alert('Restart Home Assistant to finish removing this integration');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_flowClose(ev) {
|
|
||||||
// Was the flow completed?
|
|
||||||
if (ev.detail.flowFinished) {
|
|
||||||
this._loadData();
|
|
||||||
|
|
||||||
// Remove a flow if it was not finished and was started by the user
|
|
||||||
} else if (this._userCreatedFlow) {
|
|
||||||
this.hass.callApi('delete', `config/config_entries/flow/${this._flowId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._flowId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadData() {
|
|
||||||
this._loadEntries();
|
|
||||||
this._loadDiscovery();
|
|
||||||
this.hass.callApi('get', 'config/config_entries/flow_handlers')
|
|
||||||
.then((handlers) => { this._handlers = handlers; });
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadEntries() {
|
|
||||||
this.hass.callApi('get', 'config/config_entries/entry')
|
|
||||||
.then((entries) => { this._entries = entries; });
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadDiscovery() {
|
|
||||||
this.hass.callApi('get', 'config/config_entries/flow')
|
|
||||||
.then((progress) => { this._progress = progress; });
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeIntegrationTitle(localize, integration) {
|
|
||||||
return localize(`component.${integration}.config.title`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('ha-config-entries', HaConfigManager);
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: Object,
|
||||||
|
isWide: Boolean,
|
||||||
|
|
||||||
|
_flowId: {
|
||||||
|
type: String,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
* The step of the current selected flow, if available.
|
||||||
|
*/
|
||||||
|
_flowStep: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Existing entries.
|
||||||
|
*/
|
||||||
|
_entries: Array,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current flows that are in progress and have not been started by a user.
|
||||||
|
* For example, can be discovered devices that require more config.
|
||||||
|
*/
|
||||||
|
_progress: Array,
|
||||||
|
|
||||||
|
_handlers: Array,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ready() {
|
||||||
|
super.ready();
|
||||||
|
this._loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
_createFlow(ev) {
|
||||||
|
this.hass.callApi('post', 'config/config_entries/flow', { handler: ev.model.item })
|
||||||
|
.then((flow) => {
|
||||||
|
this._userCreatedFlow = true;
|
||||||
|
this.setProperties({
|
||||||
|
_flowStep: flow,
|
||||||
|
_flowId: flow.flow_id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_continueFlow(ev) {
|
||||||
|
this._userCreatedFlow = false;
|
||||||
|
this.setProperties({
|
||||||
|
_flowId: ev.model.item.flow_id,
|
||||||
|
_flowStep: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeEntry(ev) {
|
||||||
|
if (!confirm('Are you sure you want to delete this integration?')) return;
|
||||||
|
|
||||||
|
const entryId = ev.model.item.entry_id;
|
||||||
|
|
||||||
|
this.hass.callApi('delete', `config/config_entries/entry/${entryId}`)
|
||||||
|
.then((result) => {
|
||||||
|
this._entries = this._entries.filter(entry => entry.entry_id !== entryId);
|
||||||
|
if (result.require_restart) {
|
||||||
|
alert('Restart Home Assistant to finish removing this integration');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_flowClose(ev) {
|
||||||
|
// Was the flow completed?
|
||||||
|
if (ev.detail.flowFinished) {
|
||||||
|
this._loadData();
|
||||||
|
|
||||||
|
// Remove a flow if it was not finished and was started by the user
|
||||||
|
} else if (this._userCreatedFlow) {
|
||||||
|
this.hass.callApi('delete', `config/config_entries/flow/${this._flowId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._flowId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadData() {
|
||||||
|
this._loadEntries();
|
||||||
|
this._loadDiscovery();
|
||||||
|
this.hass.callApi('get', 'config/config_entries/flow_handlers')
|
||||||
|
.then((handlers) => { this._handlers = handlers; });
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadEntries() {
|
||||||
|
this.hass.callApi('get', 'config/config_entries/entry')
|
||||||
|
.then((entries) => { this._entries = entries; });
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadDiscovery() {
|
||||||
|
this.hass.callApi('get', 'config/config_entries/flow')
|
||||||
|
.then((progress) => { this._progress = progress; });
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeIntegrationTitle(localize, integration) {
|
||||||
|
return localize(`component.${integration}.config.title`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define('ha-config-entries', HaConfigManager);
|
||||||
|
@ -11,78 +11,76 @@ import LocalizeMixin from '../../../mixins/localize-mixin.js';
|
|||||||
|
|
||||||
import isComponentLoaded from '../../../common/config/is_component_loaded.js';
|
import isComponentLoaded from '../../../common/config/is_component_loaded.js';
|
||||||
|
|
||||||
{
|
const CORE_PAGES = [
|
||||||
const CORE_PAGES = [
|
'core',
|
||||||
'core',
|
'customize',
|
||||||
'customize',
|
];
|
||||||
];
|
/*
|
||||||
/*
|
* @appliesMixin LocalizeMixin
|
||||||
* @appliesMixin LocalizeMixin
|
* @appliesMixin NavigateMixin
|
||||||
* @appliesMixin NavigateMixin
|
*/
|
||||||
*/
|
class HaConfigNavigation extends
|
||||||
class HaConfigNavigation extends
|
LocalizeMixin(NavigateMixin(PolymerElement)) {
|
||||||
LocalizeMixin(NavigateMixin(PolymerElement)) {
|
static get template() {
|
||||||
static get template() {
|
return html`
|
||||||
return html`
|
<style include="iron-flex">
|
||||||
<style include="iron-flex">
|
paper-card {
|
||||||
paper-card {
|
display: block;
|
||||||
display: block;
|
}
|
||||||
}
|
paper-item {
|
||||||
paper-item {
|
cursor: pointer;
|
||||||
cursor: pointer;
|
}
|
||||||
}
|
</style>
|
||||||
</style>
|
<paper-card>
|
||||||
<paper-card>
|
<template is="dom-repeat" items="[[pages]]">
|
||||||
<template is="dom-repeat" items="[[pages]]">
|
<template is="dom-if" if="[[_computeLoaded(hass, item)]]">
|
||||||
<template is="dom-if" if="[[_computeLoaded(hass, item)]]">
|
<paper-item on-click="_navigate">
|
||||||
<paper-item on-click="_navigate">
|
<paper-item-body two-line="">
|
||||||
<paper-item-body two-line="">
|
[[_computeCaption(item, localize)]]
|
||||||
[[_computeCaption(item, localize)]]
|
<div secondary="">[[_computeDescription(item, localize)]]</div>
|
||||||
<div secondary="">[[_computeDescription(item, localize)]]</div>
|
</paper-item-body>
|
||||||
</paper-item-body>
|
<iron-icon icon="hass:chevron-right"></iron-icon>
|
||||||
<iron-icon icon="hass:chevron-right"></iron-icon>
|
</paper-item>
|
||||||
</paper-item>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</paper-card>
|
</template>
|
||||||
|
</paper-card>
|
||||||
`;
|
`;
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
pages: {
|
|
||||||
type: Array,
|
|
||||||
value: [
|
|
||||||
'core',
|
|
||||||
'customize',
|
|
||||||
'automation',
|
|
||||||
'script',
|
|
||||||
'zwave',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeLoaded(hass, page) {
|
|
||||||
return CORE_PAGES.includes(page) || isComponentLoaded(hass, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeCaption(page, localize) {
|
|
||||||
return localize(`ui.panel.config.${page}.caption`);
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeDescription(page, localize) {
|
|
||||||
return localize(`ui.panel.config.${page}.description`);
|
|
||||||
}
|
|
||||||
|
|
||||||
_navigate(ev) {
|
|
||||||
this.navigate('/config/' + ev.model.item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('ha-config-navigation', HaConfigNavigation);
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
|
||||||
|
pages: {
|
||||||
|
type: Array,
|
||||||
|
value: [
|
||||||
|
'core',
|
||||||
|
'customize',
|
||||||
|
'automation',
|
||||||
|
'script',
|
||||||
|
'zwave',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeLoaded(hass, page) {
|
||||||
|
return CORE_PAGES.includes(page) || isComponentLoaded(hass, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeCaption(page, localize) {
|
||||||
|
return localize(`ui.panel.config.${page}.caption`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeDescription(page, localize) {
|
||||||
|
return localize(`ui.panel.config.${page}.description`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_navigate(ev) {
|
||||||
|
this.navigate('/config/' + ev.model.item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define('ha-config-navigation', HaConfigNavigation);
|
||||||
|
@ -12,293 +12,291 @@ import '../../components/ha-service-picker.js';
|
|||||||
import '../../resources/ha-style.js';
|
import '../../resources/ha-style.js';
|
||||||
import '../../util/app-localstorage-document.js';
|
import '../../util/app-localstorage-document.js';
|
||||||
|
|
||||||
{
|
const ERROR_SENTINEL = {};
|
||||||
const ERROR_SENTINEL = {};
|
class HaPanelDevService extends PolymerElement {
|
||||||
class HaPanelDevService extends PolymerElement {
|
static get template() {
|
||||||
static get template() {
|
return html`
|
||||||
return html`
|
<style include='ha-style'>
|
||||||
<style include='ha-style'>
|
:host {
|
||||||
:host {
|
-ms-user-select: initial;
|
||||||
-ms-user-select: initial;
|
-webkit-user-select: initial;
|
||||||
-webkit-user-select: initial;
|
-moz-user-select: initial;
|
||||||
-moz-user-select: initial;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ha-form {
|
.ha-form {
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@apply --paper-font-title;
|
@apply --paper-font-title;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attributes th {
|
.attributes th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attributes tr {
|
.attributes tr {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attributes tr:nth-child(odd) {
|
.attributes tr:nth-child(odd) {
|
||||||
background-color: var(--table-row-background-color,#eee)
|
background-color: var(--table-row-background-color,#eee)
|
||||||
}
|
}
|
||||||
|
|
||||||
.attributes tr:nth-child(even) {
|
.attributes tr:nth-child(even) {
|
||||||
background-color: var(--table-row-alternative-background-color,#eee)
|
background-color: var(--table-row-alternative-background-color,#eee)
|
||||||
}
|
}
|
||||||
|
|
||||||
.attributes td:nth-child(3) {
|
.attributes td:nth-child(3) {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: var(--google-red-500);
|
color: var(--google-red-500);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<app-header-layout has-scrolling-region>
|
<app-header-layout has-scrolling-region>
|
||||||
<app-header slot="header" fixed>
|
<app-header slot="header" fixed>
|
||||||
<app-toolbar>
|
<app-toolbar>
|
||||||
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
|
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
|
||||||
<div main-title>Services</div>
|
<div main-title>Services</div>
|
||||||
</app-toolbar>
|
</app-toolbar>
|
||||||
</app-header>
|
</app-header>
|
||||||
|
|
||||||
<app-localstorage-document
|
<app-localstorage-document
|
||||||
key='panel-dev-service-state-domain-service'
|
key='panel-dev-service-state-domain-service'
|
||||||
data='{{domainService}}'>
|
data='{{domainService}}'>
|
||||||
</app-localstorage-document>
|
</app-localstorage-document>
|
||||||
<app-localstorage-document
|
<app-localstorage-document
|
||||||
key='[[_computeServicedataKey(domainService)]]'
|
key='[[_computeServicedataKey(domainService)]]'
|
||||||
data='{{serviceData}}'>
|
data='{{serviceData}}'>
|
||||||
</app-localstorage-document>
|
</app-localstorage-document>
|
||||||
|
|
||||||
<div class='content'>
|
<div class='content'>
|
||||||
<p>
|
<p>
|
||||||
The service dev tool allows you to call any available service in Home Assistant.
|
The service dev tool allows you to call any available service in Home Assistant.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class='ha-form'>
|
<div class='ha-form'>
|
||||||
<ha-service-picker
|
<ha-service-picker
|
||||||
|
hass='[[hass]]'
|
||||||
|
value='{{domainService}}'
|
||||||
|
></ha-service-picker>
|
||||||
|
<template is='dom-if' if='[[_computeHasEntity(_attributes)]]'>
|
||||||
|
<ha-entity-picker
|
||||||
hass='[[hass]]'
|
hass='[[hass]]'
|
||||||
value='{{domainService}}'
|
value='[[_computeEntityValue(parsedJSON)]]'
|
||||||
></ha-service-picker>
|
on-change='_entityPicked'
|
||||||
<template is='dom-if' if='[[_computeHasEntity(_attributes)]]'>
|
|
||||||
<ha-entity-picker
|
|
||||||
hass='[[hass]]'
|
|
||||||
value='[[_computeEntityValue(parsedJSON)]]'
|
|
||||||
on-change='_entityPicked'
|
|
||||||
disabled='[[!validJSON]]'
|
|
||||||
domain-filter='[[_computeEntityDomainFilter(_domain)]]'
|
|
||||||
allow-custom-entity
|
|
||||||
></ha-entity-picker>
|
|
||||||
</template>
|
|
||||||
<paper-textarea
|
|
||||||
always-float-label
|
|
||||||
label='Service Data (JSON, optional)'
|
|
||||||
value='{{serviceData}}'
|
|
||||||
></paper-textarea>
|
|
||||||
<paper-button
|
|
||||||
on-click='_callService'
|
|
||||||
raised
|
|
||||||
disabled='[[!validJSON]]'
|
disabled='[[!validJSON]]'
|
||||||
>Call Service</paper-button>
|
domain-filter='[[_computeEntityDomainFilter(_domain)]]'
|
||||||
<template is='dom-if' if='[[!validJSON]]'>
|
allow-custom-entity
|
||||||
<span class='error'>Invalid JSON</span>
|
></ha-entity-picker>
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template is='dom-if' if='[[!domainService]]'>
|
|
||||||
<h1>Select a service to see the description</h1>
|
|
||||||
</template>
|
</template>
|
||||||
|
<paper-textarea
|
||||||
<template is='dom-if' if='[[domainService]]'>
|
always-float-label
|
||||||
<template is='dom-if' if='[[!_description]]'>
|
label='Service Data (JSON, optional)'
|
||||||
<h1>No description is available</h1>
|
value='{{serviceData}}'
|
||||||
</template>
|
></paper-textarea>
|
||||||
<template is='dom-if' if='[[_description]]'>
|
<paper-button
|
||||||
<h3>[[_description]]</h3>
|
on-click='_callService'
|
||||||
|
raised
|
||||||
<table class='attributes'>
|
disabled='[[!validJSON]]'
|
||||||
<tr>
|
>Call Service</paper-button>
|
||||||
<th>Parameter</th>
|
<template is='dom-if' if='[[!validJSON]]'>
|
||||||
<th>Description</th>
|
<span class='error'>Invalid JSON</span>
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
<template is='dom-if' if='[[!_attributes.length]]'>
|
|
||||||
<tr><td colspan='3'>This service takes no parameters.</td></tr>
|
|
||||||
</template>
|
|
||||||
<template is='dom-repeat' items='[[_attributes]]' as='attribute'>
|
|
||||||
<tr>
|
|
||||||
<td><pre>[[attribute.key]]</pre></td>
|
|
||||||
<td>[[attribute.description]]</td>
|
|
||||||
<td>[[attribute.example]]</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</table>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</app-header-layout>
|
<template is='dom-if' if='[[!domainService]]'>
|
||||||
`;
|
<h1>Select a service to see the description</h1>
|
||||||
}
|
</template>
|
||||||
|
|
||||||
static get properties() {
|
<template is='dom-if' if='[[domainService]]'>
|
||||||
return {
|
<template is='dom-if' if='[[!_description]]'>
|
||||||
hass: {
|
<h1>No description is available</h1>
|
||||||
type: Object,
|
</template>
|
||||||
},
|
<template is='dom-if' if='[[_description]]'>
|
||||||
|
<h3>[[_description]]</h3>
|
||||||
|
|
||||||
narrow: {
|
<table class='attributes'>
|
||||||
type: Boolean,
|
<tr>
|
||||||
value: false,
|
<th>Parameter</th>
|
||||||
},
|
<th>Description</th>
|
||||||
|
<th>Example</th>
|
||||||
|
</tr>
|
||||||
|
<template is='dom-if' if='[[!_attributes.length]]'>
|
||||||
|
<tr><td colspan='3'>This service takes no parameters.</td></tr>
|
||||||
|
</template>
|
||||||
|
<template is='dom-repeat' items='[[_attributes]]' as='attribute'>
|
||||||
|
<tr>
|
||||||
|
<td><pre>[[attribute.key]]</pre></td>
|
||||||
|
<td>[[attribute.description]]</td>
|
||||||
|
<td>[[attribute.example]]</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
showMenu: {
|
</app-header-layout>
|
||||||
type: Boolean,
|
`;
|
||||||
value: false,
|
}
|
||||||
},
|
|
||||||
|
|
||||||
domainService: {
|
static get properties() {
|
||||||
type: String,
|
return {
|
||||||
observer: '_domainServiceChanged',
|
hass: {
|
||||||
},
|
type: Object,
|
||||||
|
},
|
||||||
|
|
||||||
_domain: {
|
narrow: {
|
||||||
type: String,
|
type: Boolean,
|
||||||
computed: '_computeDomain(domainService)',
|
value: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
_service: {
|
showMenu: {
|
||||||
type: String,
|
type: Boolean,
|
||||||
computed: '_computeService(domainService)',
|
value: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
serviceData: {
|
domainService: {
|
||||||
type: String,
|
type: String,
|
||||||
value: '',
|
observer: '_domainServiceChanged',
|
||||||
},
|
},
|
||||||
|
|
||||||
parsedJSON: {
|
_domain: {
|
||||||
type: Object,
|
type: String,
|
||||||
computed: '_computeParsedServiceData(serviceData)'
|
computed: '_computeDomain(domainService)',
|
||||||
},
|
},
|
||||||
|
|
||||||
validJSON: {
|
_service: {
|
||||||
type: Boolean,
|
type: String,
|
||||||
computed: '_computeValidJSON(parsedJSON)',
|
computed: '_computeService(domainService)',
|
||||||
},
|
},
|
||||||
|
|
||||||
_attributes: {
|
serviceData: {
|
||||||
type: Array,
|
type: String,
|
||||||
computed: '_computeAttributesArray(hass, _domain, _service)',
|
value: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
_description: {
|
parsedJSON: {
|
||||||
type: String,
|
type: Object,
|
||||||
computed: '_computeDescription(hass, _domain, _service)',
|
computed: '_computeParsedServiceData(serviceData)'
|
||||||
},
|
},
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_domainServiceChanged() {
|
validJSON: {
|
||||||
this.serviceData = '';
|
type: Boolean,
|
||||||
}
|
computed: '_computeValidJSON(parsedJSON)',
|
||||||
|
},
|
||||||
|
|
||||||
_computeAttributesArray(hass, domain, service) {
|
_attributes: {
|
||||||
const serviceDomains = hass.config.services;
|
type: Array,
|
||||||
if (!(domain in serviceDomains)) return [];
|
computed: '_computeAttributesArray(hass, _domain, _service)',
|
||||||
if (!(service in serviceDomains[domain])) return [];
|
},
|
||||||
|
|
||||||
const fields = serviceDomains[domain][service].fields;
|
_description: {
|
||||||
return Object.keys(fields).map(function (field) {
|
type: String,
|
||||||
return Object.assign({ key: field }, fields[field]);
|
computed: '_computeDescription(hass, _domain, _service)',
|
||||||
});
|
},
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
|
||||||
_computeDescription(hass, domain, service) {
|
_domainServiceChanged() {
|
||||||
const serviceDomains = hass.config.services;
|
this.serviceData = '';
|
||||||
if (!(domain in serviceDomains)) return undefined;
|
}
|
||||||
if (!(service in serviceDomains[domain])) return undefined;
|
|
||||||
return serviceDomains[domain][service].description;
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeServicedataKey(domainService) {
|
_computeAttributesArray(hass, domain, service) {
|
||||||
return `panel-dev-service-state-servicedata.${domainService}`;
|
const serviceDomains = hass.config.services;
|
||||||
}
|
if (!(domain in serviceDomains)) return [];
|
||||||
|
if (!(service in serviceDomains[domain])) return [];
|
||||||
|
|
||||||
_computeDomain(domainService) {
|
const fields = serviceDomains[domain][service].fields;
|
||||||
return domainService.split('.', 1)[0];
|
return Object.keys(fields).map(function (field) {
|
||||||
}
|
return Object.assign({ key: field }, fields[field]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_computeService(domainService) {
|
_computeDescription(hass, domain, service) {
|
||||||
return domainService.split('.', 2)[1] || null;
|
const serviceDomains = hass.config.services;
|
||||||
}
|
if (!(domain in serviceDomains)) return undefined;
|
||||||
|
if (!(service in serviceDomains[domain])) return undefined;
|
||||||
|
return serviceDomains[domain][service].description;
|
||||||
|
}
|
||||||
|
|
||||||
_computeParsedServiceData(serviceData) {
|
_computeServicedataKey(domainService) {
|
||||||
try {
|
return `panel-dev-service-state-servicedata.${domainService}`;
|
||||||
return serviceData ? JSON.parse(serviceData) : {};
|
}
|
||||||
} catch (err) {
|
|
||||||
return ERROR_SENTINEL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeValidJSON(parsedJSON) {
|
_computeDomain(domainService) {
|
||||||
return parsedJSON !== ERROR_SENTINEL;
|
return domainService.split('.', 1)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
_computeHasEntity(attributes) {
|
_computeService(domainService) {
|
||||||
return attributes.some(attr => attr.key === 'entity_id');
|
return domainService.split('.', 2)[1] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_computeEntityValue(parsedJSON) {
|
_computeParsedServiceData(serviceData) {
|
||||||
return parsedJSON === ERROR_SENTINEL ? '' : parsedJSON.entity_id;
|
try {
|
||||||
}
|
return serviceData ? JSON.parse(serviceData) : {};
|
||||||
|
} catch (err) {
|
||||||
_computeEntityDomainFilter(domain) {
|
return ERROR_SENTINEL;
|
||||||
return domain === 'homeassistant' ? null : domain;
|
|
||||||
}
|
|
||||||
|
|
||||||
_callService() {
|
|
||||||
if (this.parsedJSON === ERROR_SENTINEL) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
alert(`Error parsing JSON: ${this.serviceData}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hass.callService(this._domain, this._service, this.parsedJSON);
|
|
||||||
}
|
|
||||||
|
|
||||||
_entityPicked(ev) {
|
|
||||||
this.serviceData = JSON.stringify(Object.assign({}, this.parsedJSON, {
|
|
||||||
entity_id: ev.target.value
|
|
||||||
}), null, 2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('ha-panel-dev-service', HaPanelDevService);
|
_computeValidJSON(parsedJSON) {
|
||||||
|
return parsedJSON !== ERROR_SENTINEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeHasEntity(attributes) {
|
||||||
|
return attributes.some(attr => attr.key === 'entity_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeEntityValue(parsedJSON) {
|
||||||
|
return parsedJSON === ERROR_SENTINEL ? '' : parsedJSON.entity_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeEntityDomainFilter(domain) {
|
||||||
|
return domain === 'homeassistant' ? null : domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
_callService() {
|
||||||
|
if (this.parsedJSON === ERROR_SENTINEL) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
alert(`Error parsing JSON: ${this.serviceData}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hass.callService(this._domain, this._service, this.parsedJSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
_entityPicked(ev) {
|
||||||
|
this.serviceData = JSON.stringify(Object.assign({}, this.parsedJSON, {
|
||||||
|
entity_id: ev.target.value
|
||||||
|
}), null, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define('ha-panel-dev-service', HaPanelDevService);
|
||||||
|
@ -1,76 +1,74 @@
|
|||||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||||
|
|
||||||
{
|
var DATE_CACHE = {};
|
||||||
var DATE_CACHE = {};
|
|
||||||
|
|
||||||
class HaLogbookData extends PolymerElement {
|
class HaLogbookData extends PolymerElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
hass: {
|
hass: {
|
||||||
type: Object,
|
type: Object,
|
||||||
observer: 'hassChanged',
|
observer: 'hassChanged',
|
||||||
},
|
},
|
||||||
|
|
||||||
filterDate: {
|
filterDate: {
|
||||||
type: String,
|
type: String,
|
||||||
observer: 'filterDateChanged',
|
observer: 'filterDateChanged',
|
||||||
},
|
},
|
||||||
|
|
||||||
isLoading: {
|
isLoading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
value: true,
|
value: true,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
notify: true,
|
notify: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
entries: {
|
entries: {
|
||||||
type: Object,
|
type: Object,
|
||||||
value: null,
|
value: null,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
notify: true,
|
notify: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
hassChanged(newHass, oldHass) {
|
hassChanged(newHass, oldHass) {
|
||||||
if (!oldHass && this.filterDate) {
|
if (!oldHass && this.filterDate) {
|
||||||
this.filterDateChanged(this.filterDate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filterDateChanged(filterDate) {
|
|
||||||
if (!this.hass) return;
|
|
||||||
|
|
||||||
this._setIsLoading(true);
|
|
||||||
|
|
||||||
this.getDate(filterDate).then(function (logbookEntries) {
|
|
||||||
this._setEntries(logbookEntries);
|
|
||||||
this._setIsLoading(false);
|
|
||||||
}.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
getDate(date) {
|
|
||||||
if (!DATE_CACHE[date]) {
|
|
||||||
DATE_CACHE[date] = this.hass.callApi('GET', 'logbook/' + date).then(
|
|
||||||
function (logbookEntries) {
|
|
||||||
logbookEntries.reverse();
|
|
||||||
return logbookEntries;
|
|
||||||
},
|
|
||||||
function () {
|
|
||||||
DATE_CACHE[date] = false;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DATE_CACHE[date];
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshLogbook() {
|
|
||||||
DATE_CACHE[this.filterDate] = null;
|
|
||||||
this.filterDateChanged(this.filterDate);
|
this.filterDateChanged(this.filterDate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('ha-logbook-data', HaLogbookData);
|
filterDateChanged(filterDate) {
|
||||||
|
if (!this.hass) return;
|
||||||
|
|
||||||
|
this._setIsLoading(true);
|
||||||
|
|
||||||
|
this.getDate(filterDate).then(function (logbookEntries) {
|
||||||
|
this._setEntries(logbookEntries);
|
||||||
|
this._setIsLoading(false);
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
getDate(date) {
|
||||||
|
if (!DATE_CACHE[date]) {
|
||||||
|
DATE_CACHE[date] = this.hass.callApi('GET', 'logbook/' + date).then(
|
||||||
|
function (logbookEntries) {
|
||||||
|
logbookEntries.reverse();
|
||||||
|
return logbookEntries;
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
DATE_CACHE[date] = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DATE_CACHE[date];
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshLogbook() {
|
||||||
|
DATE_CACHE[this.filterDate] = null;
|
||||||
|
this.filterDateChanged(this.filterDate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define('ha-logbook-data', HaLogbookData);
|
||||||
|
@ -1,49 +1,47 @@
|
|||||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||||
|
|
||||||
{
|
const STORED_STATE = [
|
||||||
const STORED_STATE = [
|
'dockedSidebar',
|
||||||
'dockedSidebar',
|
'selectedTheme',
|
||||||
'selectedTheme',
|
'selectedLanguage',
|
||||||
'selectedLanguage',
|
];
|
||||||
];
|
|
||||||
|
|
||||||
class HaPrefStorage extends PolymerElement {
|
class HaPrefStorage extends PolymerElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
hass: Object,
|
hass: Object,
|
||||||
storage: {
|
storage: {
|
||||||
type: Object,
|
type: Object,
|
||||||
value: window.localStorage || {},
|
value: window.localStorage || {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
storeState() {
|
storeState() {
|
||||||
if (!this.hass) return;
|
if (!this.hass) return;
|
||||||
|
|
||||||
try {
|
|
||||||
for (var i = 0; i < STORED_STATE.length; i++) {
|
|
||||||
var key = STORED_STATE[i];
|
|
||||||
var value = this.hass[key];
|
|
||||||
this.storage[key] = JSON.stringify(value === undefined ? null : value);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Safari throws exception in private mode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getStoredState() {
|
|
||||||
var state = {};
|
|
||||||
|
|
||||||
|
try {
|
||||||
for (var i = 0; i < STORED_STATE.length; i++) {
|
for (var i = 0; i < STORED_STATE.length; i++) {
|
||||||
var key = STORED_STATE[i];
|
var key = STORED_STATE[i];
|
||||||
if (key in this.storage) {
|
var value = this.hass[key];
|
||||||
state[key] = JSON.parse(this.storage[key]);
|
this.storage[key] = JSON.stringify(value === undefined ? null : value);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
return state;
|
// Safari throws exception in private mode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customElements.define('ha-pref-storage', HaPrefStorage);
|
|
||||||
|
getStoredState() {
|
||||||
|
var state = {};
|
||||||
|
|
||||||
|
for (var i = 0; i < STORED_STATE.length; i++) {
|
||||||
|
var key = STORED_STATE[i];
|
||||||
|
if (key in this.storage) {
|
||||||
|
state[key] = JSON.parse(this.storage[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
customElements.define('ha-pref-storage', HaPrefStorage);
|
||||||
|
@ -2,74 +2,72 @@ import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
|||||||
|
|
||||||
import EventsMixin from '../mixins/events-mixin.js';
|
import EventsMixin from '../mixins/events-mixin.js';
|
||||||
|
|
||||||
{
|
/* eslint-disable no-console */
|
||||||
/* eslint-disable no-console */
|
const DEBUG = false;
|
||||||
const DEBUG = false;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @appliesMixin EventsMixin
|
* @appliesMixin EventsMixin
|
||||||
*/
|
*/
|
||||||
class HaUrlSync extends EventsMixin(PolymerElement) {
|
class HaUrlSync extends EventsMixin(PolymerElement) {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
hass: {
|
hass: {
|
||||||
type: Object,
|
type: Object,
|
||||||
observer: 'hassChanged',
|
observer: 'hassChanged',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
hassChanged(newHass, oldHass) {
|
hassChanged(newHass, oldHass) {
|
||||||
if (this.ignoreNextHassChange) {
|
if (this.ignoreNextHassChange) {
|
||||||
if (DEBUG) console.log('ignore hasschange');
|
if (DEBUG) console.log('ignore hasschange');
|
||||||
this.ignoreNextHassChange = false;
|
|
||||||
return;
|
|
||||||
} else if (!oldHass || oldHass.moreInfoEntityId === newHass.moreInfoEntityId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newHass.moreInfoEntityId) {
|
|
||||||
if (DEBUG) console.log('pushing state');
|
|
||||||
// We keep track of where we opened moreInfo from so that we don't
|
|
||||||
// pop the state when we close the modal if the modal has navigated
|
|
||||||
// us away.
|
|
||||||
this.moreInfoOpenedFromPath = window.location.pathname;
|
|
||||||
history.pushState(null, null, window.location.pathname);
|
|
||||||
} else if (window.location.pathname === this.moreInfoOpenedFromPath) {
|
|
||||||
if (DEBUG) console.log('history back');
|
|
||||||
this.ignoreNextPopstate = true;
|
|
||||||
history.back();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
popstateChangeListener(ev) {
|
|
||||||
if (this.ignoreNextPopstate) {
|
|
||||||
if (DEBUG) console.log('ignore popstate');
|
|
||||||
this.ignoreNextPopstate = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DEBUG) console.log('popstate', ev);
|
|
||||||
|
|
||||||
if (this.hass.moreInfoEntityId) {
|
|
||||||
if (DEBUG) console.log('deselect entity');
|
|
||||||
this.ignoreNextHassChange = true;
|
|
||||||
this.fire('hass-more-info', { entityId: null });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this.ignoreNextPopstate = false;
|
|
||||||
this.ignoreNextHassChange = false;
|
this.ignoreNextHassChange = false;
|
||||||
this.popstateChangeListener = this.popstateChangeListener.bind(this);
|
return;
|
||||||
window.addEventListener('popstate', this.popstateChangeListener);
|
} else if (!oldHass || oldHass.moreInfoEntityId === newHass.moreInfoEntityId) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
if (newHass.moreInfoEntityId) {
|
||||||
super.disconnectedCallback();
|
if (DEBUG) console.log('pushing state');
|
||||||
window.removeEventListener('popstate', this.popstateChangeListener);
|
// We keep track of where we opened moreInfo from so that we don't
|
||||||
|
// pop the state when we close the modal if the modal has navigated
|
||||||
|
// us away.
|
||||||
|
this.moreInfoOpenedFromPath = window.location.pathname;
|
||||||
|
history.pushState(null, null, window.location.pathname);
|
||||||
|
} else if (window.location.pathname === this.moreInfoOpenedFromPath) {
|
||||||
|
if (DEBUG) console.log('history back');
|
||||||
|
this.ignoreNextPopstate = true;
|
||||||
|
history.back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customElements.define('ha-url-sync', HaUrlSync);
|
|
||||||
|
popstateChangeListener(ev) {
|
||||||
|
if (this.ignoreNextPopstate) {
|
||||||
|
if (DEBUG) console.log('ignore popstate');
|
||||||
|
this.ignoreNextPopstate = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG) console.log('popstate', ev);
|
||||||
|
|
||||||
|
if (this.hass.moreInfoEntityId) {
|
||||||
|
if (DEBUG) console.log('deselect entity');
|
||||||
|
this.ignoreNextHassChange = true;
|
||||||
|
this.fire('hass-more-info', { entityId: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.ignoreNextPopstate = false;
|
||||||
|
this.ignoreNextHassChange = false;
|
||||||
|
this.popstateChangeListener = this.popstateChangeListener.bind(this);
|
||||||
|
window.addEventListener('popstate', this.popstateChangeListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
window.removeEventListener('popstate', this.popstateChangeListener);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
customElements.define('ha-url-sync', HaUrlSync);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user