mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-07 17:36: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 LocalizeMixin from '../mixins/localize-mixin.js';
|
||||
|
||||
{
|
||||
const UPDATE_INTERVAL = 10000; // ms
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="paper-material-styles">
|
||||
:host {
|
||||
@apply --paper-material-elevation-1;
|
||||
display: block;
|
||||
position: relative;
|
||||
font-size: 0px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
min-height: 48px;
|
||||
line-height: 0;
|
||||
}
|
||||
.camera-feed {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.caption {
|
||||
@apply --paper-font-common-nowrap;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
const UPDATE_INTERVAL = 10000; // ms
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="paper-material-styles">
|
||||
:host {
|
||||
@apply --paper-material-elevation-1;
|
||||
display: block;
|
||||
position: relative;
|
||||
font-size: 0px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
min-height: 48px;
|
||||
line-height: 0;
|
||||
}
|
||||
.camera-feed {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.caption {
|
||||
@apply --paper-font-common-nowrap;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 16px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 16px;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template is="dom-if" if="[[cameraFeedSrc]]">
|
||||
<img src="[[cameraFeedSrc]]" class="camera-feed" alt="[[_computeStateName(stateObj)]]">
|
||||
<template is="dom-if" if="[[cameraFeedSrc]]">
|
||||
<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>
|
||||
<div class="caption">
|
||||
[[_computeStateName(stateObj)]]
|
||||
<template is="dom-if" if="[[!imageLoaded]]">
|
||||
([[localize('ui.card.camera.not_available')]])
|
||||
</template>
|
||||
</div>
|
||||
</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 getGroupEntities from '../common/entity/get_group_entities.js';
|
||||
|
||||
{
|
||||
// mapping domain to size of the card.
|
||||
const DOMAINS_WITH_CARD = {
|
||||
camera: 4,
|
||||
history_graph: 4,
|
||||
media_player: 3,
|
||||
persistent_notification: 0,
|
||||
plant: 3,
|
||||
weather: 4,
|
||||
};
|
||||
// mapping domain to size of the card.
|
||||
const DOMAINS_WITH_CARD = {
|
||||
camera: 4,
|
||||
history_graph: 4,
|
||||
media_player: 3,
|
||||
persistent_notification: 0,
|
||||
plant: 3,
|
||||
weather: 4,
|
||||
};
|
||||
|
||||
// 4 types:
|
||||
// badges: 0 .. 10
|
||||
// 4 types:
|
||||
// badges: 0 .. 10
|
||||
// before groups < 0
|
||||
// groups: X
|
||||
// rest: 100
|
||||
|
||||
const PRIORITY = {
|
||||
// before groups < 0
|
||||
// groups: X
|
||||
// rest: 100
|
||||
configurator: -20,
|
||||
persistent_notification: -15,
|
||||
|
||||
const PRIORITY = {
|
||||
// before groups < 0
|
||||
configurator: -20,
|
||||
persistent_notification: -15,
|
||||
// badges have priority >= 0
|
||||
updater: 0,
|
||||
sun: 1,
|
||||
device_tracker: 2,
|
||||
alarm_control_panel: 3,
|
||||
timer: 4,
|
||||
sensor: 5,
|
||||
binary_sensor: 6,
|
||||
mailbox: 7,
|
||||
};
|
||||
|
||||
// badges have priority >= 0
|
||||
updater: 0,
|
||||
sun: 1,
|
||||
device_tracker: 2,
|
||||
alarm_control_panel: 3,
|
||||
timer: 4,
|
||||
sensor: 5,
|
||||
binary_sensor: 6,
|
||||
mailbox: 7,
|
||||
};
|
||||
const getPriority = domain =>
|
||||
((domain in PRIORITY) ? PRIORITY[domain] : 100);
|
||||
|
||||
const getPriority = domain =>
|
||||
((domain in PRIORITY) ? PRIORITY[domain] : 100);
|
||||
const sortPriority = (domainA, domainB) =>
|
||||
domainA.priority - domainB.priority;
|
||||
|
||||
const sortPriority = (domainA, domainB) =>
|
||||
domainA.priority - domainB.priority;
|
||||
const entitySortBy = (entityA, entityB) => {
|
||||
const nameA = (entityA.attributes.friendly_name ||
|
||||
entityA.entity_id).toLowerCase();
|
||||
const nameB = (entityB.attributes.friendly_name ||
|
||||
entityB.entity_id).toLowerCase();
|
||||
|
||||
const entitySortBy = (entityA, entityB) => {
|
||||
const nameA = (entityA.attributes.friendly_name ||
|
||||
entityA.entity_id).toLowerCase();
|
||||
const nameB = (entityB.attributes.friendly_name ||
|
||||
entityB.entity_id).toLowerCase();
|
||||
if (nameA < nameB) {
|
||||
return -1;
|
||||
}
|
||||
if (nameA > nameB) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
if (nameA < nameB) {
|
||||
return -1;
|
||||
const iterateDomainSorted = (collection, func) => {
|
||||
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) => {
|
||||
Object.keys(collection)
|
||||
.map(key => collection[key])
|
||||
.sort(sortPriority)
|
||||
.forEach((domain) => {
|
||||
domain.states.sort(entitySortBy);
|
||||
func(domain);
|
||||
});
|
||||
};
|
||||
.column {
|
||||
max-width: 500px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
class HaCards extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-flex-factors"></style>
|
||||
<style>
|
||||
ha-card-chooser {
|
||||
display: block;
|
||||
margin-left: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
:host {
|
||||
display: block;
|
||||
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;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
ha-card-chooser {
|
||||
display: block;
|
||||
margin-left: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
:host {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
ha-card-chooser {
|
||||
margin-left: 0;
|
||||
}
|
||||
@media (max-width: 599px) {
|
||||
.column {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@media (max-width: 599px) {
|
||||
.column {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div id="main">
|
||||
<template is="dom-if" if="[[cards.badges]]">
|
||||
<div class="badges">
|
||||
<template is="dom-if" if="[[cards.demo]]">
|
||||
<ha-demo-badge></ha-demo-badge>
|
||||
</template>
|
||||
|
||||
<div id="main">
|
||||
<template is="dom-if" if="[[cards.badges]]">
|
||||
<div class="badges">
|
||||
<template is="dom-if" if="[[cards.demo]]">
|
||||
<ha-demo-badge></ha-demo-badge>
|
||||
<ha-badges-card states="[[cards.badges]]" hass="[[hass]]"></ha-badges-card>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<ha-badges-card states="[[cards.badges]]" hass="[[hass]]"></ha-badges-card>
|
||||
</div>
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
|
||||
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() {
|
||||
return {
|
||||
hass: Object,
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
columns: {
|
||||
type: Number,
|
||||
value: 2,
|
||||
},
|
||||
entityCount[minIndex] += size;
|
||||
|
||||
states: Object,
|
||||
panelVisible: Boolean,
|
||||
|
||||
viewVisible: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
orderedGroupEntities: Array,
|
||||
|
||||
cards: Object,
|
||||
};
|
||||
return minIndex;
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return [
|
||||
'updateCards(columns, states, panelVisible, viewVisible, orderedGroupEntities)',
|
||||
];
|
||||
}
|
||||
function addEntitiesCard(name, entities, groupEntity) {
|
||||
if (entities.length === 0) return;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
const owncard = [];
|
||||
const other = [];
|
||||
|
||||
emptyCards() {
|
||||
return {
|
||||
demo: false,
|
||||
badges: [],
|
||||
columns: [],
|
||||
};
|
||||
}
|
||||
let size = 0;
|
||||
|
||||
computeCards(columns, states, orderedGroupEntities) {
|
||||
const hass = this.hass;
|
||||
entities.forEach((entity) => {
|
||||
const domain = computeStateDomain(entity);
|
||||
|
||||
const cards = this.emptyCards();
|
||||
|
||||
const entityCount = [];
|
||||
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;
|
||||
if (domain in DOMAINS_WITH_CARD) {
|
||||
owncard.push(entity);
|
||||
size += DOMAINS_WITH_CARD[domain];
|
||||
} 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) {
|
||||
Object.keys(badgesColl)
|
||||
.map(key => badgesColl[key])
|
||||
.forEach((domain) => {
|
||||
cards.badges.push.apply(cards.badges, domain.states);
|
||||
});
|
||||
// Add 1 to the size if we're rendering entities card
|
||||
size += other.length > 0;
|
||||
|
||||
cards.badges.sort((e1, e2) => orderedGroupEntities[e1.entity_id] -
|
||||
orderedGroupEntities[e2.entity_id]);
|
||||
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 {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
iterateDomainSorted(beforeGroupColl, (domain) => {
|
||||
addEntitiesCard(domain.domain, domain.states);
|
||||
cards.badges.sort((e1, e2) => orderedGroupEntities[e1.entity_id] -
|
||||
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 LocalizeMixin from '../mixins/localize-mixin.js';
|
||||
|
||||
{
|
||||
const RECENT_THRESHOLD = 60000; // 1 minute
|
||||
const RECENT_CACHE = {};
|
||||
const DOMAINS_USE_LAST_UPDATED = ['thermostat', 'climate'];
|
||||
const LINE_ATTRIBUTES_TO_KEEP = ['temperature', 'current_temperature', 'target_temp_low', 'target_temp_high'];
|
||||
const stateHistoryCache = {};
|
||||
const RECENT_THRESHOLD = 60000; // 1 minute
|
||||
const RECENT_CACHE = {};
|
||||
const DOMAINS_USE_LAST_UPDATED = ['thermostat', 'climate'];
|
||||
const LINE_ATTRIBUTES_TO_KEEP = ['temperature', 'current_temperature', 'target_temp_low', 'target_temp_high'];
|
||||
const stateHistoryCache = {};
|
||||
|
||||
function computeHistory(stateHistory, localize, language) {
|
||||
const lineChartDevices = {};
|
||||
const timelineDevices = [];
|
||||
if (!stateHistory) {
|
||||
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 };
|
||||
function computeHistory(stateHistory, localize, language) {
|
||||
const lineChartDevices = {};
|
||||
const timelineDevices = [];
|
||||
if (!stateHistory) {
|
||||
return { line: [], timeline: [] };
|
||||
}
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
|
||||
static get properties() {
|
||||
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 {
|
||||
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,
|
||||
},
|
||||
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]);
|
||||
})
|
||||
};
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
static get observers() {
|
||||
return [
|
||||
'filterChangedDebouncer(filterType, entityId, startTime, endTime, cacheConfig, localize, language)',
|
||||
];
|
||||
}
|
||||
return { line: unitStates, timeline: timelineDevices };
|
||||
}
|
||||
|
||||
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.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.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';
|
||||
|
||||
{
|
||||
const FEATURE_CLASS_NAMES = {
|
||||
128: 'has-set_tilt_position',
|
||||
};
|
||||
class MoreInfoCover extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex"></style>
|
||||
<style>
|
||||
.current_position, .tilt {
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
const FEATURE_CLASS_NAMES = {
|
||||
128: 'has-set_tilt_position',
|
||||
};
|
||||
class MoreInfoCover extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex"></style>
|
||||
<style>
|
||||
.current_position, .tilt {
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.has-current_position .current_position,
|
||||
.has-set_tilt_position .tilt,
|
||||
.has-current_tilt_position .tilt
|
||||
{
|
||||
max-height: 208px;
|
||||
}
|
||||
.has-current_position .current_position,
|
||||
.has-set_tilt_position .tilt,
|
||||
.has-current_tilt_position .tilt
|
||||
{
|
||||
max-height: 208px;
|
||||
}
|
||||
|
||||
[invisible] {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
<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>
|
||||
[invisible] {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
entityObj: {
|
||||
type: Object,
|
||||
computed: 'computeEntityObj(hass, stateObj)',
|
||||
},
|
||||
coverPositionSliderValue: Number,
|
||||
coverTiltPositionSliderValue: Number
|
||||
};
|
||||
}
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
entityObj: {
|
||||
type: Object,
|
||||
computed: 'computeEntityObj(hass, stateObj)',
|
||||
},
|
||||
coverPositionSliderValue: Number,
|
||||
coverTiltPositionSliderValue: Number
|
||||
};
|
||||
}
|
||||
|
||||
computeEntityObj(hass, stateObj) {
|
||||
return new CoverEntity(hass, stateObj);
|
||||
}
|
||||
computeEntityObj(hass, stateObj) {
|
||||
return new CoverEntity(hass, stateObj);
|
||||
}
|
||||
|
||||
stateObjChanged(newVal) {
|
||||
if (newVal) {
|
||||
this.setProperties({
|
||||
coverPositionSliderValue: newVal.attributes.current_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);
|
||||
stateObjChanged(newVal) {
|
||||
if (newVal) {
|
||||
this.setProperties({
|
||||
coverPositionSliderValue: newVal.attributes.current_position,
|
||||
coverTiltPositionSliderValue: newVal.attributes.current_tilt_position,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 LocalizeMixin from '../../../mixins/localize-mixin.js';
|
||||
|
||||
{
|
||||
const FEATURE_CLASS_NAMES = {
|
||||
1: 'has-brightness',
|
||||
2: 'has-color_temp',
|
||||
4: 'has-effect_list',
|
||||
16: 'has-color',
|
||||
128: 'has-white_value',
|
||||
};
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex"></style>
|
||||
<style>
|
||||
.effect_list {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
const FEATURE_CLASS_NAMES = {
|
||||
1: 'has-brightness',
|
||||
2: 'has-color_temp',
|
||||
4: 'has-effect_list',
|
||||
16: 'has-color',
|
||||
128: 'has-white_value',
|
||||
};
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex"></style>
|
||||
<style>
|
||||
.effect_list {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.effect_list, .brightness, .color_temp, .white_value {
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height .5s ease-in;
|
||||
}
|
||||
.effect_list, .brightness, .color_temp, .white_value {
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height .5s ease-in;
|
||||
}
|
||||
|
||||
.color_temp {
|
||||
--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". */
|
||||
--paper-slider-knob-start-border-color: var(--primary-color);
|
||||
}
|
||||
.color_temp {
|
||||
--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". */
|
||||
--paper-slider-knob-start-border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
ha-color-picker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
ha-color-picker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height .5s ease-in;
|
||||
}
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height .5s ease-in;
|
||||
}
|
||||
|
||||
.has-effect_list.is-on .effect_list,
|
||||
.has-brightness .brightness,
|
||||
.has-color_temp.is-on .color_temp,
|
||||
.has-white_value.is-on .white_value {
|
||||
max-height: 84px;
|
||||
}
|
||||
.has-effect_list.is-on .effect_list,
|
||||
.has-brightness .brightness,
|
||||
.has-color_temp.is-on .color_temp,
|
||||
.has-white_value.is-on .white_value {
|
||||
max-height: 84px;
|
||||
}
|
||||
|
||||
.has-color.is-on ha-color-picker {
|
||||
max-height: 500px;
|
||||
overflow: visible;
|
||||
--ha-color-picker-wheel-borderwidth: 5;
|
||||
--ha-color-picker-wheel-bordercolor: white;
|
||||
--ha-color-picker-wheel-shadow: none;
|
||||
--ha-color-picker-marker-borderwidth: 2;
|
||||
--ha-color-picker-marker-bordercolor: white;
|
||||
}
|
||||
.has-color.is-on ha-color-picker {
|
||||
max-height: 500px;
|
||||
overflow: visible;
|
||||
--ha-color-picker-wheel-borderwidth: 5;
|
||||
--ha-color-picker-wheel-bordercolor: white;
|
||||
--ha-color-picker-wheel-shadow: none;
|
||||
--ha-color-picker-marker-borderwidth: 2;
|
||||
--ha-color-picker-marker-bordercolor: white;
|
||||
}
|
||||
|
||||
.is-unavailable .control {
|
||||
max-height: 0px;
|
||||
}
|
||||
.is-unavailable .control {
|
||||
max-height: 0px;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class\$="[[computeClassNames(stateObj)]]">
|
||||
<div class\$="[[computeClassNames(stateObj)]]">
|
||||
|
||||
<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>
|
||||
</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 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>
|
||||
</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() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
|
||||
effectIndex: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
observer: 'effectChanged',
|
||||
},
|
||||
effectIndex: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
observer: 'effectChanged',
|
||||
},
|
||||
|
||||
brightnessSliderValue: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
brightnessSliderValue: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
|
||||
ctSliderValue: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
ctSliderValue: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
|
||||
wvSliderValue: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
wvSliderValue: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
|
||||
colorPickerColor: {
|
||||
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;
|
||||
}
|
||||
colorPickerColor: {
|
||||
type: Object,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.setProperties(props);
|
||||
stateObjChanged(newVal, oldVal) {
|
||||
const props = {
|
||||
brightnessSliderValue: 0
|
||||
};
|
||||
|
||||
if (oldVal) {
|
||||
setTimeout(() => {
|
||||
this.fire('iron-resize');
|
||||
}, 500);
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
if (newVal.attributes.effect_list) {
|
||||
props.effectIndex = newVal.attributes.effect_list.indexOf(newVal.attributes.effect);
|
||||
} else {
|
||||
this.hass.callService('light', 'turn_on', {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
brightness: bri,
|
||||
});
|
||||
props.effectIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
ctSliderChanged(ev) {
|
||||
var ct = parseInt(ev.target.value, 10);
|
||||
this.setProperties(props);
|
||||
|
||||
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);
|
||||
if (oldVal) {
|
||||
setTimeout(() => {
|
||||
this.fire('iron-resize');
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
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 LocalizeMixin from '../../../mixins/localize-mixin.js';
|
||||
|
||||
{
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-flex-alignment"></style>
|
||||
<style>
|
||||
.media-state {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-flex-alignment"></style>
|
||||
<style>
|
||||
.media-state {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
paper-icon-button[highlight] {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
paper-icon-button[highlight] {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.volume {
|
||||
margin-bottom: 8px;
|
||||
.volume {
|
||||
margin-bottom: 8px;
|
||||
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height .5s ease-in;
|
||||
}
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height .5s ease-in;
|
||||
}
|
||||
|
||||
.has-volume_level .volume {
|
||||
max-height: 40px;
|
||||
}
|
||||
.has-volume_level .volume {
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
iron-icon.source-input {
|
||||
padding: 7px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
iron-icon.source-input {
|
||||
padding: 7px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
paper-dropdown-menu.source-input {
|
||||
margin-left: 10px;
|
||||
}
|
||||
paper-dropdown-menu.source-input {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class\$="[[computeClassNames(stateObj)]]">
|
||||
<div class="layout horizontal">
|
||||
<div class="flex">
|
||||
<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 class\$="[[computeClassNames(stateObj)]]">
|
||||
<div class="layout horizontal">
|
||||
<div class="flex">
|
||||
<paper-icon-button icon="hass:power" highlight\$="[[playerObj.isOff]]" on-click="handleTogglePower" hidden\$="[[computeHidePowerButton(playerObj)]]"></paper-icon-button>
|
||||
</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>
|
||||
<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>
|
||||
<!-- 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() {
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
if (oldVal) {
|
||||
setTimeout(() => {
|
||||
this.fire('iron-resize');
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
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 EventsMixin from '../../mixins/events-mixin.js';
|
||||
|
||||
{
|
||||
const DOMAINS_NO_INFO = [
|
||||
'camera',
|
||||
'configurator',
|
||||
'history_graph',
|
||||
];
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style-dialog">
|
||||
app-toolbar {
|
||||
color: var(--more-info-header-color);
|
||||
background-color: var(--more-info-header-background);
|
||||
const DOMAINS_NO_INFO = [
|
||||
'camera',
|
||||
'configurator',
|
||||
'history_graph',
|
||||
];
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style-dialog">
|
||||
app-toolbar {
|
||||
color: var(--more-info-header-color);
|
||||
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] {
|
||||
@apply --ha-more-info-app-toolbar-title;
|
||||
}
|
||||
:host([domain=camera]) paper-dialog-scrollable {
|
||||
margin: 0 -24px -5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
: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>
|
||||
<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>
|
||||
<paper-dialog-scrollable dialog-element="[[dialogElement]]">
|
||||
<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>
|
||||
</app-toolbar>
|
||||
|
||||
<template is="dom-if" if="[[_computeShowStateInfo(stateObj)]]" restamp="">
|
||||
<state-card-content state-obj="[[stateObj]]" hass="[[hass]]" in-dialog=""></state-card-content>
|
||||
</template>
|
||||
<paper-dialog-scrollable dialog-element="[[dialogElement]]">
|
||||
<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() {
|
||||
return {
|
||||
hass: Object,
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: '_stateObjChanged',
|
||||
stateObj: {
|
||||
type: Object,
|
||||
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,
|
||||
canConfigure: Boolean,
|
||||
enlarge() {
|
||||
this.large = !this.large;
|
||||
}
|
||||
|
||||
domain: {
|
||||
type: String,
|
||||
reflectToAttribute: true,
|
||||
computed: '_computeDomain(stateObj)',
|
||||
},
|
||||
_computeShowStateInfo(stateObj) {
|
||||
return !stateObj || !DOMAINS_NO_INFO.includes(computeStateDomain(stateObj));
|
||||
}
|
||||
|
||||
_stateHistory: Object,
|
||||
_stateHistoryLoading: Boolean,
|
||||
_computeShowHistoryComponent(hass, stateObj) {
|
||||
return hass && stateObj &&
|
||||
isComponentLoaded(hass, 'history') &&
|
||||
!DOMAINS_MORE_INFO_NO_HISTORY.includes(computeStateDomain(stateObj));
|
||||
}
|
||||
|
||||
large: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
},
|
||||
_computeDomain(stateObj) {
|
||||
return stateObj ? computeStateDomain(stateObj) : '';
|
||||
}
|
||||
|
||||
_cacheConfig: {
|
||||
type: Object,
|
||||
value: {
|
||||
refresh: 60,
|
||||
cacheKey: null,
|
||||
hoursToShow: 24,
|
||||
},
|
||||
},
|
||||
};
|
||||
_computeStateName(stateObj) {
|
||||
return stateObj ? computeStateName(stateObj) : '';
|
||||
}
|
||||
|
||||
_stateObjChanged(newVal) {
|
||||
if (!newVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
enlarge() {
|
||||
this.large = !this.large;
|
||||
}
|
||||
|
||||
_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' });
|
||||
if (this._cacheConfig.cacheKey !== `more_info.${newVal.entity_id}`) {
|
||||
this._cacheConfig = Object.assign(
|
||||
{}, this._cacheConfig,
|
||||
{ cacheKey: `more_info.${newVal.entity_id}` }
|
||||
);
|
||||
}
|
||||
}
|
||||
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: "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)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
/* remove the grey tap highlights in iOS on the fullscreen touch targets */
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
}
|
||||
iron-pages, ha-sidebar {
|
||||
/* allow a light tap highlight on the actual interface elements */
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0.1);
|
||||
}
|
||||
iron-pages {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<ha-more-info-dialog hass="[[hass]]"></ha-more-info-dialog>
|
||||
<ha-url-sync hass="[[hass]]"></ha-url-sync>
|
||||
<app-route route="{{route}}" pattern="/states" tail="{{statesRouteTail}}"></app-route>
|
||||
<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>
|
||||
class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
/* remove the grey tap highlights in iOS on the fullscreen touch targets */
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
}
|
||||
iron-pages, ha-sidebar {
|
||||
/* allow a light tap highlight on the actual interface elements */
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0.1);
|
||||
}
|
||||
iron-pages {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<ha-more-info-dialog hass="[[hass]]"></ha-more-info-dialog>
|
||||
<ha-url-sync hass="[[hass]]"></ha-url-sync>
|
||||
<app-route route="{{route}}" pattern="/states" tail="{{statesRouteTail}}"></app-route>
|
||||
<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>
|
||||
|
||||
<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]]">
|
||||
<ha-sidebar narrow="[[narrow]]" hass="[[hass]]"></ha-sidebar>
|
||||
</app-drawer>
|
||||
<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]]">
|
||||
<ha-sidebar narrow="[[narrow]]" hass="[[hass]]"></ha-sidebar>
|
||||
</app-drawer>
|
||||
|
||||
<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>
|
||||
<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-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>
|
||||
</app-drawer-layout>
|
||||
</iron-pages>
|
||||
</app-drawer-layout>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
narrow: Boolean,
|
||||
route: {
|
||||
type: Object,
|
||||
observer: '_routeChanged',
|
||||
},
|
||||
statesRouteTail: Object,
|
||||
dockedSidebar: {
|
||||
type: Boolean,
|
||||
computed: 'computeDockedSidebar(hass)',
|
||||
},
|
||||
};
|
||||
}
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
narrow: Boolean,
|
||||
route: {
|
||||
type: Object,
|
||||
observer: '_routeChanged',
|
||||
},
|
||||
statesRouteTail: Object,
|
||||
dockedSidebar: {
|
||||
type: Boolean,
|
||||
computed: 'computeDockedSidebar(hass)',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('hass-open-menu', () => this.handleOpenMenu());
|
||||
this.addEventListener('hass-close-menu', () => this.handleCloseMenu());
|
||||
this.addEventListener('hass-start-voice', ev => this.handleStartVoice(ev));
|
||||
}
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('hass-open-menu', () => this.handleOpenMenu());
|
||||
this.addEventListener('hass-close-menu', () => this.handleCloseMenu());
|
||||
this.addEventListener('hass-start-voice', ev => this.handleStartVoice(ev));
|
||||
}
|
||||
|
||||
_routeChanged() {
|
||||
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() {
|
||||
_routeChanged() {
|
||||
if (this.narrow) {
|
||||
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 EventsMixin from '../mixins/events-mixin.js';
|
||||
|
||||
{
|
||||
const DEFAULT_VIEW_ENTITY_ID = 'group.default_view';
|
||||
const ALWAYS_SHOW_DOMAIN = ['persistent_notification', 'configurator'];
|
||||
const DEFAULT_VIEW_ENTITY_ID = 'group.default_view';
|
||||
const ALWAYS_SHOW_DOMAIN = ['persistent_notification', 'configurator'];
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin NavigateMixin
|
||||
*/
|
||||
class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-positioning ha-style">
|
||||
:host {
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin NavigateMixin
|
||||
*/
|
||||
class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-positioning ha-style">
|
||||
:host {
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
ha-app-layout {
|
||||
background-color: var(--secondary-background-color, #E5E5E5);
|
||||
}
|
||||
ha-app-layout {
|
||||
background-color: var(--secondary-background-color, #E5E5E5);
|
||||
}
|
||||
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
--paper-tabs-selection-bar-color: var(--text-primary-color, #FFF);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
<app-route route="{{route}}" pattern="/:view" data="{{routeData}}" active="{{routeMatch}}"></app-route>
|
||||
<ha-app-layout has-scrolling-region="" id="layout">
|
||||
<app-header effects="waterfall" condenses="" fixed="" slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button>
|
||||
<div main-title="">[[computeTitle(views, defaultView, locationName)]]</div>
|
||||
<ha-start-voice-button hass="[[hass]]"></ha-start-voice-button>
|
||||
</app-toolbar>
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
--paper-tabs-selection-bar-color: var(--text-primary-color, #FFF);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
<app-route route="{{route}}" pattern="/:view" data="{{routeData}}" active="{{routeMatch}}"></app-route>
|
||||
<ha-app-layout has-scrolling-region="" id="layout">
|
||||
<app-header effects="waterfall" condenses="" fixed="" slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button>
|
||||
<div main-title="">[[computeTitle(views, defaultView, locationName)]]</div>
|
||||
<ha-start-voice-button hass="[[hass]]"></ha-start-voice-button>
|
||||
</app-toolbar>
|
||||
|
||||
<div sticky="" hidden\$="[[areTabsHidden(views, showTabs)]]">
|
||||
<paper-tabs scrollable="" selected="[[currentView]]" attr-for-selected="data-entity" on-iron-activate="handleViewSelected">
|
||||
<paper-tab data-entity="" on-click="scrollToTop">
|
||||
<template is="dom-if" if="[[!defaultView]]">
|
||||
Home
|
||||
<div sticky="" hidden\$="[[areTabsHidden(views, showTabs)]]">
|
||||
<paper-tabs scrollable="" selected="[[currentView]]" attr-for-selected="data-entity" on-iron-activate="handleViewSelected">
|
||||
<paper-tab data-entity="" on-click="scrollToTop">
|
||||
<template is="dom-if" if="[[!defaultView]]">
|
||||
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 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 is="dom-if" if="[[!defaultView.attributes.icon]]">
|
||||
[[_computeStateName(defaultView)]]
|
||||
</template>
|
||||
<template is="dom-if" if="[[!defaultView.attributes.icon]]">
|
||||
[[_computeStateName(defaultView)]]
|
||||
</template>
|
||||
</template>
|
||||
</paper-tab>
|
||||
<template is="dom-repeat" items="[[views]]">
|
||||
<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>
|
||||
</paper-tab>
|
||||
<template is="dom-repeat" items="[[views]]">
|
||||
<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>
|
||||
</paper-tab>
|
||||
</template>
|
||||
</paper-tabs>
|
||||
</div>
|
||||
</app-header>
|
||||
</template>
|
||||
</paper-tabs>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
</iron-pages>
|
||||
</ha-app-layout>
|
||||
</iron-pages>
|
||||
</ha-app-layout>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
static get properties() {
|
||||
return {
|
||||
|
||||
hass: {
|
||||
type: Object,
|
||||
value: null,
|
||||
observer: 'hassChanged'
|
||||
},
|
||||
hass: {
|
||||
type: Object,
|
||||
value: null,
|
||||
observer: 'hassChanged'
|
||||
},
|
||||
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
observer: 'handleWindowChange',
|
||||
},
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
observer: 'handleWindowChange',
|
||||
},
|
||||
|
||||
panelVisible: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
panelVisible: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
route: Object,
|
||||
routeData: Object,
|
||||
routeMatch: Boolean,
|
||||
route: Object,
|
||||
routeData: Object,
|
||||
routeMatch: Boolean,
|
||||
|
||||
_columns: {
|
||||
type: Number,
|
||||
value: 1,
|
||||
},
|
||||
_columns: {
|
||||
type: Number,
|
||||
value: 1,
|
||||
},
|
||||
|
||||
locationName: {
|
||||
type: String,
|
||||
value: '',
|
||||
computed: '_computeLocationName(hass)',
|
||||
},
|
||||
locationName: {
|
||||
type: String,
|
||||
value: '',
|
||||
computed: '_computeLocationName(hass)',
|
||||
},
|
||||
|
||||
currentView: {
|
||||
type: String,
|
||||
computed: '_computeCurrentView(hass, routeMatch, routeData)',
|
||||
},
|
||||
currentView: {
|
||||
type: String,
|
||||
computed: '_computeCurrentView(hass, routeMatch, routeData)',
|
||||
},
|
||||
|
||||
views: {
|
||||
type: Array,
|
||||
},
|
||||
views: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
defaultView: {
|
||||
type: Object,
|
||||
},
|
||||
defaultView: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
viewStates: {
|
||||
type: Object,
|
||||
computed: 'computeViewStates(currentView, hass, defaultView)',
|
||||
},
|
||||
viewStates: {
|
||||
type: Object,
|
||||
computed: 'computeViewStates(currentView, hass, defaultView)',
|
||||
},
|
||||
|
||||
orderedGroupEntities: {
|
||||
type: Array,
|
||||
computed: 'computeOrderedGroupEntities(currentView, hass, defaultView)',
|
||||
},
|
||||
orderedGroupEntities: {
|
||||
type: Array,
|
||||
computed: 'computeOrderedGroupEntities(currentView, hass, defaultView)',
|
||||
},
|
||||
|
||||
showTabs: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
showTabs: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
this.handleWindowChange = this.handleWindowChange.bind(this);
|
||||
this.mqls = [300, 600, 900, 1200].map(width => matchMedia(`(min-width: ${width}px)`));
|
||||
super.ready();
|
||||
}
|
||||
ready() {
|
||||
this.handleWindowChange = this.handleWindowChange.bind(this);
|
||||
this.mqls = [300, 600, 900, 1200].map(width => matchMedia(`(min-width: ${width}px)`));
|
||||
super.ready();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.mqls.forEach(mql => mql.addListener(this.handleWindowChange));
|
||||
}
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.mqls.forEach(mql => mql.addListener(this.handleWindowChange));
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.mqls.forEach(mql => mql.removeListener(this.handleWindowChange));
|
||||
}
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.mqls.forEach(mql => mql.removeListener(this.handleWindowChange));
|
||||
}
|
||||
|
||||
handleWindowChange() {
|
||||
const matchColumns = this.mqls.reduce((cols, mql) => cols + mql.matches, 0);
|
||||
// Do -1 column if the menu is docked and open
|
||||
this._columns = Math.max(1, matchColumns - (!this.narrow && this.showMenu));
|
||||
}
|
||||
handleWindowChange() {
|
||||
const matchColumns = this.mqls.reduce((cols, mql) => cols + mql.matches, 0);
|
||||
// Do -1 column if the menu is docked and open
|
||||
this._columns = Math.max(1, matchColumns - (!this.narrow && this.showMenu));
|
||||
}
|
||||
|
||||
areTabsHidden(views, showTabs) {
|
||||
return !views || !views.length || !showTabs;
|
||||
}
|
||||
areTabsHidden(views, showTabs) {
|
||||
return !views || !views.length || !showTabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to a specific y coordinate.
|
||||
*
|
||||
* Copied from paper-scroll-header-panel.
|
||||
*
|
||||
* @method scroll
|
||||
* @param {number} top The coordinate to scroll to, along the y-axis.
|
||||
* @param {boolean} smooth true if the scroll position should be smoothly adjusted.
|
||||
*/
|
||||
scrollToTop() {
|
||||
// the scroll event will trigger _updateScrollState directly,
|
||||
// However, _updateScrollState relies on the previous `scrollTop` to update the states.
|
||||
// Calling _updateScrollState will ensure that the states are synced correctly.
|
||||
var top = 0;
|
||||
var scroller = this.$.layout.header.scrollTarget;
|
||||
var easingFn = function easeOutQuad(t, b, c, d) {
|
||||
/* eslint-disable no-param-reassign, space-infix-ops, no-mixed-operators */
|
||||
t /= d;
|
||||
return -c * t*(t-2) + b;
|
||||
/* eslint-enable no-param-reassign, space-infix-ops, no-mixed-operators */
|
||||
};
|
||||
var animationId = Math.random();
|
||||
var duration = 200;
|
||||
var startTime = Date.now();
|
||||
var currentScrollTop = scroller.scrollTop;
|
||||
var deltaScrollTop = top - currentScrollTop;
|
||||
this._currentAnimationId = animationId;
|
||||
(function updateFrame() {
|
||||
var now = Date.now();
|
||||
var elapsedTime = now - startTime;
|
||||
if (elapsedTime > duration) {
|
||||
scroller.scrollTop = top;
|
||||
} else if (this._currentAnimationId === animationId) {
|
||||
scroller.scrollTop = easingFn(elapsedTime, currentScrollTop, deltaScrollTop, duration);
|
||||
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);
|
||||
/**
|
||||
* Scroll to a specific y coordinate.
|
||||
*
|
||||
* Copied from paper-scroll-header-panel.
|
||||
*
|
||||
* @method scroll
|
||||
* @param {number} top The coordinate to scroll to, along the y-axis.
|
||||
* @param {boolean} smooth true if the scroll position should be smoothly adjusted.
|
||||
*/
|
||||
scrollToTop() {
|
||||
// the scroll event will trigger _updateScrollState directly,
|
||||
// However, _updateScrollState relies on the previous `scrollTop` to update the states.
|
||||
// Calling _updateScrollState will ensure that the states are synced correctly.
|
||||
var top = 0;
|
||||
var scroller = this.$.layout.header.scrollTarget;
|
||||
var easingFn = function easeOutQuad(t, b, c, d) {
|
||||
/* eslint-disable no-param-reassign, space-infix-ops, no-mixed-operators */
|
||||
t /= d;
|
||||
return -c * t*(t-2) + b;
|
||||
/* eslint-enable no-param-reassign, space-infix-ops, no-mixed-operators */
|
||||
};
|
||||
var animationId = Math.random();
|
||||
var duration = 200;
|
||||
var startTime = Date.now();
|
||||
var currentScrollTop = scroller.scrollTop;
|
||||
var deltaScrollTop = top - currentScrollTop;
|
||||
this._currentAnimationId = animationId;
|
||||
(function updateFrame() {
|
||||
var now = Date.now();
|
||||
var elapsedTime = now - startTime;
|
||||
if (elapsedTime > duration) {
|
||||
scroller.scrollTop = top;
|
||||
} else if (this._currentAnimationId === animationId) {
|
||||
scroller.scrollTop = easingFn(elapsedTime, currentScrollTop, deltaScrollTop, duration);
|
||||
requestAnimationFrame(updateFrame.bind(this));
|
||||
}
|
||||
}
|
||||
}).call(this);
|
||||
}
|
||||
|
||||
_computeCurrentView(hass, routeMatch, routeData) {
|
||||
if (!routeMatch) return '';
|
||||
if (!hass.states[routeData.view] || !hass.states[routeData.view].attributes.view) {
|
||||
return '';
|
||||
handleViewSelected(ev) {
|
||||
const view = ev.detail.item.getAttribute('data-entity') || null;
|
||||
|
||||
if (view !== this.currentView) {
|
||||
let path = '/states';
|
||||
if (view) {
|
||||
path += '/' + view;
|
||||
}
|
||||
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;
|
||||
this.navigate(path);
|
||||
}
|
||||
}
|
||||
|
||||
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 NavigateMixin from '../../../mixins/navigate-mixin.js';
|
||||
|
||||
{
|
||||
const LOGGED_IN_URLS = [
|
||||
'/cloud/account',
|
||||
];
|
||||
const NOT_LOGGED_IN_URLS = [
|
||||
'/cloud/login',
|
||||
'/cloud/register',
|
||||
'/cloud/forgot-password',
|
||||
];
|
||||
const LOGGED_IN_URLS = [
|
||||
'/cloud/account',
|
||||
];
|
||||
const NOT_LOGGED_IN_URLS = [
|
||||
'/cloud/login',
|
||||
'/cloud/register',
|
||||
'/cloud/forgot-password',
|
||||
];
|
||||
|
||||
/*
|
||||
* @appliesMixin NavigateMixin
|
||||
*/
|
||||
class HaConfigCloud extends NavigateMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<app-route route="[[route]]" pattern="/cloud/:page" data="{{_routeData}}" tail="{{_routeTail}}"></app-route>
|
||||
/*
|
||||
* @appliesMixin NavigateMixin
|
||||
*/
|
||||
class HaConfigCloud extends NavigateMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<app-route route="[[route]]" pattern="/cloud/:page" data="{{_routeData}}" tail="{{_routeTail}}"></app-route>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
<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>
|
||||
</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 LocalizeMixin from '../../../mixins/localize-mixin.js';
|
||||
|
||||
{
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaConfigManager extends
|
||||
LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
top: 3px;
|
||||
margin-right: -.57em;
|
||||
}
|
||||
paper-card:last-child {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.config-entry-row {
|
||||
display: flex;
|
||||
padding: 0 16px;
|
||||
}
|
||||
</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>
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaConfigManager extends
|
||||
LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
top: 3px;
|
||||
margin-right: -.57em;
|
||||
}
|
||||
paper-card:last-child {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.config-entry-row {
|
||||
display: flex;
|
||||
padding: 0 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<hass-subpage header="Integrations">
|
||||
<template is="dom-if" if="[[_progress.length]]">
|
||||
<ha-config-section is-wide="[[isWide]]">
|
||||
<span slot="header">Configured</span>
|
||||
<span slot="header">In Progress</span>
|
||||
<paper-card>
|
||||
<template is="dom-if" if="[[!_entries.length]]">
|
||||
<template is="dom-repeat" items="[[_progress]]">
|
||||
<div class="config-entry-row">
|
||||
<paper-item-body>
|
||||
Nothing configured yet
|
||||
[[_computeIntegrationTitle(localize, item.handler)]]
|
||||
</paper-item-body>
|
||||
</div>
|
||||
</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>
|
||||
<paper-button on-click="_continueFlow">Configure</paper-button>
|
||||
</div>
|
||||
</template>
|
||||
</paper-card>
|
||||
</ha-config-section>
|
||||
</template>
|
||||
|
||||
<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-section is-wide="[[isWide]]">
|
||||
<span slot="header">Configured</span>
|
||||
<paper-card>
|
||||
<template is="dom-if" if="[[!_entries.length]]">
|
||||
<div class="config-entry-row">
|
||||
<paper-item-body>
|
||||
Nothing configured yet
|
||||
</paper-item-body>
|
||||
</div>
|
||||
</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>
|
||||
</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';
|
||||
|
||||
{
|
||||
const CORE_PAGES = [
|
||||
'core',
|
||||
'customize',
|
||||
];
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin NavigateMixin
|
||||
*/
|
||||
class HaConfigNavigation extends
|
||||
LocalizeMixin(NavigateMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex">
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<paper-card>
|
||||
<template is="dom-repeat" items="[[pages]]">
|
||||
<template is="dom-if" if="[[_computeLoaded(hass, item)]]">
|
||||
<paper-item on-click="_navigate">
|
||||
<paper-item-body two-line="">
|
||||
[[_computeCaption(item, localize)]]
|
||||
<div secondary="">[[_computeDescription(item, localize)]]</div>
|
||||
</paper-item-body>
|
||||
<iron-icon icon="hass:chevron-right"></iron-icon>
|
||||
</paper-item>
|
||||
</template>
|
||||
const CORE_PAGES = [
|
||||
'core',
|
||||
'customize',
|
||||
];
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin NavigateMixin
|
||||
*/
|
||||
class HaConfigNavigation extends
|
||||
LocalizeMixin(NavigateMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex">
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<paper-card>
|
||||
<template is="dom-repeat" items="[[pages]]">
|
||||
<template is="dom-if" if="[[_computeLoaded(hass, item)]]">
|
||||
<paper-item on-click="_navigate">
|
||||
<paper-item-body two-line="">
|
||||
[[_computeCaption(item, localize)]]
|
||||
<div secondary="">[[_computeDescription(item, localize)]]</div>
|
||||
</paper-item-body>
|
||||
<iron-icon icon="hass:chevron-right"></iron-icon>
|
||||
</paper-item>
|
||||
</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 '../../util/app-localstorage-document.js';
|
||||
|
||||
{
|
||||
const ERROR_SENTINEL = {};
|
||||
class HaPanelDevService extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include='ha-style'>
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
const ERROR_SENTINEL = {};
|
||||
class HaPanelDevService extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include='ha-style'>
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.ha-form {
|
||||
margin-right: 16px;
|
||||
max-width: 400px;
|
||||
}
|
||||
.ha-form {
|
||||
margin-right: 16px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 24px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.description {
|
||||
margin-top: 24px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply --paper-font-title;
|
||||
}
|
||||
.header {
|
||||
@apply --paper-font-title;
|
||||
}
|
||||
|
||||
.attributes th {
|
||||
text-align: left;
|
||||
}
|
||||
.attributes th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.attributes tr {
|
||||
vertical-align: top;
|
||||
}
|
||||
.attributes tr {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.attributes tr:nth-child(odd) {
|
||||
background-color: var(--table-row-background-color,#eee)
|
||||
}
|
||||
.attributes tr:nth-child(odd) {
|
||||
background-color: var(--table-row-background-color,#eee)
|
||||
}
|
||||
|
||||
.attributes tr:nth-child(even) {
|
||||
background-color: var(--table-row-alternative-background-color,#eee)
|
||||
}
|
||||
.attributes tr:nth-child(even) {
|
||||
background-color: var(--table-row-alternative-background-color,#eee)
|
||||
}
|
||||
|
||||
.attributes td:nth-child(3) {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.attributes td:nth-child(3) {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
white-space: normal;
|
||||
}
|
||||
h1 {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 4px;
|
||||
}
|
||||
td {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
</style>
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
</style>
|
||||
|
||||
<app-header-layout has-scrolling-region>
|
||||
<app-header slot="header" fixed>
|
||||
<app-toolbar>
|
||||
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
|
||||
<div main-title>Services</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<app-header-layout has-scrolling-region>
|
||||
<app-header slot="header" fixed>
|
||||
<app-toolbar>
|
||||
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
|
||||
<div main-title>Services</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
<app-localstorage-document
|
||||
key='panel-dev-service-state-domain-service'
|
||||
data='{{domainService}}'>
|
||||
</app-localstorage-document>
|
||||
<app-localstorage-document
|
||||
key='[[_computeServicedataKey(domainService)]]'
|
||||
data='{{serviceData}}'>
|
||||
</app-localstorage-document>
|
||||
<app-localstorage-document
|
||||
key='panel-dev-service-state-domain-service'
|
||||
data='{{domainService}}'>
|
||||
</app-localstorage-document>
|
||||
<app-localstorage-document
|
||||
key='[[_computeServicedataKey(domainService)]]'
|
||||
data='{{serviceData}}'>
|
||||
</app-localstorage-document>
|
||||
|
||||
<div class='content'>
|
||||
<p>
|
||||
The service dev tool allows you to call any available service in Home Assistant.
|
||||
</p>
|
||||
<div class='content'>
|
||||
<p>
|
||||
The service dev tool allows you to call any available service in Home Assistant.
|
||||
</p>
|
||||
|
||||
<div class='ha-form'>
|
||||
<ha-service-picker
|
||||
<div class='ha-form'>
|
||||
<ha-service-picker
|
||||
hass='[[hass]]'
|
||||
value='{{domainService}}'
|
||||
></ha-service-picker>
|
||||
<template is='dom-if' if='[[_computeHasEntity(_attributes)]]'>
|
||||
<ha-entity-picker
|
||||
hass='[[hass]]'
|
||||
value='{{domainService}}'
|
||||
></ha-service-picker>
|
||||
<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
|
||||
value='[[_computeEntityValue(parsedJSON)]]'
|
||||
on-change='_entityPicked'
|
||||
disabled='[[!validJSON]]'
|
||||
>Call Service</paper-button>
|
||||
<template is='dom-if' if='[[!validJSON]]'>
|
||||
<span class='error'>Invalid JSON</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template is='dom-if' if='[[!domainService]]'>
|
||||
<h1>Select a service to see the description</h1>
|
||||
domain-filter='[[_computeEntityDomainFilter(_domain)]]'
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>
|
||||
</template>
|
||||
|
||||
<template is='dom-if' if='[[domainService]]'>
|
||||
<template is='dom-if' if='[[!_description]]'>
|
||||
<h1>No description is available</h1>
|
||||
</template>
|
||||
<template is='dom-if' if='[[_description]]'>
|
||||
<h3>[[_description]]</h3>
|
||||
|
||||
<table class='attributes'>
|
||||
<tr>
|
||||
<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>
|
||||
<paper-textarea
|
||||
always-float-label
|
||||
label='Service Data (JSON, optional)'
|
||||
value='{{serviceData}}'
|
||||
></paper-textarea>
|
||||
<paper-button
|
||||
on-click='_callService'
|
||||
raised
|
||||
disabled='[[!validJSON]]'
|
||||
>Call Service</paper-button>
|
||||
<template is='dom-if' if='[[!validJSON]]'>
|
||||
<span class='error'>Invalid JSON</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</app-header-layout>
|
||||
`;
|
||||
}
|
||||
<template is='dom-if' if='[[!domainService]]'>
|
||||
<h1>Select a service to see the description</h1>
|
||||
</template>
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
<template is='dom-if' if='[[domainService]]'>
|
||||
<template is='dom-if' if='[[!_description]]'>
|
||||
<h1>No description is available</h1>
|
||||
</template>
|
||||
<template is='dom-if' if='[[_description]]'>
|
||||
<h3>[[_description]]</h3>
|
||||
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
<table class='attributes'>
|
||||
<tr>
|
||||
<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: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
</app-header-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
domainService: {
|
||||
type: String,
|
||||
observer: '_domainServiceChanged',
|
||||
},
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
_domain: {
|
||||
type: String,
|
||||
computed: '_computeDomain(domainService)',
|
||||
},
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
_service: {
|
||||
type: String,
|
||||
computed: '_computeService(domainService)',
|
||||
},
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
serviceData: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
domainService: {
|
||||
type: String,
|
||||
observer: '_domainServiceChanged',
|
||||
},
|
||||
|
||||
parsedJSON: {
|
||||
type: Object,
|
||||
computed: '_computeParsedServiceData(serviceData)'
|
||||
},
|
||||
_domain: {
|
||||
type: String,
|
||||
computed: '_computeDomain(domainService)',
|
||||
},
|
||||
|
||||
validJSON: {
|
||||
type: Boolean,
|
||||
computed: '_computeValidJSON(parsedJSON)',
|
||||
},
|
||||
_service: {
|
||||
type: String,
|
||||
computed: '_computeService(domainService)',
|
||||
},
|
||||
|
||||
_attributes: {
|
||||
type: Array,
|
||||
computed: '_computeAttributesArray(hass, _domain, _service)',
|
||||
},
|
||||
serviceData: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
_description: {
|
||||
type: String,
|
||||
computed: '_computeDescription(hass, _domain, _service)',
|
||||
},
|
||||
};
|
||||
}
|
||||
parsedJSON: {
|
||||
type: Object,
|
||||
computed: '_computeParsedServiceData(serviceData)'
|
||||
},
|
||||
|
||||
_domainServiceChanged() {
|
||||
this.serviceData = '';
|
||||
}
|
||||
validJSON: {
|
||||
type: Boolean,
|
||||
computed: '_computeValidJSON(parsedJSON)',
|
||||
},
|
||||
|
||||
_computeAttributesArray(hass, domain, service) {
|
||||
const serviceDomains = hass.config.services;
|
||||
if (!(domain in serviceDomains)) return [];
|
||||
if (!(service in serviceDomains[domain])) return [];
|
||||
_attributes: {
|
||||
type: Array,
|
||||
computed: '_computeAttributesArray(hass, _domain, _service)',
|
||||
},
|
||||
|
||||
const fields = serviceDomains[domain][service].fields;
|
||||
return Object.keys(fields).map(function (field) {
|
||||
return Object.assign({ key: field }, fields[field]);
|
||||
});
|
||||
}
|
||||
_description: {
|
||||
type: String,
|
||||
computed: '_computeDescription(hass, _domain, _service)',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeDescription(hass, domain, service) {
|
||||
const serviceDomains = hass.config.services;
|
||||
if (!(domain in serviceDomains)) return undefined;
|
||||
if (!(service in serviceDomains[domain])) return undefined;
|
||||
return serviceDomains[domain][service].description;
|
||||
}
|
||||
_domainServiceChanged() {
|
||||
this.serviceData = '';
|
||||
}
|
||||
|
||||
_computeServicedataKey(domainService) {
|
||||
return `panel-dev-service-state-servicedata.${domainService}`;
|
||||
}
|
||||
_computeAttributesArray(hass, domain, service) {
|
||||
const serviceDomains = hass.config.services;
|
||||
if (!(domain in serviceDomains)) return [];
|
||||
if (!(service in serviceDomains[domain])) return [];
|
||||
|
||||
_computeDomain(domainService) {
|
||||
return domainService.split('.', 1)[0];
|
||||
}
|
||||
const fields = serviceDomains[domain][service].fields;
|
||||
return Object.keys(fields).map(function (field) {
|
||||
return Object.assign({ key: field }, fields[field]);
|
||||
});
|
||||
}
|
||||
|
||||
_computeService(domainService) {
|
||||
return domainService.split('.', 2)[1] || null;
|
||||
}
|
||||
_computeDescription(hass, domain, service) {
|
||||
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) {
|
||||
try {
|
||||
return serviceData ? JSON.parse(serviceData) : {};
|
||||
} catch (err) {
|
||||
return ERROR_SENTINEL;
|
||||
}
|
||||
}
|
||||
_computeServicedataKey(domainService) {
|
||||
return `panel-dev-service-state-servicedata.${domainService}`;
|
||||
}
|
||||
|
||||
_computeValidJSON(parsedJSON) {
|
||||
return parsedJSON !== ERROR_SENTINEL;
|
||||
}
|
||||
_computeDomain(domainService) {
|
||||
return domainService.split('.', 1)[0];
|
||||
}
|
||||
|
||||
_computeHasEntity(attributes) {
|
||||
return attributes.some(attr => attr.key === 'entity_id');
|
||||
}
|
||||
_computeService(domainService) {
|
||||
return domainService.split('.', 2)[1] || null;
|
||||
}
|
||||
|
||||
_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);
|
||||
_computeParsedServiceData(serviceData) {
|
||||
try {
|
||||
return serviceData ? JSON.parse(serviceData) : {};
|
||||
} catch (err) {
|
||||
return ERROR_SENTINEL;
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
{
|
||||
var DATE_CACHE = {};
|
||||
var DATE_CACHE = {};
|
||||
|
||||
class HaLogbookData extends PolymerElement {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: 'hassChanged',
|
||||
},
|
||||
class HaLogbookData extends PolymerElement {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: 'hassChanged',
|
||||
},
|
||||
|
||||
filterDate: {
|
||||
type: String,
|
||||
observer: 'filterDateChanged',
|
||||
},
|
||||
filterDate: {
|
||||
type: String,
|
||||
observer: 'filterDateChanged',
|
||||
},
|
||||
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
readOnly: true,
|
||||
notify: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
readOnly: true,
|
||||
notify: true,
|
||||
},
|
||||
|
||||
entries: {
|
||||
type: Object,
|
||||
value: null,
|
||||
readOnly: true,
|
||||
notify: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
entries: {
|
||||
type: Object,
|
||||
value: null,
|
||||
readOnly: true,
|
||||
notify: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
hassChanged(newHass, oldHass) {
|
||||
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;
|
||||
hassChanged(newHass, oldHass) {
|
||||
if (!oldHass && 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';
|
||||
|
||||
{
|
||||
const STORED_STATE = [
|
||||
'dockedSidebar',
|
||||
'selectedTheme',
|
||||
'selectedLanguage',
|
||||
];
|
||||
const STORED_STATE = [
|
||||
'dockedSidebar',
|
||||
'selectedTheme',
|
||||
'selectedLanguage',
|
||||
];
|
||||
|
||||
class HaPrefStorage extends PolymerElement {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
storage: {
|
||||
type: Object,
|
||||
value: window.localStorage || {},
|
||||
},
|
||||
};
|
||||
}
|
||||
class HaPrefStorage extends PolymerElement {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
storage: {
|
||||
type: Object,
|
||||
value: window.localStorage || {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
storeState() {
|
||||
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 = {};
|
||||
storeState() {
|
||||
if (!this.hass) return;
|
||||
|
||||
try {
|
||||
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]);
|
||||
}
|
||||
var value = this.hass[key];
|
||||
this.storage[key] = JSON.stringify(value === undefined ? null : value);
|
||||
}
|
||||
|
||||
return state;
|
||||
} catch (err) {
|
||||
// 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';
|
||||
|
||||
{
|
||||
/* eslint-disable no-console */
|
||||
const DEBUG = false;
|
||||
/* eslint-disable no-console */
|
||||
const DEBUG = false;
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaUrlSync extends EventsMixin(PolymerElement) {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: 'hassChanged',
|
||||
},
|
||||
};
|
||||
}
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaUrlSync extends EventsMixin(PolymerElement) {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: 'hassChanged',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
hassChanged(newHass, oldHass) {
|
||||
if (this.ignoreNextHassChange) {
|
||||
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;
|
||||
hassChanged(newHass, oldHass) {
|
||||
if (this.ignoreNextHassChange) {
|
||||
if (DEBUG) console.log('ignore hasschange');
|
||||
this.ignoreNextHassChange = false;
|
||||
this.popstateChangeListener = this.popstateChangeListener.bind(this);
|
||||
window.addEventListener('popstate', this.popstateChangeListener);
|
||||
return;
|
||||
} else if (!oldHass || oldHass.moreInfoEntityId === newHass.moreInfoEntityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener('popstate', this.popstateChangeListener);
|
||||
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();
|
||||
}
|
||||
}
|
||||
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