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/prefer-default-export": 0,
"import/no-unresolved": 0, "import/no-unresolved": 0,
"import/extensions": [2, "ignorePackages"], "import/extensions": [2, "ignorePackages"],
"object-curly-newline": 0,
"react/jsx-no-bind": [2, { "ignoreRefs": true }], "react/jsx-no-bind": [2, { "ignoreRefs": true }],
"react/jsx-no-duplicate-props": 2, "react/jsx-no-duplicate-props": 2,
"react/self-closing-comp": 2, "react/self-closing-comp": 2,

View File

@ -51,7 +51,7 @@
"paper-toast": "PolymerElements/paper-toast#^2.0.0", "paper-toast": "PolymerElements/paper-toast#^2.0.0",
"paper-toggle-button": "PolymerElements/paper-toggle-button#^2.0.0", "paper-toggle-button": "PolymerElements/paper-toggle-button#^2.0.0",
"polymer": "^2.1.1", "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", "vaadin-date-picker": "vaadin/vaadin-date-picker#^2.0.0",
"web-animations-js": "^2.2.5", "web-animations-js": "^2.2.5",
"webcomponentsjs": "^1.0.10" "webcomponentsjs": "^1.0.10"

View File

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

View File

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

View File

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

View File

@ -7,20 +7,28 @@ export default class StateTrigger extends Component {
super(); super();
this.onChange = onChangeEvent.bind(this, 'trigger'); 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 */ /* eslint-disable camelcase */
render({ trigger }) { render({ trigger, hass }) {
const { entity_id, to } = trigger; const { entity_id, to } = trigger;
const trgFrom = trigger.from; const trgFrom = trigger.from;
const trgFor = trigger.for; const trgFor = trigger.for;
return ( return (
<div> <div>
<paper-input <ha-entity-picker
label="Entity Id"
name="entity_id"
value={entity_id} value={entity_id}
onChange={this.onChange} onChange={this.entityPicked}
hass={hass}
allowCustomEntity
/> />
<paper-input <paper-input
label="From" 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 Comp = TYPES[trigger.platform];
const selected = OPTIONS.indexOf(trigger.platform); const selected = OPTIONS.indexOf(trigger.platform);
@ -69,6 +69,7 @@ export default class TriggerEdit extends Component {
index={index} index={index}
trigger={trigger} trigger={trigger}
onChange={onChange} onChange={onChange}
hass={hass}
/> />
</div> </div>
); );

View File

@ -1,6 +1,12 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { onChangeEvent } from '../../common/util/event.js'; 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 { export default class ZoneTrigger extends Component {
constructor() { constructor() {
@ -8,6 +14,22 @@ export default class ZoneTrigger extends Component {
this.onChange = onChangeEvent.bind(this, 'trigger'); this.onChange = onChangeEvent.bind(this, 'trigger');
this.radioGroupPicked = this.radioGroupPicked.bind(this); 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) { radioGroupPicked(ev) {
@ -18,21 +40,25 @@ export default class ZoneTrigger extends Component {
} }
/* eslint-disable camelcase */ /* eslint-disable camelcase */
render({ trigger }) { render({ trigger, hass }) {
const { entity_id, zone, event } = trigger; const { entity_id, zone, event } = trigger;
return ( return (
<div> <div>
<paper-input <ha-entity-picker
label="Entity Id" label='Entity with location'
name="entity_id"
value={entity_id} value={entity_id}
onChange={this.onChange} onChange={this.entityPicked}
hass={hass}
allowCustomEntity
entityFilter={zoneAndLocationFilter}
/> />
<paper-input <ha-entity-picker
label="Zone" label='Zone'
name="zone"
value={zone} value={zone}
onChange={this.onChange} onChange={this.zonePicked}
hass={hass}
allowCustomEntity
domainFilter='zone'
/> />
<label id="eventlabel">Event:</label> <label id="eventlabel">Event:</label>
<paper-radio-group <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 Comp = TYPES[condition.condition];
const selected = OPTIONS.indexOf(condition.condition); const selected = OPTIONS.indexOf(condition.condition);
@ -64,6 +64,7 @@ export default class ConditionRow extends Component {
index={index} index={index}
condition={condition} condition={condition}
onChange={onChange} onChange={onChange}
hass={hass}
/> />
</div> </div>
); );

View File

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

View File

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

View File

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

View File

@ -1,30 +1,56 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { onChangeEvent } from '../../util/event.js'; 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 { export default class ZoneCondition extends Component {
constructor() { constructor() {
super(); super();
this.onChange = onChangeEvent.bind(this, 'condition'); 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 */ /* eslint-disable camelcase */
render({ condition }) { render({ condition, hass }) {
const { entity_id, zone } = condition; const { entity_id, zone } = condition;
return ( return (
<div> <div>
<paper-input <ha-entity-picker
label="Entity Id" label='Entity with location'
name="entity_id"
value={entity_id} value={entity_id}
onChange={this.onChange} onChange={this.entityPicked}
hass={hass}
allowCustomEntity
entityFilter={zoneAndLocationFilter}
/> />
<paper-input <ha-entity-picker
label="Zone entity id" label='Zone'
name="zone"
value={zone} value={zone}
onChange={this.onChange} onChange={this.zonePicked}
hass={hass}
allowCustomEntity
domainFilter='zone'
/> />
</div> </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 type = getType(action);
const Comp = type && TYPES[type]; const Comp = type && TYPES[type];
const selected = OPTIONS.indexOf(type); const selected = OPTIONS.indexOf(type);
@ -70,6 +70,7 @@ export default class Action extends Component {
index={index} index={index}
action={action} action={action}
onChange={onChange} onChange={onChange}
hass={hass}
/> />
</div> </div>
); );

View File

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

View File

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

View File

@ -30,7 +30,7 @@ export default class Script extends Component {
this.props.onChange(script); this.props.onChange(script);
} }
render({ script }) { render({ script, hass }) {
return ( return (
<div class="script"> <div class="script">
{script.map((act, idx) => ( {script.map((act, idx) => (
@ -38,6 +38,7 @@ export default class Script extends Component {
index={idx} index={idx}
action={act} action={act}
onChange={this.actionChanged} onChange={this.actionChanged}
hass={hass}
/>))} />))}
<paper-card> <paper-card>
<div class='card-actions add-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/paper-fab/paper-fab.html">
<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.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='../../../src/util/hass-mixins.html'>
<link rel="import" href="../ha-config-section.html"> <link rel="import" href="../ha-config-section.html">
@ -117,6 +120,7 @@ class HaAutomationEditor extends window.hassMixins.EventsMixin(Polymer.Element)
return { return {
hass: { hass: {
type: Object, type: Object,
observer: '_updateComponent',
}, },
narrow: { narrow: {
@ -160,7 +164,7 @@ class HaAutomationEditor extends window.hassMixins.EventsMixin(Polymer.Element)
isWide: { isWide: {
type: Boolean, type: Boolean,
observer: 'isWideChanged', observer: '_updateComponent',
}, },
}; };
} }
@ -169,6 +173,7 @@ class HaAutomationEditor extends window.hassMixins.EventsMixin(Polymer.Element)
super.ready(); super.ready();
this.configChanged = this.configChanged.bind(this); this.configChanged = this.configChanged.bind(this);
this._rendered = null; this._rendered = null;
this._renderScheduled = false;
} }
disconnectedCallback() { disconnectedCallback() {
@ -184,13 +189,13 @@ class HaAutomationEditor extends window.hassMixins.EventsMixin(Polymer.Element)
this.config = config; this.config = config;
this.errors = null; this.errors = null;
this.dirty = true; this.dirty = true;
this._updateComponent(config); this._updateComponent();
} }
automationChanged(newVal, oldVal) { automationChanged(newVal, oldVal) {
if (!newVal) return; if (!newVal) return;
if (!this.hass) { if (!this.hass) {
setTimeout(this.automationChanged.bind(this, newVal, oldVal), 0); setTimeout(() => this.automationChanged(newVal, oldVal), 0);
return; return;
} }
if (oldVal && oldVal.attributes.id === newVal.attributes.id) { if (oldVal && oldVal.attributes.id === newVal.attributes.id) {
@ -230,11 +235,6 @@ class HaAutomationEditor extends window.hassMixins.EventsMixin(Polymer.Element)
this._updateComponent(); this._updateComponent();
} }
isWideChanged() {
if (this.config === null) return;
this._updateComponent();
}
backTapped() { backTapped() {
if (this.dirty && if (this.dirty &&
// eslint-disable-next-line // eslint-disable-next-line
@ -245,11 +245,17 @@ class HaAutomationEditor extends window.hassMixins.EventsMixin(Polymer.Element)
} }
_updateComponent() { _updateComponent() {
if (this._renderScheduled || !this.hass || !this.config) return;
this._renderScheduled = true;
Promise.resolve().then(() => {
this._rendered = window.AutomationEditor(this.$.root, { this._rendered = window.AutomationEditor(this.$.root, {
automation: this.config, automation: this.config,
onChange: this.configChanged, onChange: this.configChanged,
isWide: this.isWide, isWide: this.isWide,
hass: this.hass,
}, this._rendered); }, this._rendered);
this._renderScheduled = false;
});
} }
saveAutomation() { 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-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/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/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'> <link rel='import' href='../../src/resources/ha-style.html'>
<dom-module id='ha-panel-dev-service'> <dom-module id='ha-panel-dev-service'>
@ -28,7 +28,7 @@
.ha-form { .ha-form {
margin-right: 16px; margin-right: 16px;
max-width: 500px; max-width: 400px;
} }
.description { .description {
@ -68,6 +68,14 @@
h1 { h1 {
white-space: normal; white-space: normal;
} }
td {
padding: 4px;
}
.error {
color: var(--google-red-500);
}
</style> </style>
<app-header-layout has-scrolling-region> <app-header-layout has-scrolling-region>
@ -79,60 +87,69 @@
</app-header> </app-header>
<app-localstorage-document <app-localstorage-document
key='panel-dev-service-state-domain' key='panel-dev-service-state-domain-service'
data='{{domain}}'> data='{{domainService}}'>
</app-localstorage-document> </app-localstorage-document>
<app-localstorage-document <app-localstorage-document
key='[[computeServiceKey(domain)]]' key='[[_computeServicedataKey(domainService)]]'
data='{{service}}'>
</app-localstorage-document>
<app-localstorage-document
key='[[computeServicedataKey(domain, service)]]'
data='{{serviceData}}'> data='{{serviceData}}'>
</app-localstorage-document> </app-localstorage-document>
<div class='content'> <div class='content'>
<p> <p>
Call a service from a component. The service dev tool allows you to call any available service in Home Assistant.
</p> </p>
<div class='ha-form'> <div class='ha-form'>
<vaadin-combo-box label='Domain' items='[[computeDomains(serviceDomains)]]' value='{{domain}}'></vaadin-combo-box> <ha-service-picker
<vaadin-combo-box label='Service' items='[[computeServices(serviceDomains, domain)]]' value='{{service}}'></vaadin-combo-box> 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 <paper-textarea
always-float-label always-float-label
label='Service Data (JSON, optional)' label='Service Data (JSON, optional)'
value='{{serviceData}}' value='{{serviceData}}'
></paper-textarea> ></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> </div>
<template is='dom-if' if='[[!domain]]'> <template is='dom-if' if='[[!domainService]]'>
<h1>Select a domain and 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> <h1>Select a service to see the description</h1>
</template> </template>
</template>
<template is='dom-if' if='[[domain]]'> <template is='dom-if' if='[[domainService]]'>
<template is='dom-if' if='[[service]]'>
<template is='dom-if' if='[[!_description]]'> <template is='dom-if' if='[[!_description]]'>
<h1>No description is available</h1> <h1>No description is available</h1>
</template> </template>
<template is='dom-if' if='[[_description]]'> <template is='dom-if' if='[[_description]]'>
<h3>[[_description]]</h3> <h3>[[_description]]</h3>
</template>
<template is='dom-if' if='[[_attributes.length]]'>
<h1>Valid Parameters</h1>
<table class='attributes'> <table class='attributes'>
<tr> <tr>
<th>Parameter</th> <th>Parameter</th>
<th>Description</th> <th>Description</th>
<th>Example</th> <th>Example</th>
</tr> </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'> <template is='dom-repeat' items='[[_attributes]]' as='attribute'>
<tr> <tr>
<td><pre>[[attribute.key]]</pre></td> <td><pre>[[attribute.key]]</pre></td>
@ -143,7 +160,6 @@
</table> </table>
</template> </template>
</template> </template>
</template>
</div> </div>
</app-header-layout> </app-header-layout>
@ -151,7 +167,9 @@
</dom-module> </dom-module>
<script> <script>
class HaPanelDevService extends Polymer.Element { {
const ERROR_SENTINEL = {};
class HaPanelDevService extends Polymer.Element {
static get is() { return 'ha-panel-dev-service'; } static get is() { return 'ha-panel-dev-service'; }
static get properties() { static get properties() {
@ -170,16 +188,19 @@ class HaPanelDevService extends Polymer.Element {
value: false, value: false,
}, },
domain: { domainService: {
type: String, type: String,
value: '', observer: '_domainServiceChanged',
observer: 'domainChanged',
}, },
service: { _domain: {
type: String, type: String,
value: '', computed: '_computeDomain(domainService)',
observer: 'serviceChanged', },
_service: {
type: String,
computed: '_computeService(domainService)',
}, },
serviceData: { serviceData: {
@ -187,92 +208,102 @@ class HaPanelDevService extends Polymer.Element {
value: '', value: '',
}, },
parsedJSON: {
type: Object,
computed: '_computeParsedServiceData(serviceData)'
},
validJSON: {
type: Boolean,
computed: '_computeValidJSON(parsedJSON)',
},
_attributes: { _attributes: {
type: Array, type: Array,
computed: 'computeAttributesArray(serviceDomains, domain, service)', computed: '_computeAttributesArray(hass, _domain, _service)',
}, },
_description: { _description: {
type: String, type: String,
computed: 'computeDescription(serviceDomains, domain, service)', computed: '_computeDescription(hass, _domain, _service)',
},
serviceDomains: {
type: Object,
computed: 'computeServiceDomains(hass)',
}, },
}; };
} }
computeServiceDomains(hass) { _domainServiceChanged() {
return hass.config.services; this.serviceData = '';
} }
computeAttributesArray(serviceDomains, domain, service) { _computeAttributesArray(hass, domain, service) {
if (!serviceDomains) return []; const serviceDomains = hass.config.services;
if (!(domain in serviceDomains)) return []; if (!(domain in serviceDomains)) return [];
if (!(service in serviceDomains[domain])) return []; if (!(service in serviceDomains[domain])) return [];
var fields = serviceDomains[domain][service].fields; const fields = serviceDomains[domain][service].fields;
return Object.keys(fields).map(function (field) { return Object.keys(fields).map(function (field) {
return Object.assign({}, fields[field], { key: field }); return Object.assign({ key: field }, fields[field]);
}); });
} }
computeDescription(serviceDomains, domain, service) { _computeDescription(hass, domain, service) {
if (!serviceDomains) return undefined; const serviceDomains = hass.config.services;
if (!(domain in serviceDomains)) return undefined; if (!(domain in serviceDomains)) return undefined;
if (!(service in serviceDomains[domain])) return undefined; if (!(service in serviceDomains[domain])) return undefined;
return serviceDomains[domain][service].description; return serviceDomains[domain][service].description;
} }
computeDomains(serviceDomains) { _computeServicedataKey(domainService) {
return Object.keys(serviceDomains).sort(); return `panel-dev-service-state-servicedata.${domainService}`;
} }
computeServices(serviceDomains, domain) { _computeDomain(domainService) {
if (!(domain in serviceDomains)) return []; return domainService.split('.', 1)[0];
return Object.keys(serviceDomains[domain]).sort();
} }
computeServiceKey(domain) { _computeService(domainService) {
if (!domain) { return domainService.split('.', 2)[1] || null;
return 'panel-dev-service-state-service';
}
return 'panel-dev-service-state-service.' + domain;
} }
computeServicedataKey(domain, service) { _computeParsedServiceData(serviceData) {
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 { try {
serviceData = this.serviceData ? JSON.parse(this.serviceData) : {}; return serviceData ? JSON.parse(serviceData) : {};
} catch (err) { } catch (err) {
/* eslint-disable no-alert */ return ERROR_SENTINEL;
alert('Error parsing JSON: ' + err); }
/* eslint-enable no-alert */
return;
} }
this.hass.callService(this.domain, this.service, serviceData); _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> </script>

View File

@ -10,7 +10,7 @@
<link rel="import" href="../../bower_components/app-layout/app-toolbar/app-toolbar.html"> <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/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"> <link rel="import" href="../../src/resources/ha-style.html">
<dom-module id="ha-panel-dev-state"> <dom-module id="ha-panel-dev-state">
@ -26,9 +26,9 @@
padding: 16px; padding: 16px;
} }
ha-entity-dropdown { ha-entity-picker, .state-input, paper-textarea {
display: block; display: block;
max-width: 300px; max-width: 400px;
} }
.entities th { .entities th {
@ -72,12 +72,18 @@
This will not communicate with the actual device. This will not communicate with the actual device.
</p> </p>
<ha-entity-dropdown <ha-entity-picker
autofocus autofocus
hass="[[hass]]" hass="[[hass]]"
value="{{_entityId}}" value="{{_entityId}}"
></ha-entity-dropdown> allow-custom-entity
<paper-input label="State" required value='{{_state}}'></paper-input> ></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-textarea label="State attributes (JSON, optional)" value='{{_stateAttributes}}'></paper-textarea>
<paper-button on-tap='handleSetState' raised>Set State</paper-button> <paper-button on-tap='handleSetState' raised>Set State</paper-button>
</div> </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-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/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/ha-menu-button.html'>
<link rel='import' href='../../src/resources/ha-style.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 */ <style is="custom-style">/* remove is= on Polymer 2 */
body { body {
font-size: 14px; font-size: 14px;
height: 100vh;
/* for paper-toggle-button */ /* for paper-toggle-button */
--paper-grey-50: #fafafa; --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));
});
});