Entity dropdown improvement (#674)

* Ignore hass changes while dropdown is open

* Upgrade vaadin-combo-box

* Fix styling on dev-service panel

* Fix styling for ha-entity-dropdown

* Fix height vaadin-combo-box dropdown

* Rename ha-entity-dropdown to ha-entity-picker

* More entity improvement (#675)

* Update script and automation editor to use entity picker

* Add entity and service picker to service dev panel

* Lint
This commit is contained in:
Paulus Schoutsen 2017-11-25 16:00:43 -08:00 committed by GitHub
parent 28457747e7
commit 0707528bd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 730 additions and 304 deletions

View File

@ -48,6 +48,7 @@
"import/prefer-default-export": 0,
"import/no-unresolved": 0,
"import/extensions": [2, "ignorePackages"],
"object-curly-newline": 0,
"react/jsx-no-bind": [2, { "ignoreRefs": true }],
"react/jsx-no-duplicate-props": 2,
"react/self-closing-comp": 2,

View File

@ -51,7 +51,7 @@
"paper-toast": "PolymerElements/paper-toast#^2.0.0",
"paper-toggle-button": "PolymerElements/paper-toggle-button#^2.0.0",
"polymer": "^2.1.1",
"vaadin-combo-box": "vaadin/vaadin-combo-box#^2.0.0",
"vaadin-combo-box": "vaadin/vaadin-combo-box#^3.0.2",
"vaadin-date-picker": "vaadin/vaadin-date-picker#^2.0.0",
"web-animations-js": "^2.2.5",
"webcomponentsjs": "^1.0.10"

View File

@ -42,7 +42,7 @@ export default class Automation extends Component {
});
}
render({ automation, isWide }) {
render({ automation, isWide, hass }) {
const {
alias, trigger, condition, action
} = automation;
@ -77,7 +77,11 @@ export default class Automation extends Component {
Learn more about triggers.
</a></p>
</span>
<Trigger trigger={trigger} onChange={this.triggerChanged} />
<Trigger
trigger={trigger}
onChange={this.triggerChanged}
hass={hass}
/>
</ha-config-section>
<ha-config-section is-wide={isWide}>
@ -93,7 +97,11 @@ export default class Automation extends Component {
Learn more about conditions.
</a></p>
</span>
<Condition condition={condition || []} onChange={this.conditionChanged} />
<Condition
condition={condition || []}
onChange={this.conditionChanged}
hass={hass}
/>
</ha-config-section>
<ha-config-section is-wide={isWide}>
@ -104,7 +112,11 @@ export default class Automation extends Component {
Learn more about actions.
</a></p>
</span>
<Script script={action} onChange={this.actionChanged} />
<Script
script={action}
onChange={this.actionChanged}
hass={hass}
/>
</ha-config-section>
</div>
);

View File

@ -32,7 +32,7 @@ export default class Trigger extends Component {
this.props.onChange(trigger);
}
render({ trigger }) {
render({ trigger, hass }) {
return (
<div class="triggers">
{trigger.map((trg, idx) => (
@ -40,6 +40,7 @@ export default class Trigger extends Component {
index={idx}
trigger={trg}
onChange={this.triggerChanged}
hass={hass}
/>))}
<paper-card>
<div class='card-actions add-card'>

View File

@ -7,20 +7,29 @@ export default class NumericStateTrigger extends Component {
super();
this.onChange = onChangeEvent.bind(this, 'trigger');
this.entityPicked = this.entityPicked.bind(this);
}
entityPicked(ev) {
this.props.onChange(this.props.index, {
...this.props.trigger,
entity_id: ev.target.value,
});
}
/* eslint-disable camelcase */
render({ trigger }) {
render({ trigger, hass }) {
const {
value_template, entity_id, below, above
} = trigger;
return (
<div>
<paper-input
label="Entity Id"
name="entity_id"
<ha-entity-picker
value={entity_id}
onChange={this.onChange}
onChange={this.entityPicked}
hass={hass}
allowCustomEntity
/>
<paper-input
label="Above"

View File

@ -7,20 +7,28 @@ export default class StateTrigger extends Component {
super();
this.onChange = onChangeEvent.bind(this, 'trigger');
this.entityPicked = this.entityPicked.bind(this);
}
entityPicked(ev) {
this.props.onChange(this.props.index, {
...this.props.trigger,
entity_id: ev.target.value,
});
}
/* eslint-disable camelcase */
render({ trigger }) {
render({ trigger, hass }) {
const { entity_id, to } = trigger;
const trgFrom = trigger.from;
const trgFor = trigger.for;
return (
<div>
<paper-input
label="Entity Id"
name="entity_id"
<ha-entity-picker
value={entity_id}
onChange={this.onChange}
onChange={this.entityPicked}
hass={hass}
allowCustomEntity
/>
<paper-input
label="From"

View File

@ -42,7 +42,7 @@ export default class TriggerEdit extends Component {
}
}
render({ index, trigger, onChange }) {
render({ index, trigger, onChange, hass }) {
const Comp = TYPES[trigger.platform];
const selected = OPTIONS.indexOf(trigger.platform);
@ -69,6 +69,7 @@ export default class TriggerEdit extends Component {
index={index}
trigger={trigger}
onChange={onChange}
hass={hass}
/>
</div>
);

View File

@ -1,6 +1,12 @@
import { h, Component } from 'preact';
import { onChangeEvent } from '../../common/util/event.js';
import { hasLocation } from '../../common/util/location.js';
import computeDomain from '../../common/util/compute_domain.js';
function zoneAndLocationFilter(stateObj) {
return hasLocation(stateObj) && computeDomain(stateObj) !== 'zone';
}
export default class ZoneTrigger extends Component {
constructor() {
@ -8,6 +14,22 @@ export default class ZoneTrigger extends Component {
this.onChange = onChangeEvent.bind(this, 'trigger');
this.radioGroupPicked = this.radioGroupPicked.bind(this);
this.entityPicked = this.entityPicked.bind(this);
this.zonePicked = this.zonePicked.bind(this);
}
entityPicked(ev) {
this.props.onChange(this.props.index, {
...this.props.trigger,
entity_id: ev.target.value,
});
}
zonePicked(ev) {
this.props.onChange(this.props.index, {
...this.props.trigger,
zone: ev.target.value,
});
}
radioGroupPicked(ev) {
@ -18,21 +40,25 @@ export default class ZoneTrigger extends Component {
}
/* eslint-disable camelcase */
render({ trigger }) {
render({ trigger, hass }) {
const { entity_id, zone, event } = trigger;
return (
<div>
<paper-input
label="Entity Id"
name="entity_id"
<ha-entity-picker
label='Entity with location'
value={entity_id}
onChange={this.onChange}
onChange={this.entityPicked}
hass={hass}
allowCustomEntity
entityFilter={zoneAndLocationFilter}
/>
<paper-input
label="Zone"
name="zone"
<ha-entity-picker
label='Zone'
value={zone}
onChange={this.onChange}
onChange={this.zonePicked}
hass={hass}
allowCustomEntity
domainFilter='zone'
/>
<label id="eventlabel">Event:</label>
<paper-radio-group

View File

@ -36,7 +36,7 @@ export default class ConditionRow extends Component {
}
}
render({ index, condition, onChange }) {
render({ index, condition, onChange, hass }) {
const Comp = TYPES[condition.condition];
const selected = OPTIONS.indexOf(condition.condition);
@ -64,6 +64,7 @@ export default class ConditionRow extends Component {
index={index}
condition={condition}
onChange={onChange}
hass={hass}
/>
</div>
);

View File

@ -30,7 +30,7 @@ export default class Condition extends Component {
this.props.onChange(condition);
}
render({ condition }) {
render({ condition, hass }) {
return (
<div class="triggers">
{condition.map((cnd, idx) => (
@ -38,6 +38,7 @@ export default class Condition extends Component {
index={idx}
condition={cnd}
onChange={this.conditionChanged}
hass={hass}
/>))}
<paper-card>
<div class='card-actions add-card'>

View File

@ -7,20 +7,28 @@ export default class NumericStateCondition extends Component {
super();
this.onChange = onChangeEvent.bind(this, 'condition');
this.entityPicked = this.entityPicked.bind(this);
}
entityPicked(ev) {
this.props.onChange(this.props.index, {
...this.props.condition,
entity_id: ev.target.value,
});
}
/* eslint-disable camelcase */
render({ condition }) {
render({ condition, hass }) {
const {
value_template, entity_id, below, above
} = condition;
return (
<div>
<paper-input
label="Entity Id"
name="entity_id"
<ha-entity-picker
value={entity_id}
onChange={this.onChange}
onChange={this.entityPicked}
hass={hass}
allowCustomEntity
/>
<paper-input
label="Above"

View File

@ -7,19 +7,27 @@ export default class StateCondition extends Component {
super();
this.onChange = onChangeEvent.bind(this, 'condition');
this.entityPicked = this.entityPicked.bind(this);
}
entityPicked(ev) {
this.props.onChange(this.props.index, {
...this.props.condition,
entity_id: ev.target.value,
});
}
/* eslint-disable camelcase */
render({ condition }) {
render({ condition, hass }) {
const { entity_id, state } = condition;
const cndFor = condition.for;
return (
<div>
<paper-input
label="Entity Id"
name="entity_id"
<ha-entity-picker
value={entity_id}
onChange={this.onChange}
onChange={this.entityPicked}
hass={hass}
allowCustomEntity
/>
<paper-input
label="State"

View File

@ -1,30 +1,56 @@
import { h, Component } from 'preact';
import { onChangeEvent } from '../../util/event.js';
import { hasLocation } from '../../util/location.js';
import computeDomain from '../../util/compute_domain.js';
function zoneAndLocationFilter(stateObj) {
return hasLocation(stateObj) && computeDomain(stateObj) !== 'zone';
}
export default class ZoneCondition extends Component {
constructor() {
super();
this.onChange = onChangeEvent.bind(this, 'condition');
this.entityPicked = this.entityPicked.bind(this);
this.zonePicked = this.zonePicked.bind(this);
}
entityPicked(ev) {
this.props.onChange(this.props.index, {
...this.props.condition,
entity_id: ev.target.value,
});
}
zonePicked(ev) {
this.props.onChange(this.props.index, {
...this.props.condition,
zone: ev.target.value,
});
}
/* eslint-disable camelcase */
render({ condition }) {
render({ condition, hass }) {
const { entity_id, zone } = condition;
return (
<div>
<paper-input
label="Entity Id"
name="entity_id"
<ha-entity-picker
label='Entity with location'
value={entity_id}
onChange={this.onChange}
onChange={this.entityPicked}
hass={hass}
allowCustomEntity
entityFilter={zoneAndLocationFilter}
/>
<paper-input
label="Zone entity id"
name="zone"
<ha-entity-picker
label='Zone'
value={zone}
onChange={this.onChange}
onChange={this.zonePicked}
hass={hass}
allowCustomEntity
domainFilter='zone'
/>
</div>
);

View File

@ -42,7 +42,7 @@ export default class Action extends Component {
}
}
render({ index, action, onChange }) {
render({ index, action, onChange, hass }) {
const type = getType(action);
const Comp = type && TYPES[type];
const selected = OPTIONS.indexOf(type);
@ -70,6 +70,7 @@ export default class Action extends Component {
index={index}
action={action}
onChange={onChange}
hass={hass}
/>
</div>
);

View File

@ -1,16 +1,22 @@
import { h, Component } from 'preact';
import JSONTextArea from '../json_textarea.js';
import { onChangeEvent } from '../../util/event.js';
export default class CallServiceAction extends Component {
constructor() {
super();
this.onChange = onChangeEvent.bind(this, 'action');
this.serviceChanged = this.serviceChanged.bind(this);
this.serviceDataChanged = this.serviceDataChanged.bind(this);
}
serviceChanged(ev) {
this.props.onChange(this.props.index, {
...this.props.action,
service: ev.target.value,
});
}
serviceDataChanged(data) {
this.props.onChange(this.props.index, {
...this.props.action,
@ -18,21 +24,15 @@ export default class CallServiceAction extends Component {
});
}
render({ action }) {
const { alias, service, data } = action;
render({ action, hass }) {
const { service, data } = action;
return (
<div>
<paper-input
label="Alias"
name="alias"
value={alias}
onChange={this.onChange}
/>
<paper-input
label="Service"
name="service"
<ha-service-picker
hass={hass}
value={service}
onChange={this.onChange}
onChange={this.serviceChanged}
/>
<JSONTextArea
label="Service Data"

View File

@ -5,12 +5,13 @@ import ConditionEdit from '../condition/condition_edit.js';
export default class ConditionAction extends Component {
// eslint-disable-next-line
render({ action, index, onChange }) {
render({ action, index, onChange, hass }) {
return (
<ConditionEdit
condition={action}
onChange={onChange}
index={index}
hass={hass}
/>
);
}

View File

@ -30,7 +30,7 @@ export default class Script extends Component {
this.props.onChange(script);
}
render({ script }) {
render({ script, hass }) {
return (
<div class="script">
{script.map((act, idx) => (
@ -38,6 +38,7 @@ export default class Script extends Component {
index={idx}
action={act}
onChange={this.actionChanged}
hass={hass}
/>))}
<paper-card>
<div class='card-actions add-card'>

View File

@ -0,0 +1,4 @@
export function hasLocation(stateObj) {
return ('latitude' in stateObj.attributes &&
'longitude' in stateObj.attributes);
}

View File

@ -16,6 +16,9 @@
<link rel="import" href="../../../bower_components/paper-fab/paper-fab.html">
<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
<link rel='import' href='../../../src/components/entity/ha-entity-picker.html'>
<link rel='import' href='../../../src/components/ha-combo-box.html'>
<link rel='import' href='../../../src/components/ha-service-picker.html'>
<link rel='import' href='../../../src/util/hass-mixins.html'>
<link rel="import" href="../ha-config-section.html">
@ -117,6 +120,7 @@ class HaAutomationEditor extends window.hassMixins.EventsMixin(Polymer.Element)
return {
hass: {
type: Object,
observer: '_updateComponent',
},
narrow: {
@ -160,7 +164,7 @@ class HaAutomationEditor extends window.hassMixins.EventsMixin(Polymer.Element)
isWide: {
type: Boolean,
observer: 'isWideChanged',
observer: '_updateComponent',
},
};
}
@ -169,6 +173,7 @@ class HaAutomationEditor extends window.hassMixins.EventsMixin(Polymer.Element)
super.ready();
this.configChanged = this.configChanged.bind(this);
this._rendered = null;
this._renderScheduled = false;
}
disconnectedCallback() {
@ -184,13 +189,13 @@ class HaAutomationEditor extends window.hassMixins.EventsMixin(Polymer.Element)
this.config = config;
this.errors = null;
this.dirty = true;
this._updateComponent(config);
this._updateComponent();
}
automationChanged(newVal, oldVal) {
if (!newVal) return;
if (!this.hass) {
setTimeout(this.automationChanged.bind(this, newVal, oldVal), 0);
setTimeout(() => this.automationChanged(newVal, oldVal), 0);
return;
}
if (oldVal && oldVal.attributes.id === newVal.attributes.id) {
@ -230,11 +235,6 @@ class HaAutomationEditor extends window.hassMixins.EventsMixin(Polymer.Element)
this._updateComponent();
}
isWideChanged() {
if (this.config === null) return;
this._updateComponent();
}
backTapped() {
if (this.dirty &&
// eslint-disable-next-line
@ -245,11 +245,17 @@ class HaAutomationEditor extends window.hassMixins.EventsMixin(Polymer.Element)
}
_updateComponent() {
this._rendered = window.AutomationEditor(this.$.root, {
automation: this.config,
onChange: this.configChanged,
isWide: this.isWide,
}, this._rendered);
if (this._renderScheduled || !this.hass || !this.config) return;
this._renderScheduled = true;
Promise.resolve().then(() => {
this._rendered = window.AutomationEditor(this.$.root, {
automation: this.config,
onChange: this.configChanged,
isWide: this.isWide,
hass: this.hass,
}, this._rendered);
this._renderScheduled = false;
});
}
saveAutomation() {

View File

@ -8,9 +8,9 @@
<link rel='import' href='../../bower_components/app-layout/app-toolbar/app-toolbar.html'>
<link rel="import" href="../../bower_components/app-storage/app-localstorage/app-localstorage-document.html">
<link rel="import" href="../../bower_components/vaadin-combo-box/vaadin-combo-box.html">
<link rel='import' href='../../src/components/ha-menu-button.html'>
<link rel='import' href='../../src/components/entity/ha-entity-picker.html'>
<link rel='import' href='../../src/components/ha-service-picker.html'>
<link rel='import' href='../../src/resources/ha-style.html'>
<dom-module id='ha-panel-dev-service'>
@ -28,7 +28,7 @@
.ha-form {
margin-right: 16px;
max-width: 500px;
max-width: 400px;
}
.description {
@ -68,6 +68,14 @@
h1 {
white-space: normal;
}
td {
padding: 4px;
}
.error {
color: var(--google-red-500);
}
</style>
<app-header-layout has-scrolling-region>
@ -79,69 +87,77 @@
</app-header>
<app-localstorage-document
key='panel-dev-service-state-domain'
data='{{domain}}'>
key='panel-dev-service-state-domain-service'
data='{{domainService}}'>
</app-localstorage-document>
<app-localstorage-document
key='[[computeServiceKey(domain)]]'
data='{{service}}'>
</app-localstorage-document>
<app-localstorage-document
key='[[computeServicedataKey(domain, service)]]'
key='[[_computeServicedataKey(domainService)]]'
data='{{serviceData}}'>
</app-localstorage-document>
<div class='content'>
<p>
Call a service from a component.
The service dev tool allows you to call any available service in Home Assistant.
</p>
<div class='ha-form'>
<vaadin-combo-box label='Domain' items='[[computeDomains(serviceDomains)]]' value='{{domain}}'></vaadin-combo-box>
<vaadin-combo-box label='Service' items='[[computeServices(serviceDomains, domain)]]' value='{{service}}'></vaadin-combo-box>
<ha-service-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-tap='callService' raised>Call Service</paper-button>
<paper-button
on-tap='_callService'
raised
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='[[!domain]]'>
<h1>Select a domain and service to see the description</h1>
<template is='dom-if' if='[[!domainService]]'>
<h1>Select a service to see the description</h1>
</template>
<template is='dom-if' if='[[domain]]'>
<template is='dom-if' if='[[!service]]'>
<h1>Select a service to see the description</h1>
<template is='dom-if' if='[[domainService]]'>
<template is='dom-if' if='[[!_description]]'>
<h1>No description is available</h1>
</template>
</template>
<template is='dom-if' if='[[_description]]'>
<h3>[[_description]]</h3>
<template is='dom-if' if='[[domain]]'>
<template is='dom-if' if='[[service]]'>
<template is='dom-if' if='[[!_description]]'>
<h1>No description is available</h1>
</template>
<template is='dom-if' if='[[_description]]'>
<h3>[[_description]]</h3>
</template>
<template is='dom-if' if='[[_attributes.length]]'>
<h1>Valid Parameters</h1>
<table class='attributes'>
<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>
<th>Parameter</th>
<th>Description</th>
<th>Example</th>
<td><pre>[[attribute.key]]</pre></td>
<td>[[attribute.description]]</td>
<td>[[attribute.example]]</td>
</tr>
<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>
</table>
</template>
</template>
</div>
@ -151,128 +167,143 @@
</dom-module>
<script>
class HaPanelDevService extends Polymer.Element {
static get is() { return 'ha-panel-dev-service'; }
{
const ERROR_SENTINEL = {};
class HaPanelDevService extends Polymer.Element {
static get is() { return 'ha-panel-dev-service'; }
static get properties() {
return {
hass: {
type: Object,
},
static get properties() {
return {
hass: {
type: Object,
},
narrow: {
type: Boolean,
value: false,
},
narrow: {
type: Boolean,
value: false,
},
showMenu: {
type: Boolean,
value: false,
},
showMenu: {
type: Boolean,
value: false,
},
domain: {
type: String,
value: '',
observer: 'domainChanged',
},
domainService: {
type: String,
observer: '_domainServiceChanged',
},
service: {
type: String,
value: '',
observer: 'serviceChanged',
},
_domain: {
type: String,
computed: '_computeDomain(domainService)',
},
serviceData: {
type: String,
value: '',
},
_service: {
type: String,
computed: '_computeService(domainService)',
},
_attributes: {
type: Array,
computed: 'computeAttributesArray(serviceDomains, domain, service)',
},
serviceData: {
type: String,
value: '',
},
_description: {
type: String,
computed: 'computeDescription(serviceDomains, domain, service)',
},
parsedJSON: {
type: Object,
computed: '_computeParsedServiceData(serviceData)'
},
serviceDomains: {
type: Object,
computed: 'computeServiceDomains(hass)',
},
};
}
validJSON: {
type: Boolean,
computed: '_computeValidJSON(parsedJSON)',
},
computeServiceDomains(hass) {
return hass.config.services;
}
_attributes: {
type: Array,
computed: '_computeAttributesArray(hass, _domain, _service)',
},
computeAttributesArray(serviceDomains, domain, service) {
if (!serviceDomains) return [];
if (!(domain in serviceDomains)) return [];
if (!(service in serviceDomains[domain])) return [];
var fields = serviceDomains[domain][service].fields;
return Object.keys(fields).map(function (field) {
return Object.assign({}, fields[field], { key: field });
});
}
computeDescription(serviceDomains, domain, service) {
if (!serviceDomains) return undefined;
if (!(domain in serviceDomains)) return undefined;
if (!(service in serviceDomains[domain])) return undefined;
return serviceDomains[domain][service].description;
}
computeDomains(serviceDomains) {
return Object.keys(serviceDomains).sort();
}
computeServices(serviceDomains, domain) {
if (!(domain in serviceDomains)) return [];
return Object.keys(serviceDomains[domain]).sort();
}
computeServiceKey(domain) {
if (!domain) {
return 'panel-dev-service-state-service';
}
return 'panel-dev-service-state-service.' + domain;
}
computeServicedataKey(domain, service) {
if (!domain || !service) {
return 'panel-dev-service-state-servicedata';
}
return 'panel-dev-service-state-servicedata.' + domain + '.' + service;
}
domainChanged() {
this.service = '';
this.serviceData = '';
}
serviceChanged() {
this.serviceData = '';
}
callService() {
var serviceData;
try {
serviceData = this.serviceData ? JSON.parse(this.serviceData) : {};
} catch (err) {
/* eslint-disable no-alert */
alert('Error parsing JSON: ' + err);
/* eslint-enable no-alert */
return;
_description: {
type: String,
computed: '_computeDescription(hass, _domain, _service)',
},
};
}
this.hass.callService(this.domain, this.service, serviceData);
_domainServiceChanged() {
this.serviceData = '';
}
_computeAttributesArray(hass, domain, service) {
const serviceDomains = hass.config.services;
if (!(domain in serviceDomains)) return [];
if (!(service in serviceDomains[domain])) return [];
const fields = serviceDomains[domain][service].fields;
return Object.keys(fields).map(function (field) {
return Object.assign({ key: field }, fields[field]);
});
}
_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;
}
_computeServicedataKey(domainService) {
return `panel-dev-service-state-servicedata.${domainService}`;
}
_computeDomain(domainService) {
return domainService.split('.', 1)[0];
}
_computeService(domainService) {
return domainService.split('.', 2)[1] || null;
}
_computeParsedServiceData(serviceData) {
try {
return serviceData ? JSON.parse(serviceData) : {};
} catch (err) {
return ERROR_SENTINEL;
}
}
_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(HaPanelDevService.is, HaPanelDevService);
}
customElements.define(HaPanelDevService.is, HaPanelDevService);
</script>

View File

@ -10,7 +10,7 @@
<link rel="import" href="../../bower_components/app-layout/app-toolbar/app-toolbar.html">
<link rel="import" href="../../src/components/ha-menu-button.html">
<link rel="import" href="../../src/components/entity/ha-entity-dropdown.html">
<link rel="import" href="../../src/components/entity/ha-entity-picker.html">
<link rel="import" href="../../src/resources/ha-style.html">
<dom-module id="ha-panel-dev-state">
@ -26,9 +26,9 @@
padding: 16px;
}
ha-entity-dropdown {
ha-entity-picker, .state-input, paper-textarea {
display: block;
max-width: 300px;
max-width: 400px;
}
.entities th {
@ -72,12 +72,18 @@
This will not communicate with the actual device.
</p>
<ha-entity-dropdown
<ha-entity-picker
autofocus
hass="[[hass]]"
value="{{_entityId}}"
></ha-entity-dropdown>
<paper-input label="State" required value='{{_state}}'></paper-input>
allow-custom-entity
></ha-entity-picker>
<paper-input
label="State"
required
value='{{_state}}'
class='state-input'
></paper-input>
<paper-textarea label="State attributes (JSON, optional)" value='{{_stateAttributes}}'></paper-textarea>
<paper-button on-tap='handleSetState' raised>Set State</paper-button>
</div>

View File

@ -12,8 +12,6 @@
<link rel='import' href='../../bower_components/app-layout/app-toolbar/app-toolbar.html'>
<link rel="import" href="../../bower_components/app-storage/app-localstorage/app-localstorage-document.html">
<link rel="import" href="../../bower_components/vaadin-combo-box/vaadin-combo-box.html">
<link rel='import' href='../../src/components/ha-menu-button.html'>
<link rel='import' href='../../src/resources/ha-style.html'>

View File

@ -1,67 +0,0 @@
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/vaadin-combo-box/vaadin-combo-box.html">
<link rel="import" href="../../../bower_components/paper-listbox/paper-listbox.html">
<link rel="import" href="../../../bower_components/paper-item/paper-icon-item.html">
<link rel="import" href="../../../bower_components/paper-item/paper-item-body.html">
<link rel="import" href="./state-badge.html">
<dom-module id="ha-entity-dropdown">
<template>
<vaadin-combo-box
autofocus="[[autofocus]]"
label="[[label]]"
items='[[computeStates(hass)]]'
item-value-path='entity_id'
item-label-path='entity_id'
value='{{value}}'
>
<template>
<style>
paper-icon-item {
margin: -13px -16px;
}
</style>
<paper-icon-item>
<state-badge state-obj="[[item]]" slot='item-icon'></state-badge>
<paper-item-body two-line>
<div>[[computeStateName(item)]]</div>
<div secondary>[[item.entity_id]]</div>
</paper-item-body>
</paper-icon-item>
</template>
</vaadin-combo-box>
</template>
</dom-module>
<script>
class HaEntityDropdown extends Polymer.Element {
static get is() { return 'ha-entity-dropdown'; }
static get properties() {
return {
hass: Object,
autofocus: Boolean,
label: {
type: String,
value: 'Entity',
},
value: {
type: String,
notify: true,
}
};
}
computeStates(hass) {
return Object.keys(hass.states).sort().map(key => hass.states[key]);
}
computeStateName(state) {
return window.hassUtil.computeStateName(state);
}
}
customElements.define(HaEntityDropdown.is, HaEntityDropdown);
</script>

View File

@ -0,0 +1,152 @@
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/vaadin-combo-box/vaadin-combo-box-light.html">
<link rel="import" href="../../../bower_components/paper-item/paper-icon-item.html">
<link rel="import" href="../../../bower_components/paper-item/paper-item-body.html">
<link rel="import" href="./state-badge.html">
<dom-module id="ha-entity-picker">
<template>
<style>
paper-input > paper-icon-button {
width: 24px;
height: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
</style>
<vaadin-combo-box-light
items='[[_states]]'
item-value-path='entity_id'
item-label-path='entity_id'
value='{{value}}'
opened='{{opened}}'
allow-custom-value='[[allowCustomEntity]]'
>
<paper-input
autofocus="[[autofocus]]"
label="[[label]]"
class="input"
disabled='[[disabled]]'
>
<paper-icon-button
slot="suffix"
class="clear-button"
icon="mdi:close"
no-ripple
hidden$='[[!value]]'
>Clear</paper-icon-button>
<paper-icon-button
slot="suffix"
class="toggle-button"
icon='[[_computeToggleIcon(opened)]]'
hidden='[[!_states.length]]'
>Toggle</paper-icon-button>
</paper-input>
<template>
<style>
paper-icon-item {
margin: -10px;
}
</style>
<paper-icon-item>
<state-badge state-obj="[[item]]" slot='item-icon'></state-badge>
<paper-item-body two-line>
<div>[[computeStateName(item)]]</div>
<div secondary>[[item.entity_id]]</div>
</paper-item-body>
</paper-icon-item>
</template>
</vaadin-combo-box-light>
</template>
</dom-module>
<script>
class HaEntityPicker extends Polymer.Element {
static get is() { return 'ha-entity-picker'; }
static get properties() {
return {
allowCustomEntity: {
type: Boolean,
value: false,
},
hass: {
type: Object,
observer: '_hassChanged',
},
_hass: Object,
_states: {
type: Array,
computed: '_computeStates(_hass, domainFilter, entityFilter)',
},
autofocus: Boolean,
label: {
type: String,
value: 'Entity',
},
value: {
type: String,
notify: true,
},
opened: {
type: Boolean,
value: false,
observer: '_openedChanged',
},
domainFilter: {
type: String,
value: null,
},
entityFilter: {
type: Function,
value: null,
},
disabled: Boolean,
};
}
_computeStates(hass, domainFilter, entityFilter) {
if (!hass) return [];
let entityIds = Object.keys(hass.states);
if (domainFilter) {
entityIds = entityIds.filter(eid => eid.substr(0, eid.indexOf('.')) === domainFilter);
}
let entities = entityIds.sort().map(key => hass.states[key]);
if (entityFilter) {
entities = entities.filter(entityFilter);
}
return entities;
}
computeStateName(state) {
return window.hassUtil.computeStateName(state);
}
_openedChanged(newVal) {
if (!newVal) {
this._hass = this.hass;
}
}
_hassChanged(newVal) {
if (!this.opened) {
this._hass = newVal;
}
}
_computeToggleIcon(opened) {
return opened ? 'mdi:menu-up' : 'mdi:menu-down';
}
}
customElements.define(HaEntityPicker.is, HaEntityPicker);
</script>

View File

@ -0,0 +1,106 @@
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../bower_components/vaadin-combo-box/vaadin-combo-box-light.html">
<link rel="import" href="../../bower_components/paper-item/paper-item.html">
<dom-module id="ha-combo-box">
<template>
<style>
paper-input > paper-icon-button {
width: 24px;
height: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
</style>
<vaadin-combo-box-light
items='[[items]]'
item-value-path='[[itemValuePath]]'
item-label-path='[[itemLabelPath]]'
value='{{value}}'
opened='{{opened}}'
allow-custom-value='[[allowCustomValue]]'
>
<paper-input
autofocus="[[autofocus]]"
label="[[label]]"
class="input"
>
<paper-icon-button
slot="suffix"
class="clear-button"
icon="mdi:close"
hidden$='[[!value]]'
>Clear</paper-icon-button>
<paper-icon-button
slot="suffix"
class="toggle-button"
icon='[[_computeToggleIcon(opened)]]'
hidden$='[[!items.length]]'
>Toggle</paper-icon-button>
</paper-input>
<template>
<style>
paper-item {
margin: -5px -10px;
}
</style>
<paper-item>[[_computeItemLabel(item, itemLabelPath)]]</paper-item>
</template>
</vaadin-combo-box-light>
</template>
</dom-module>
<script>
class HaComboBox extends Polymer.Element {
static get is() { return 'ha-combo-box'; }
static get properties() {
return {
allowCustomValue: Boolean,
items: {
type: Object,
observer: '_itemsChanged',
},
_items: Object,
itemLabelPath: String,
itemValuePath: String,
autofocus: Boolean,
label: String,
opened: {
type: Boolean,
value: false,
observer: '_openedChanged',
},
value: {
type: String,
notify: true,
},
};
}
_openedChanged(newVal) {
if (!newVal) {
this._items = this.items;
}
}
_itemsChanged(newVal) {
if (!this.opened) {
this._items = newVal;
}
}
_computeToggleIcon(opened) {
return opened ? 'mdi:menu-up' : 'mdi:menu-down';
}
_computeItemLabel(item, itemLabelPath) {
return itemLabelPath ? item[itemLabelPath] : item;
}
}
customElements.define(HaComboBox.is, HaComboBox);
</script>

View File

@ -0,0 +1,46 @@
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<link rel="import" href="./ha-combo-box.html">
<dom-module id="ha-service-picker">
<template>
<ha-combo-box
label='Service'
items='[[_computeServices(hass)]]'
value='{{value}}'
allow-custom-value
></ha-combo-box>
</template>
</dom-module>
<script>
class HaServicePicker extends Polymer.Element {
static get is() { return 'ha-service-picker'; }
static get properties() {
return {
hass: Object,
value: {
type: String,
notify: true,
},
};
}
_computeServices(hass) {
const result = [];
Object.keys(hass.config.services).sort().forEach((domain) => {
const services = Object.keys(hass.config.services[domain]).sort();
for (let i = 0; i < services.length; i++) {
result.push(`${domain}.${services[i]}`);
}
});
return result;
}
}
customElements.define(HaServicePicker.is, HaServicePicker);
</script>

View File

@ -4,6 +4,7 @@
<style is="custom-style">/* remove is= on Polymer 2 */
body {
font-size: 14px;
height: 100vh;
/* for paper-toggle-button */
--paper-grey-50: #fafafa;

View File

@ -0,0 +1,38 @@
import { hasLocation } from '../../../js/common/util/location';
const assert = require('assert');
describe('hasLocation', () => {
it('flags states with location', () => {
const stateObj = {
attributes: {
latitude: 12.34,
longitude: 12.34
}
};
assert(hasLocation(stateObj));
});
it('does not flag states with only latitude', () => {
const stateObj = {
attributes: {
latitude: 12.34,
}
};
assert(!hasLocation(stateObj));
});
it('does not flag states with only longitude', () => {
const stateObj = {
attributes: {
longitude: 12.34
}
};
assert(!hasLocation(stateObj));
});
it('does not flag states with no location', () => {
const stateObj = {
attributes: {
}
};
assert(!hasLocation(stateObj));
});
});