Remove no longer needed blocks (#1262)

This commit is contained in:
Paulus Schoutsen 2018-06-05 21:20:34 -04:00 committed by GitHub
parent 4d48a63141
commit 10c997b7b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 2561 additions and 2593 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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, &quot;account&quot;)]]" 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, &quot;account&quot;)]]" 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, &quot;login&quot;)]]" 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, &quot;login&quot;)]]" 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, &quot;register&quot;)]]" 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, &quot;register&quot;)]]" 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, &quot;forgot-password&quot;)]]" 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, &quot;forgot-password&quot;)]]" 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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);