Merge pull request #4318 from home-assistant/dev

20191204.0
This commit is contained in:
Bram Kragten 2019-12-04 20:02:41 +01:00 committed by GitHub
commit d7e7798a55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
225 changed files with 7153 additions and 2621 deletions

View File

@ -41,7 +41,32 @@ Provide details about what browser (and version) you are seeing the issue in. An
**Description of problem:**
<!--
Explain what the issue is, and how things should look/behave. If possible provide a screenshot with a description.
Explain what the issue is, and what is the current behaviour. If possible provide a screenshot with a description.
-->
**Expected behaviour:**
<!--
Explain how things should look/behave. If possible provide a screenshot with a description.
-->
**Relevant config:**
<!--
Give the config of both the integration that is used, the Lovelace config, scene, automation or otherwise relevant configuration.
-->
**Steps to reproduce this problem:**
<!--
Sum up all steps that are necesarry to reproduce this bug.
For example:
1. Add a climate integration
2. Navigate to Lovelace
3. Click more info of the climate entity
4. Set the hvac action to heat
5. Set the temperature higher than the current temperature
6. Set the hvac action to cool
-->
**Javascript errors shown in the web inspector (if applicable):**

View File

@ -2,9 +2,9 @@
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
[![Screenshot of the frontend](https://raw.githubusercontent.com/home-assistant/home-assistant-polymer/master/docs/screenshot.png)](https://home-assistant.io/demo/)
[![Screenshot of the frontend](https://raw.githubusercontent.com/home-assistant/home-assistant-polymer/master/docs/screenshot.png)](https://demo.home-assistant.io/)
- [View demo of the Polymer frontend](https://home-assistant.io/demo/)
- [View demo of Home Assistant](https://demo.home-assistant.io/)
- [More information about Home Assistant](https://home-assistant.io)
- [Frontend development instructions](https://developers.home-assistant.io/docs/en/frontend_index.html)
@ -31,3 +31,5 @@ It is possible to compile the project and/or run commands in the development env
## License
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variation of devices.

View File

@ -33,6 +33,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
pragma: "h",
},
],
"@babel/plugin-proposal-optional-chaining",
[
require("@babel/plugin-proposal-decorators").default,
{ decoratorsBeforeExport: true },

View File

@ -175,9 +175,9 @@ export class HcMain extends HassElement {
} catch (err) {
// Generate a Lovelace config.
this._unsubLovelace = () => undefined;
const {
generateLovelaceConfigFromHass,
} = await import("../../../../src/panels/lovelace/common/generate-lovelace-config");
const { generateLovelaceConfigFromHass } = await import(
"../../../../src/panels/lovelace/common/generate-lovelace-config"
);
this._handleNewLovelaceConfig(
await generateLovelaceConfigFromHass(this.hass!)
);

View File

@ -53,7 +53,7 @@ class CardModder extends LitElement {
for (var k in this._config.style) {
if (window.cardTools.hasTemplate(this._config.style[k]))
this.templated.push(k);
this.card.style.setProperty(k, '');
this.card.style.setProperty(k, "");
target.style.setProperty(
k,
window.cardTools.parseTemplate(this._config.style[k])

View File

@ -12,5 +12,7 @@ import "./resources/hademo-icons";
/* polyfill for paper-dropdown */
setTimeout(() => {
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
import(
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
);
}, 1000);

View File

@ -65,74 +65,79 @@ const generateHistory = (state, deltas) => {
const incrementalUnits = ["clients", "queries", "ads"];
export const mockHistory = (mockHass: MockHomeAssistant) => {
mockHass.mockAPI(new RegExp("history/period/.+"), (
hass,
// @ts-ignore
method,
path,
// @ts-ignore
parameters
) => {
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
const entities = params.filter_entity_id.split(",");
mockHass.mockAPI(
new RegExp("history/period/.+"),
(
hass,
// @ts-ignore
method,
path,
// @ts-ignore
parameters
) => {
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
const entities = params.filter_entity_id.split(",");
const results: HassEntity[][] = [];
const results: HassEntity[][] = [];
for (const entityId of entities) {
const state = hass.states[entityId];
for (const entityId of entities) {
const state = hass.states[entityId];
if (!state) {
continue;
}
if (!state) {
continue;
}
if (!state.attributes.unit_of_measurement) {
results.push(generateHistory(state, [state.state]));
continue;
}
if (!state.attributes.unit_of_measurement) {
results.push(generateHistory(state, [state.state]));
continue;
}
const numberState = Number(state.state);
const numberState = Number(state.state);
if (isNaN(numberState)) {
// tslint:disable-next-line
console.log(
"Ignoring state with unparsable state but with a unit",
entityId,
state
if (isNaN(numberState)) {
// tslint:disable-next-line
console.log(
"Ignoring state with unparsable state but with a unit",
entityId,
state
);
continue;
}
const statesToGenerate = 15;
let genFunc;
if (incrementalUnits.includes(state.attributes.unit_of_measurement)) {
let initial = Math.floor(
numberState * 0.4 + numberState * Math.random() * 0.2
);
const diff = Math.max(
1,
Math.floor((numberState - initial) / statesToGenerate)
);
genFunc = () => {
initial += diff;
return Math.min(numberState, initial);
};
} else {
const diff = Math.floor(
numberState * (numberState > 80 ? 0.05 : 0.5)
);
genFunc = () =>
numberState - diff + Math.floor(Math.random() * 2 * diff);
}
results.push(
generateHistory(
{
entity_id: state.entity_id,
attributes: state.attributes,
},
Array.from({ length: statesToGenerate }, genFunc)
)
);
continue;
}
const statesToGenerate = 15;
let genFunc;
if (incrementalUnits.includes(state.attributes.unit_of_measurement)) {
let initial = Math.floor(
numberState * 0.4 + numberState * Math.random() * 0.2
);
const diff = Math.max(
1,
Math.floor((numberState - initial) / statesToGenerate)
);
genFunc = () => {
initial += diff;
return Math.min(numberState, initial);
};
} else {
const diff = Math.floor(numberState * (numberState > 80 ? 0.05 : 0.5));
genFunc = () =>
numberState - diff + Math.floor(Math.random() * 2 * diff);
}
results.push(
generateHistory(
{
entity_id: state.entity_id,
attributes: state.attributes,
},
Array.from({ length: statesToGenerate }, genFunc)
)
);
return results;
}
return results;
});
);
};

View File

@ -12,9 +12,10 @@ export const mockLovelace = (
localizePromise: Promise<LocalizeFunc>
) => {
hass.mockWS("lovelace/config", () =>
Promise.all([selectedDemoConfig, localizePromise]).then(
([config, localize]) => config.lovelace(localize)
)
Promise.all([
selectedDemoConfig,
localizePromise,
]).then(([config, localize]) => config.lovelace(localize))
);
hass.mockWS("lovelace/config/save", () => Promise.resolve());

View File

@ -44,9 +44,7 @@ class HassioAddonAudio extends EventsMixin(PolymerElement) {
selected="{{selectedInput}}"
>
<template is="dom-repeat" items="[[inputDevices]]">
<paper-item device\$="[[item.device]]"
>[[item.name]]</paper-item
>
<paper-item device$="[[item.device]]">[[item.name]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
@ -57,9 +55,7 @@ class HassioAddonAudio extends EventsMixin(PolymerElement) {
selected="{{selectedOutput}}"
>
<template is="dom-repeat" items="[[outputDevices]]">
<paper-item device\$="[[item.device]]"
>[[item.name]]</paper-item
>
<paper-item device$="[[item.device]]">[[item.name]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>

View File

@ -569,7 +569,10 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
openChangelog() {
this.hass
.callApi("get", `hassio/addons/${this.addonSlug}/changelog`)
.then((resp) => resp, () => "Error getting changelog")
.then(
(resp) => resp,
() => "Error getting changelog"
)
.then((content) => {
showHassioMarkdownDialog(this, {
title: "Changelog",

View File

@ -74,9 +74,7 @@ export class HassioUpdate extends LitElement {
this.supervisorInfo.version,
this.supervisorInfo.last_version,
"hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${
this.supervisorInfo.last_version
}`
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.last_version}`
)}
${this.hassOsInfo
? this._renderUpdateCard(
@ -84,9 +82,7 @@ export class HassioUpdate extends LitElement {
this.hassOsInfo.version,
this.hassOsInfo.version_latest,
"hassio/hassos/update",
`https://github.com//home-assistant/hassos/releases/tag/${
this.hassOsInfo.version_latest
}`
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}`
)
: ""}
</div>

View File

@ -12,7 +12,9 @@ export const showHassioMarkdownDialog = (
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-markdown",
dialogImport: () =>
import(/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"),
import(
/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"
),
dialogParams,
});
};

View File

@ -12,7 +12,9 @@ export const showHassioSnapshotDialog = (
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-snapshot",
dialogImport: () =>
import(/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"),
import(
/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"
),
dialogParams,
});
};

View File

@ -56,12 +56,16 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
addon: {
tag: "hassio-addon-view",
load: () =>
import(/* webpackChunkName: "hassio-addon-view" */ "./addon-view/hassio-addon-view"),
import(
/* webpackChunkName: "hassio-addon-view" */ "./addon-view/hassio-addon-view"
),
},
ingress: {
tag: "hassio-ingress-view",
load: () =>
import(/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"),
import(
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
),
},
},
};

View File

@ -19,13 +19,14 @@
"dependencies": {
"@material/chips": "^3.2.0",
"@material/data-table": "^3.2.0",
"@material/mwc-base": "^0.8.0",
"@material/mwc-button": "^0.8.0",
"@material/mwc-checkbox": "^0.8.0",
"@material/mwc-fab": "^0.8.0",
"@material/mwc-ripple": "^0.8.0",
"@material/mwc-switch": "^0.8.0",
"@mdi/svg": "4.5.95",
"@material/mwc-base": "^0.10.0",
"@material/mwc-button": "^0.10.0",
"@material/mwc-checkbox": "^0.10.0",
"@material/mwc-dialog": "^0.10.0",
"@material/mwc-fab": "^0.10.0",
"@material/mwc-ripple": "^0.10.0",
"@material/mwc-switch": "^0.10.0",
"@mdi/svg": "4.6.95",
"@polymer/app-layout": "^3.0.2",
"@polymer/app-localize-behavior": "^3.0.1",
"@polymer/app-route": "^3.0.2",
@ -68,8 +69,8 @@
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.3.7",
"@vaadin/vaadin-combo-box": "^4.2.8",
"@vaadin/vaadin-date-picker": "^3.3.3",
"@vaadin/vaadin-combo-box": "^5.0.6",
"@vaadin/vaadin-date-picker": "^4.0.3",
"@webcomponents/shadycss": "^1.9.0",
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0",
@ -98,21 +99,23 @@
"regenerator-runtime": "^0.13.2",
"roboto-fontface": "^0.10.0",
"superstruct": "^0.6.1",
"copy-to-clipboard": "^1.0.9",
"tslib": "^1.10.0",
"unfetch": "^4.1.0",
"web-animations-js": "^2.3.1",
"xss": "^1.0.6"
},
"devDependencies": {
"@babel/core": "^7.4.0",
"@babel/plugin-external-helpers": "^7.2.0",
"@babel/plugin-proposal-class-properties": "^7.4.0",
"@babel/plugin-proposal-decorators": "^7.4.0",
"@babel/plugin-proposal-object-rest-spread": "^7.4.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-react-jsx": "^7.3.0",
"@babel/preset-env": "^7.4.2",
"@babel/preset-typescript": "^7.4.0",
"@babel/core": "^7.7.4",
"@babel/plugin-external-helpers": "^7.7.4",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-proposal-decorators": "^7.7.4",
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
"@babel/plugin-proposal-optional-chaining": "^7.7.4",
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/plugin-transform-react-jsx": "^7.7.4",
"@babel/preset-env": "^7.7.4",
"@babel/preset-typescript": "^7.7.4",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12",
"@types/chromecast-caf-sender": "^1.0.1",
@ -154,18 +157,18 @@
"merge-stream": "^1.0.1",
"mocha": "^6.0.2",
"parse5": "^5.1.0",
"prettier": "^1.16.4",
"prettier": "^1.19.1",
"raw-loader": "^2.0.0",
"reify": "^0.18.1",
"require-dir": "^1.2.0",
"sinon": "^7.3.1",
"terser-webpack-plugin": "^1.2.3",
"ts-mocha": "^6.0.0",
"tslint": "^5.14.0",
"tslint": "^5.20.1",
"tslint-config-prettier": "^1.18.0",
"tslint-eslint-rules": "^5.4.0",
"tslint-plugin-prettier": "^2.0.1",
"typescript": "^3.6.3",
"typescript": "^3.7.2",
"web-component-tester": "^6.9.2",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.9",
@ -178,7 +181,6 @@
"_comment_2": "Fix in https://github.com/Polymer/polymer/pull/5569",
"resolutions": {
"@webcomponents/webcomponentsjs": "^2.2.10",
"@vaadin/vaadin-lumo-styles": "^1.4.2",
"@polymer/polymer": "3.1.0",
"lit-html": "^1.1.2"
},

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20191119.6",
version="20191204.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",

View File

@ -98,9 +98,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
<ha-markdown
allowsvg
.content=${this.localize(
`ui.panel.page-authorize.form.providers.${
step.handler[0]
}.abort.${step.reason}`
`ui.panel.page-authorize.form.providers.${step.handler[0]}.abort.${step.reason}`
)}
></ha-markdown>
`;
@ -229,9 +227,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
}
private _computeStepDescription(step: DataEntryFlowStepForm) {
const resourceKey = `ui.panel.page-authorize.form.providers.${
step.handler[0]
}.step.${step.step_id}.description`;
const resourceKey = `ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${step.step_id}.description`;
const args: string[] = [];
const placeholders = step.description_placeholders || {};
Object.keys(placeholders).forEach((key) => {
@ -245,9 +241,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
this.localize(
`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${
step.step_id
}.data.${schema.name}`
`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${step.step_id}.data.${schema.name}`
);
}
@ -255,9 +249,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
// Returns a callback for ha-form to calculate error messages
return (error) =>
this.localize(
`ui.panel.page-authorize.form.providers.${
step.handler[0]
}.error.${error}`
`ui.panel.page-authorize.form.providers.${step.handler[0]}.error.${error}`
);
}

View File

@ -11,7 +11,9 @@ import "./ha-auth-flow";
import { AuthProvider, fetchAuthProviders } from "../data/auth";
import { registerServiceWorker } from "../util/register-service-worker";
import(/* webpackChunkName: "pick-auth-provider" */ "../auth/ha-pick-auth-provider");
import(
/* webpackChunkName: "pick-auth-provider" */ "../auth/ha-pick-auth-provider"
);
interface QueryParams {
client_id?: string;

View File

@ -10,11 +10,11 @@ function toLocaleDateStringSupportsOptions() {
return false;
}
export default (toLocaleDateStringSupportsOptions()
export default toLocaleDateStringSupportsOptions()
? (dateObj: Date, locales: string) =>
dateObj.toLocaleDateString(locales, {
year: "numeric",
month: "long",
day: "numeric",
})
: (dateObj: Date) => fecha.format(dateObj, "mediumDate"));
: (dateObj: Date) => fecha.format(dateObj, "mediumDate");

View File

@ -10,7 +10,7 @@ function toLocaleStringSupportsOptions() {
return false;
}
export default (toLocaleStringSupportsOptions()
export default toLocaleStringSupportsOptions()
? (dateObj: Date, locales: string) =>
dateObj.toLocaleString(locales, {
year: "numeric",
@ -19,4 +19,4 @@ export default (toLocaleStringSupportsOptions()
hour: "numeric",
minute: "2-digit",
})
: (dateObj: Date) => fecha.format(dateObj, "haDateTime"));
: (dateObj: Date) => fecha.format(dateObj, "haDateTime");

View File

@ -10,10 +10,10 @@ function toLocaleTimeStringSupportsOptions() {
return false;
}
export default (toLocaleTimeStringSupportsOptions()
export default toLocaleTimeStringSupportsOptions()
? (dateObj: Date, locales: string) =>
dateObj.toLocaleTimeString(locales, {
hour: "numeric",
minute: "2-digit",
})
: (dateObj: Date) => fecha.format(dateObj, "shortTime"));
: (dateObj: Date) => fecha.format(dateObj, "shortTime");

View File

@ -60,7 +60,7 @@ export const applyThemesOnElement = (
element.updateStyles(styles);
} else if (window.ShadyCSS) {
// implement updateStyles() method of Polymer elements
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ (element), styles);
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles);
}
if (!updateMeta) {

View File

@ -0,0 +1,29 @@
import { directive, Part, NodePart } from "lit-html";
export const dynamicContentDirective = directive(
(tag: string, properties: { [key: string]: any }) => (part: Part): void => {
if (!(part instanceof NodePart)) {
throw new Error(
"dynamicContentDirective can only be used in content bindings"
);
}
let element = part.value as HTMLElement | undefined;
if (
element !== undefined &&
tag.toUpperCase() === (element as HTMLElement).tagName
) {
Object.entries(properties).forEach(([key, value]) => {
element![key] = value;
});
return;
}
element = document.createElement(tag);
Object.entries(properties).forEach(([key, value]) => {
element![key] = value;
});
part.setValue(element);
}
);

View File

@ -11,7 +11,9 @@ export const setupLeafletMap = async (
throw new Error("Cannot setup Leaflet map on disconnected element");
}
// tslint:disable-next-line
const Leaflet = (await import(/* webpackChunkName: "leaflet" */ "leaflet")) as LeafletModuleType;
const Leaflet = (await import(
/* webpackChunkName: "leaflet" */ "leaflet"
)) as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
const map = Leaflet.map(mapElement);

View File

@ -7,8 +7,11 @@
// export function onChangeEvent(this: OnChangeComponent, prop, ev) {
export function onChangeEvent(this: any, prop, ev) {
const origData = this.props[prop];
if (!this.initialized) {
return;
}
const origData = this.props[prop];
if (ev.target.value === origData[ev.target.name]) {
return;
}

View File

@ -6,8 +6,9 @@ import {
MDCDataTableFoundation,
} from "@material/data-table";
import { classMap } from "lit-html/directives/class-map";
import {
BaseElement,
html,
query,
queryAll,
@ -15,10 +16,11 @@ import {
css,
customElement,
property,
classMap,
TemplateResult,
PropertyValues,
} from "@material/mwc-base/base-element";
} from "lit-element";
import { BaseElement } from "@material/mwc-base/base-element";
// eslint-disable-next-line import/no-webpack-loader-syntax
// @ts-ignore

View File

@ -0,0 +1,412 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import "@polymer/paper-listbox/paper-listbox";
import memoizeOne from "memoize-one";
import {
LitElement,
TemplateResult,
html,
css,
CSSResult,
customElement,
property,
PropertyValues,
} from "lit-element";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import "./ha-devices-picker";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { compare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { computeDomain } from "../../common/entity/compute_domain";
interface DevicesByArea {
[areaId: string]: AreaDevices;
}
interface AreaDevices {
id?: string;
name: string;
devices: string[];
}
const rowRenderer = (
root: HTMLElement,
_owner,
model: { item: AreaDevices }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item {
width: 100%;
margin: -10px 0;
padding: 0;
}
paper-icon-button {
float: right;
}
.devices {
display: none;
}
.devices.visible {
display: block;
}
</style>
<paper-item>
<paper-item-body two-line="">
<div class='name'>[[item.name]]</div>
<div secondary>[[item.devices.length]] devices</div>
</paper-item-body>
</paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name!;
root.querySelector(
"[secondary]"
)!.textContent = `${model.item.devices.length.toString()} devices`;
};
@customElement("ha-area-devices-picker")
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public area?: string;
@property() public devices?: string[];
/**
* Show only devices with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no devices with entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only deviced with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property({ type: Boolean })
private _opened?: boolean;
@property() private _areaPicker = true;
@property() private _devices?: DeviceRegistryEntry[];
@property() private _areas?: AreaRegistryEntry[];
@property() private _entities?: EntityRegistryEntry[];
private _selectedDevices: string[] = [];
private _filteredDevices: DeviceRegistryEntry[] = [];
private _getDevices = memoizeOne(
(
devices: DeviceRegistryEntry[],
areas: AreaRegistryEntry[],
entities: EntityRegistryEntry[],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"]
): AreaDevices[] => {
if (!devices.length) {
return [];
}
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
let inputDevices = [...devices];
if (includeDomains) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
}
if (excludeDomains) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
});
}
if (includeDeviceClasses) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
}
this._filteredDevices = inputDevices;
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of areas) {
areaLookup[area.area_id] = area;
}
const devicesByArea: DevicesByArea = {};
for (const device of inputDevices) {
const areaId = device.area_id;
if (areaId) {
if (!(areaId in devicesByArea)) {
devicesByArea[areaId] = {
id: areaId,
name: areaLookup[areaId].name,
devices: [],
};
}
devicesByArea[areaId].devices.push(device.id);
}
}
const sorted = Object.keys(devicesByArea)
.sort((a, b) =>
compare(devicesByArea[a].name || "", devicesByArea[b].name || "")
)
.map((key) => devicesByArea[key]);
return sorted;
}
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._devices = devices;
}),
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this._areas = areas;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities;
}),
];
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("area") && this.area) {
this._areaPicker = true;
this.value = this.area;
} else if (changedProps.has("devices") && this.devices) {
this._areaPicker = false;
const filteredDeviceIds = this._filteredDevices.map(
(device) => device.id
);
const selectedDevices = this.devices.filter((device) =>
filteredDeviceIds.includes(device)
);
this._setValue(selectedDevices);
}
}
protected render(): TemplateResult | void {
if (!this._devices || !this._areas || !this._entities) {
return;
}
const areas = this._getDevices(
this._devices,
this._areas,
this._entities,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses
);
if (!this._areaPicker || areas.length === 0) {
return html`
<ha-devices-picker
@value-changed=${this._devicesPicked}
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.value=${this._selectedDevices}
.pickDeviceLabel=${`Add ${this.label} device`}
.pickedDeviceLabel=${`${this.label} device`}
></ha-devices-picker>
${areas.length > 0
? html`
<mwc-button @click=${this._switchPicker}
>Choose an area</mwc-button
>
`
: ""}
`;
}
return html`
<vaadin-combo-box-light
item-value-path="id"
item-id-path="id"
item-label-path="name"
.items=${areas}
.value=${this._value}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._areaPicked}
>
<paper-input
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.device-picker.device")
: `${this.label} in area`}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${this.value
? html`
<paper-icon-button
aria-label=${this.hass.localize(
"ui.components.device-picker.clear"
)}
slot="suffix"
class="clear-button"
icon="hass:close"
@click=${this._clearValue}
no-ripple
>
Clear
</paper-icon-button>
`
: ""}
${areas.length > 0
? html`
<paper-icon-button
aria-label=${this.hass.localize(
"ui.components.device-picker.show_devices"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</paper-icon-button>
`
: ""}
</paper-input>
</vaadin-combo-box-light>
<mwc-button @click=${this._switchPicker}
>Choose individual devices</mwc-button
>
`;
}
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue([]);
}
private get _value() {
return this.value || [];
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private async _switchPicker() {
this._areaPicker = !this._areaPicker;
}
private async _areaPicked(ev: PolymerChangedEvent<string>) {
const value = ev.detail.value;
let selectedDevices = [];
const target = ev.target as any;
if (target.selectedItem) {
selectedDevices = target.selectedItem.devices;
}
if (value !== this._value || this._selectedDevices !== selectedDevices) {
this._setValue(selectedDevices, value);
}
}
private _devicesPicked(ev: CustomEvent) {
ev.stopPropagation();
const selectedDevices = ev.detail.value;
this._setValue(selectedDevices);
}
private _setValue(selectedDevices: string[], value = "") {
this.value = value;
this._selectedDevices = selectedDevices;
setTimeout(() => {
fireEvent(this, "value-changed", { value: selectedDevices });
fireEvent(this, "change");
}, 0);
}
static get styles(): CSSResult {
return css`
paper-input > paper-icon-button {
width: 24px;
height: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-area-devices-picker": HaAreaDevicesPicker;
}
}

View File

@ -176,6 +176,7 @@ export abstract class HaDeviceAutomationPicker<
this.value = automation;
setTimeout(() => {
fireEvent(this, "change");
fireEvent(this, "value-changed", { value: automation });
}, 0);
}

View File

@ -1,7 +1,7 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import "@polymer/paper-listbox/paper-listbox";
import memoizeOne from "memoize-one";
import {

View File

@ -0,0 +1,127 @@
import {
LitElement,
TemplateResult,
property,
html,
customElement,
} from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button-light";
import { HomeAssistant } from "../../types";
import { PolymerChangedEvent } from "../../polymer-types";
import { fireEvent } from "../../common/dom/fire_event";
import "./ha-device-picker";
@customElement("ha-devices-picker")
class HaDevicesPicker extends LitElement {
@property() public hass?: HomeAssistant;
@property() public value?: string[];
/**
* Show entities from specific domains.
* @type {string}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
@property({ attribute: "picked-device-label" })
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
public pickedDeviceLabel?: string;
@property({ attribute: "pick-device-label" }) public pickDeviceLabel?: string;
protected render(): TemplateResult | void {
if (!this.hass) {
return;
}
const currentDevices = this._currentDevices;
return html`
${currentDevices.map(
(entityId) => html`
<div>
<ha-device-picker
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.value=${entityId}
.label=${this.pickedDeviceLabel}
@value-changed=${this._deviceChanged}
></ha-device-picker>
</div>
`
)}
<div>
<ha-device-picker
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.label=${this.pickDeviceLabel}
@value-changed=${this._addDevice}
></ha-device-picker>
</div>
`;
}
private get _currentDevices() {
return this.value || [];
}
private async _updateDevices(devices) {
fireEvent(this, "value-changed", {
value: devices,
});
this.value = devices;
}
private _deviceChanged(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const curValue = (event.currentTarget as any).curValue;
const newValue = event.detail.value;
if (newValue === curValue || newValue !== "") {
return;
}
if (newValue === "") {
this._updateDevices(
this._currentDevices.filter((dev) => dev !== curValue)
);
} else {
this._updateDevices(
this._currentDevices.map((dev) => (dev === curValue ? newValue : dev))
);
}
}
private async _addDevice(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const toAdd = event.detail.value;
(event.currentTarget as any).value = "";
if (!toAdd) {
return;
}
const currentDevices = this._currentDevices;
if (currentDevices.includes(toAdd)) {
return;
}
this._updateDevices([...currentDevices, toAdd]);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-devices-picker": HaDevicesPicker;
}
}

View File

@ -215,7 +215,9 @@ class HaChartBase extends mixinBehaviors(
}
if (scriptsLoaded === null) {
scriptsLoaded = import(/* webpackChunkName: "load_chart" */ "../../resources/ha-chart-scripts.js");
scriptsLoaded = import(
/* webpackChunkName: "load_chart" */ "../../resources/ha-chart-scripts.js"
);
}
scriptsLoaded.then((ChartModule) => {
this.ChartClass = ChartModule.default;

View File

@ -2,7 +2,7 @@ import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import memoizeOne from "memoize-one";
import "./state-badge";

View File

@ -122,8 +122,9 @@ class HaCameraStream extends LitElement {
private async _startHls(): Promise<void> {
// tslint:disable-next-line
const Hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
.default as HLSModule;
const Hls = ((await import(
/* webpackChunkName: "hls.js" */ "hls.js"
)) as any).default as HLSModule;
let hlsSupported = Hls.isSupported();
const videoEl = this._videoEl;

View File

@ -72,9 +72,7 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
computeCurrentStatus(hass, stateObj) {
if (!hass || !stateObj) return null;
if (stateObj.attributes.current_temperature != null) {
return `${stateObj.attributes.current_temperature} ${
hass.config.unit_system.temperature
}`;
return `${stateObj.attributes.current_temperature} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.current_humidity != null) {
return `${stateObj.attributes.current_humidity} %`;
@ -89,22 +87,16 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
stateObj.attributes.target_temp_low != null &&
stateObj.attributes.target_temp_high != null
) {
return `${stateObj.attributes.target_temp_low}-${
stateObj.attributes.target_temp_high
} ${hass.config.unit_system.temperature}`;
return `${stateObj.attributes.target_temp_low}-${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.temperature != null) {
return `${stateObj.attributes.temperature} ${
hass.config.unit_system.temperature
}`;
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
}
if (
stateObj.attributes.target_humidity_low != null &&
stateObj.attributes.target_humidity_high != null
) {
return `${stateObj.attributes.target_humidity_low}-${
stateObj.attributes.target_humidity_high
}%`;
return `${stateObj.attributes.target_humidity_low}-${stateObj.attributes.target_humidity_high}%`;
}
if (stateObj.attributes.humidity != null) {
return `${stateObj.attributes.humidity} %`;
@ -121,9 +113,7 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
const stateString = localize(`state.climate.${stateObj.state}`);
return stateObj.attributes.hvac_action
? `${localize(
`state_attributes.climate.hvac_action.${
stateObj.attributes.hvac_action
}`
`state_attributes.climate.hvac_action.${stateObj.attributes.hvac_action}`
)} (${stateString})`
: stateString;
}

View File

@ -3,7 +3,7 @@ import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { EventsMixin } from "../mixins/events-mixin";

View File

@ -1,4 +1,5 @@
import { classMap, html, customElement } from "@material/mwc-base/base-element";
import { classMap } from "lit-html/directives/class-map";
import { html, customElement } from "lit-element";
import { ripple } from "@material/mwc-ripple/ripple-directive.js";
import "@material/mwc-fab";

View File

@ -58,14 +58,10 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
stateObj.attributes.target_temp_low != null &&
stateObj.attributes.target_temp_high != null
) {
return `${stateObj.attributes.target_temp_low} - ${
stateObj.attributes.target_temp_high
} ${hass.config.unit_system.temperature}`;
return `${stateObj.attributes.target_temp_low} - ${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.temperature != null) {
return `${stateObj.attributes.temperature} ${
hass.config.unit_system.temperature
}`;
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
}
return "";

View File

@ -0,0 +1,81 @@
import { safeDump, safeLoad } from "js-yaml";
import "./ha-code-editor";
import { LitElement, property, customElement, html } from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
const isEmpty = (obj: object) => {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
return false;
}
}
return true;
};
@customElement("ha-yaml-editor")
export class HaYamlEditor extends LitElement {
@property() public value?: any;
@property() public isValid = true;
@property() public label?: string;
@property() private _yaml?: string;
protected firstUpdated() {
try {
this._yaml =
this.value && !isEmpty(this.value) ? safeDump(this.value) : "";
} catch (err) {
alert(`There was an error converting to YAML: ${err}`);
}
}
protected render() {
if (this._yaml === undefined) {
return;
}
return html`
${this.label
? html`
<p>${this.label}</p>
`
: ""}
<ha-code-editor
.value=${this._yaml}
mode="yaml"
.error=${this.isValid === false}
@value-changed=${this._onChange}
></ha-code-editor>
`;
}
private _onChange(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value;
let parsed;
let isValid = true;
if (value) {
try {
parsed = safeLoad(value);
isValid = true;
} catch (err) {
// Invalid YAML
isValid = false;
}
} else {
parsed = {};
}
this.value = parsed;
this.isValid = isValid;
if (isValid) {
fireEvent(this, "value-changed", { value: parsed });
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-yaml-editor": HaYamlEditor;
}
}

View File

@ -19,9 +19,7 @@ export interface Stream {
}
export const computeMJPEGStreamUrl = (entity: CameraEntity) =>
`/api/camera_proxy_stream/${entity.entity_id}?token=${
entity.attributes.access_token
}`;
`/api/camera_proxy_stream/${entity.entity_id}?token=${entity.attributes.access_token}`;
export const fetchThumbnailUrlWithCache = (
hass: HomeAssistant,

View File

@ -3,7 +3,7 @@ import { HomeAssistant } from "../types";
interface ProcessResults {
card: { [key: string]: { [key: string]: string } };
speech: {
[SpeechType in "plain" | "ssml"]: { extra_data: any; speech: string }
[SpeechType in "plain" | "ssml"]: { extra_data: any; speech: string };
};
}

View File

@ -18,7 +18,7 @@ export interface DeviceCondition extends DeviceAutomation {
}
export interface DeviceTrigger extends DeviceAutomation {
platform: string;
platform: "device";
}
export const fetchDeviceActions = (hass: HomeAssistant, deviceId: string) =>
@ -107,9 +107,7 @@ export const localizeDeviceAutomationAction = (
state ? computeStateName(state) : "<unknown>",
"subtype",
hass.localize(
`component.${action.domain}.device_automation.action_subtype.${
action.subtype
}`
`component.${action.domain}.device_automation.action_subtype.${action.subtype}`
)
);
};
@ -122,16 +120,12 @@ export const localizeDeviceAutomationCondition = (
? hass.states[condition.entity_id]
: undefined;
return hass.localize(
`component.${condition.domain}.device_automation.condition_type.${
condition.type
}`,
`component.${condition.domain}.device_automation.condition_type.${condition.type}`,
"entity_name",
state ? computeStateName(state) : "<unknown>",
"subtype",
hass.localize(
`component.${condition.domain}.device_automation.condition_subtype.${
condition.subtype
}`
`component.${condition.domain}.device_automation.condition_subtype.${condition.subtype}`
)
);
};
@ -142,16 +136,12 @@ export const localizeDeviceAutomationTrigger = (
) => {
const state = trigger.entity_id ? hass.states[trigger.entity_id] : undefined;
return hass.localize(
`component.${trigger.domain}.device_automation.trigger_type.${
trigger.type
}`,
`component.${trigger.domain}.device_automation.trigger_type.${trigger.type}`,
"entity_name",
state ? computeStateName(state) : "<unknown>",
"subtype",
hass.localize(
`component.${trigger.domain}.device_automation.trigger_subtype.${
trigger.subtype
}`
`component.${trigger.domain}.device_automation.trigger_subtype.${trigger.subtype}`
)
);
};

View File

@ -149,9 +149,7 @@ export const createHassioSession = async (hass: HomeAssistant) => {
"POST",
"hassio/ingress/session"
);
document.cookie = `ingress_session=${
response.data.session
};path=/api/hassio_ingress/`;
document.cookie = `ingress_session=${response.data.session};path=/api/hassio_ingress/`;
};
export const reloadHassioAddons = (hass: HomeAssistant) =>

View File

@ -18,38 +18,6 @@ export const SCENE_IGNORED_DOMAINS = [
"zone",
];
export const SCENE_SAVED_ATTRIBUTES = {
light: [
"brightness",
"color_temp",
"effect",
"rgb_color",
"xy_color",
"hs_color",
],
media_player: [
"is_volume_muted",
"volume_level",
"sound_mode",
"source",
"media_content_id",
"media_content_type",
],
climate: [
"target_temperature",
"target_temperature_high",
"target_temperature_low",
"target_humidity",
"fan_mode",
"swing_mode",
"hvac_mode",
"preset_mode",
],
vacuum: ["cleaning_mode"],
fan: ["speed", "current_direction"],
water_heather: ["temperature", "operation_mode"],
};
export interface SceneEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & { id?: string };
}

View File

@ -98,9 +98,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
"ui.dialogs.config_entry_system_options.enable_new_entities_description",
"integration",
this.hass.localize(
`component.${
this._params.entry.domain
}.config.title`
`component.${this._params.entry.domain}.config.title`
) || this._params.entry.domain
)}
</p>

View File

@ -10,7 +10,9 @@ export interface ConfigEntrySystemOptionsDialogParams {
}
export const loadConfigEntrySystemOptionsDialog = () =>
import(/* webpackChunkName: "config-entry-system-options" */ "./dialog-config-entry-system-options");
import(
/* webpackChunkName: "config-entry-system-options" */ "./dialog-config-entry-system-options"
);
export const showConfigEntrySystemOptionsDialog = (
element: HTMLElement,

View File

@ -71,9 +71,7 @@ export const showConfigFlowDialog = (
renderShowFormStepFieldLabel(hass, step, field) {
return hass.localize(
`component.${step.handler}.config.step.${step.step_id}.data.${
field.name
}`
`component.${step.handler}.config.step.${step.step_id}.data.${field.name}`
);
},

View File

@ -79,7 +79,9 @@ export interface DataEntryFlowDialogParams {
}
export const loadDataEntryFlowDialog = () =>
import(/* webpackChunkName: "dialog-config-flow" */ "./dialog-data-entry-flow");
import(
/* webpackChunkName: "dialog-config-flow" */ "./dialog-data-entry-flow"
);
export const showFlowDialog = (
element: HTMLElement,

View File

@ -54,9 +54,7 @@ export const showOptionsFlowDialog = (
renderShowFormStepFieldLabel(hass, step, field) {
return hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.data.${
field.name
}`
`component.${configEntry.domain}.options.step.${step.step_id}.data.${field.name}`
);
},

View File

@ -12,7 +12,9 @@ export interface DeviceRegistryDetailDialogParams {
}
export const loadDeviceRegistryDetailDialog = () =>
import(/* webpackChunkName: "device-registry-detail-dialog" */ "./dialog-device-registry-detail");
import(
/* webpackChunkName: "device-registry-detail-dialog" */ "./dialog-device-registry-detail"
);
export const showDeviceRegistryDetailDialog = (
element: HTMLElement,

View File

@ -6,7 +6,9 @@ export interface HaDomainTogglerDialogParams {
}
export const loadDomainTogglerDialog = () =>
import(/* webpackChunkName: "dialog-domain-toggler" */ "./dialog-domain-toggler");
import(
/* webpackChunkName: "dialog-domain-toggler" */ "./dialog-domain-toggler"
);
export const showDomainTogglerDialog = (
element: HTMLElement,

View File

@ -100,7 +100,7 @@ class MoreInfoClimate extends LitElement {
</div>
`
: ""}
${stateObj.attributes.temperature
${stateObj.attributes.temperature !== undefined
? html`
<ha-climate-control
.value=${stateObj.attributes.temperature}
@ -112,8 +112,8 @@ class MoreInfoClimate extends LitElement {
></ha-climate-control>
`
: ""}
${stateObj.attributes.target_temp_low ||
stateObj.attributes.target_temp_high
${stateObj.attributes.target_temp_low !== undefined ||
stateObj.attributes.target_temp_high !== undefined
? html`
<ha-climate-control
.value=${stateObj.attributes.target_temp_low}

View File

@ -2,7 +2,7 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@vaadin/vaadin-date-picker/vaadin-date-picker";
import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker";
import "../../../components/ha-relative-time";
import "../../../components/paper-time-input";

View File

@ -16,13 +16,14 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
import { EventsMixin } from "../../mixins/events-mixin";
import LocalizeMixin from "../../mixins/localize-mixin";
import { computeRTL } from "../../common/util/compute_rtl";
const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"];
/*
* @appliesMixin EventsMixin
*/
class MoreInfoControls extends EventsMixin(PolymerElement) {
class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="ha-style-dialog">
@ -68,7 +69,7 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
<app-toolbar>
<paper-icon-button
aria-label="Dismiss dialog"
aria-label$="[[localize('ui.dialogs.more_info_control.dismiss')]]"
icon="hass:close"
dialog-dismiss
></paper-icon-button>
@ -77,6 +78,7 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
</div>
<template is="dom-if" if="[[canConfigure]]">
<paper-icon-button
aria-label$="[[localize('ui.dialogs.more_info_control.settings')]]"
icon="hass:settings"
on-click="_gotoSettings"
></paper-icon-button>

View File

@ -47,6 +47,7 @@ class MoreInfoSettings extends LocalizeMixin(EventsMixin(PolymerElement)) {
<app-toolbar>
<ha-paper-icon-button-arrow-prev
aria-label$="[[localize('ui.dialogs.more_info_settings.back')]]"
on-click="_backTapped"
></ha-paper-icon-button-arrow-prev>
<div main-title="">[[_computeStateName(stateObj)]]</div>

View File

@ -49,10 +49,10 @@ export class HuiNotificationDrawer extends EventsMixin(
text-align: center;
}
</style>
<app-drawer id='drawer' opened="{{open}}" disable-swipe align="start">
<app-drawer id="drawer" opened="{{open}}" disable-swipe align="start">
<app-toolbar>
<div main-title>[[localize('ui.notification_drawer.title')]]</div>
<ha-paper-icon-button-prev on-click="_closeDrawer"></paper-icon-button>
<ha-paper-icon-button-prev on-click="_closeDrawer" aria-label$="[[localize('ui.notification_drawer.close')]]"></paper-icon-button>
</app-toolbar>
<div class="notifications">
<template is="dom-if" if="[[!_empty(notifications)]]">

View File

@ -1,7 +1,9 @@
import { fireEvent } from "../../common/dom/fire_event";
const loadVoiceCommandDialog = () =>
import(/* webpackChunkName: "ha-voice-command-dialog" */ "./ha-voice-command-dialog");
import(
/* webpackChunkName: "ha-voice-command-dialog" */ "./ha-voice-command-dialog"
);
export const showVoiceCommandDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {

View File

@ -5,7 +5,9 @@ export interface ZHADeviceInfoDialogParams {
}
export const loadZHADeviceInfoDialog = () =>
import(/* webpackChunkName: "dialog-zha-device-info" */ "./dialog-zha-device-info");
import(
/* webpackChunkName: "dialog-zha-device-info" */ "./dialog-zha-device-info"
);
export const showZHADeviceInfoDialog = (
element: HTMLElement,

View File

@ -10,6 +10,8 @@ import "../auth/ha-authorize";
/* polyfill for paper-dropdown */
setTimeout(
() =>
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"),
import(
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
),
2000
);

View File

@ -23,13 +23,16 @@ declare global {
}
}
const isExternal = location.search.includes("external_auth=1");
const isExternal =
window.externalApp ||
window.webkit?.messageHandlers?.getExternalAuth ||
location.search.includes("external_auth=1");
const authProm = isExternal
? () =>
import(/* webpackChunkName: "external_auth" */ "../external_app/external_auth").then(
({ createExternalAuth }) => createExternalAuth(hassUrl)
)
import(
/* webpackChunkName: "external_auth" */ "../external_app/external_auth"
).then(({ createExternalAuth }) => createExternalAuth(hassUrl))
: () =>
getAuth({
hassUrl,

View File

@ -74,20 +74,23 @@ export const provideHass = (
restResponses.push([path, callback]);
}
mockAPI(new RegExp("states/.+"), (
// @ts-ignore
method,
path,
parameters
) => {
const [domain, objectId] = path.substr(7).split(".", 2);
if (!domain || !objectId) {
return;
mockAPI(
new RegExp("states/.+"),
(
// @ts-ignore
method,
path,
parameters
) => {
const [domain, objectId] = path.substr(7).split(".", 2);
if (!domain || !objectId) {
return;
}
addEntities(
getEntity(domain, objectId, parameters.state, parameters.attributes)
);
}
addEntities(
getEntity(domain, objectId, parameters.state, parameters.attributes)
);
});
);
const localLanguage = getLocalLanguage();
@ -117,9 +120,7 @@ export const provideHass = (
? callback(msg)
: Promise.reject({
code: "command_not_mocked",
message: `WS Command ${
msg.type
} is not implemented in provide_hass.`,
message: `WS Command ${msg.type} is not implemented in provide_hass.`,
});
},
subscribeMessage: async (onChange, msg) => {
@ -128,9 +129,7 @@ export const provideHass = (
? callback(msg, onChange)
: Promise.reject({
code: "command_not_mocked",
message: `WS Command ${
msg.type
} is not implemented in provide_hass.`,
message: `WS Command ${msg.type} is not implemented in provide_hass.`,
});
},
subscribeEvents: async (

View File

@ -45,7 +45,9 @@ export class HomeAssistantAppEl extends HassElement {
this._initialize();
setTimeout(registerServiceWorker, 1000);
/* polyfill for paper-dropdown */
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
import(
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
);
}
protected updated(changedProps: PropertyValues): void {
@ -55,9 +57,10 @@ export class HomeAssistantAppEl extends HassElement {
this._updateHass({ panelUrl: this._panelUrl });
}
if (changedProps.has("hass")) {
this.hassChanged(this.hass!, changedProps.get("hass") as
| HomeAssistant
| undefined);
this.hassChanged(
this.hass!,
changedProps.get("hass") as HomeAssistant | undefined
);
}
}

View File

@ -12,33 +12,59 @@ import { removeInitSkeleton } from "../util/init-skeleton";
const CACHE_COMPONENTS = ["lovelace", "states", "developer-tools"];
const COMPONENTS = {
calendar: () =>
import(/* webpackChunkName: "panel-calendar" */ "../panels/calendar/ha-panel-calendar"),
import(
/* webpackChunkName: "panel-calendar" */ "../panels/calendar/ha-panel-calendar"
),
config: () =>
import(/* webpackChunkName: "panel-config" */ "../panels/config/ha-panel-config"),
import(
/* webpackChunkName: "panel-config" */ "../panels/config/ha-panel-config"
),
custom: () =>
import(/* webpackChunkName: "panel-custom" */ "../panels/custom/ha-panel-custom"),
import(
/* webpackChunkName: "panel-custom" */ "../panels/custom/ha-panel-custom"
),
"developer-tools": () =>
import(/* webpackChunkName: "panel-developer-tools" */ "../panels/developer-tools/ha-panel-developer-tools"),
import(
/* webpackChunkName: "panel-developer-tools" */ "../panels/developer-tools/ha-panel-developer-tools"
),
lovelace: () =>
import(/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace"),
import(
/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace"
),
states: () =>
import(/* webpackChunkName: "panel-states" */ "../panels/states/ha-panel-states"),
import(
/* webpackChunkName: "panel-states" */ "../panels/states/ha-panel-states"
),
history: () =>
import(/* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history"),
import(
/* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history"
),
iframe: () =>
import(/* webpackChunkName: "panel-iframe" */ "../panels/iframe/ha-panel-iframe"),
import(
/* webpackChunkName: "panel-iframe" */ "../panels/iframe/ha-panel-iframe"
),
kiosk: () =>
import(/* webpackChunkName: "panel-kiosk" */ "../panels/kiosk/ha-panel-kiosk"),
import(
/* webpackChunkName: "panel-kiosk" */ "../panels/kiosk/ha-panel-kiosk"
),
logbook: () =>
import(/* webpackChunkName: "panel-logbook" */ "../panels/logbook/ha-panel-logbook"),
import(
/* webpackChunkName: "panel-logbook" */ "../panels/logbook/ha-panel-logbook"
),
mailbox: () =>
import(/* webpackChunkName: "panel-mailbox" */ "../panels/mailbox/ha-panel-mailbox"),
import(
/* webpackChunkName: "panel-mailbox" */ "../panels/mailbox/ha-panel-mailbox"
),
map: () =>
import(/* webpackChunkName: "panel-map" */ "../panels/map/ha-panel-map"),
profile: () =>
import(/* webpackChunkName: "panel-profile" */ "../panels/profile/ha-panel-profile"),
import(
/* webpackChunkName: "panel-profile" */ "../panels/profile/ha-panel-profile"
),
"shopping-list": () =>
import(/* webpackChunkName: "panel-shopping-list" */ "../panels/shopping-list/ha-panel-shopping-list"),
import(
/* webpackChunkName: "panel-shopping-list" */ "../panels/shopping-list/ha-panel-shopping-list"
),
};
const getRoutes = (panels: Panels): RouterOptions => {

View File

@ -84,8 +84,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._fetchOnboardingSteps();
import(/* webpackChunkName: "onboarding-integrations" */ "./onboarding-integrations");
import(/* webpackChunkName: "onboarding-core-config" */ "./onboarding-core-config");
import(
/* webpackChunkName: "onboarding-integrations" */ "./onboarding-integrations"
);
import(
/* webpackChunkName: "onboarding-core-config" */ "./onboarding-core-config"
);
registerServiceWorker(false);
this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev));
}

View File

@ -124,7 +124,9 @@ class OnboardingIntegrations extends LitElement {
loadConfigFlowDialog();
this._loadConfigEntries();
/* polyfill for paper-dropdown */
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
import(
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
);
}
private _createFlow() {

View File

@ -14,7 +14,9 @@ export interface AreaRegistryDetailDialogParams {
}
export const loadAreaRegistryDetailDialog = () =>
import(/* webpackChunkName: "area-registry-detail-dialog" */ "./dialog-area-registry-detail");
import(
/* webpackChunkName: "area-registry-detail-dialog" */ "./dialog-area-registry-detail"
);
export const showAreaRegistryDetailDialog = (
element: HTMLElement,

View File

@ -32,6 +32,7 @@ import {
} from "../../../data/automation";
import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation";
function AutomationEditor(mountEl, props, mergeEl) {
return render(h(Automation, props), mountEl, mergeEl);
@ -210,15 +211,18 @@ export class HaAutomationEditor extends LitElement {
}
private _backTapped(): void {
if (
this._dirty &&
!confirm(
this.hass!.localize("ui.panel.config.automation.editor.unsaved_confirm")
)
) {
return;
if (this._dirty) {
showConfirmationDialog(this, {
text: this.hass!.localize(
"ui.panel.config.automation.editor.unsaved_confirm"
),
confirmBtnText: this.hass!.localize("ui.common.yes"),
cancelBtnText: this.hass!.localize("ui.common.no"),
confirm: () => history.back(),
});
} else {
history.back();
}
history.back();
}
private async _delete() {

View File

@ -115,9 +115,7 @@ class HaAutomationPicker extends LitElement {
<a
href=${ifDefined(
automation.attributes.id
? `/config/automation/edit/${
automation.attributes.id
}`
? `/config/automation/edit/${automation.attributes.id}`
: undefined
)}
>

View File

@ -6,7 +6,9 @@ export interface ThingtalkDialogParams {
}
export const loadThingtalkDialog = () =>
import(/* webpackChunkName: "thingtalk-dialog" */ "./thingtalk/dialog-thingtalk");
import(
/* webpackChunkName: "thingtalk-dialog" */ "./thingtalk/dialog-thingtalk"
);
export const showThingtalkDialog = (
element: HTMLElement,

View File

@ -27,6 +27,7 @@ import { PlaceholderValues } from "./ha-thingtalk-placeholders";
import { convertThingTalk } from "../../../../data/cloud";
export interface Placeholder {
name: string;
index: number;
fields: string[];
domains: string[];
@ -177,8 +178,21 @@ class DialogThingtalk extends LitElement {
const placeholderValues = ev.detail.value as PlaceholderValues;
Object.entries(placeholderValues).forEach(([type, values]) => {
Object.entries(values).forEach(([index, placeholder]) => {
Object.entries(placeholder).forEach(([field, value]) => {
this._config[type][index][field] = value;
const devices = Object.values(placeholder);
if (devices.length === 1) {
Object.entries(devices[0]).forEach(([field, value]) => {
this._config[type][index][field] = value;
return;
});
}
const automation = { ...this._config[type][index] };
delete this._config[type][index];
devices.forEach((fields) => {
const newAutomation = { ...automation };
Object.entries(fields).forEach(([field, value]) => {
newAutomation[field] = value;
});
this._config[type].push(newAutomation);
});
});
});

View File

@ -6,8 +6,11 @@ import {
customElement,
css,
CSSResult,
query,
PropertyValues,
} from "lit-element";
import "../../../../components/device/ha-area-devices-picker";
import { HomeAssistant } from "../../../../types";
import { PolymerChangedEvent } from "../../../../polymer-types";
import { fireEvent } from "../../../../common/dom/fire_event";
@ -17,8 +20,15 @@ import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { HassEntity } from "home-assistant-js-websocket";
import { HaDevicePicker } from "../../../../components/device/ha-device-picker";
import { getPath, applyPatch } from "../../../../common/util/patch";
import {
subscribeAreaRegistry,
AreaRegistryEntry,
} from "../../../../data/area_registry";
import {
subscribeDeviceRegistry,
DeviceRegistryEntry,
} from "../../../../data/device_registry";
declare global {
// for fire event
@ -28,7 +38,23 @@ declare global {
}
export interface PlaceholderValues {
[key: string]: { [index: number]: { [key: string]: string } };
[key: string]: {
[index: number]: {
[index: number]: { device_id?: string; entity_id?: string };
};
};
}
export interface ExtraInfo {
[key: string]: {
[index: number]: {
[index: number]: {
area_id?: string;
device_ids?: string[];
manualEntity: boolean;
};
};
};
}
interface DeviceEntitiesLookup {
@ -43,9 +69,11 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
@property() public placeholders!: PlaceholderContainer;
@property() private _error?: string;
private _deviceEntityLookup: DeviceEntitiesLookup = {};
private _manualEntities: PlaceholderValues = {};
@property() private _extraInfo: ExtraInfo = {};
@property() private _placeholderValues: PlaceholderValues = {};
@query("#device-entity-picker") private _deviceEntityPicker?: HaDevicePicker;
private _devices?: DeviceRegistryEntry[];
private _areas?: AreaRegistryEntry[];
private _search = false;
public hassSubscribe() {
return [
@ -66,12 +94,28 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
}
}
}),
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._devices = devices;
this._searchNames();
}),
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this._areas = areas;
this._searchNames();
}),
];
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("placeholders")) {
this._search = true;
this._searchNames();
}
}
protected render(): TemplateResult | void {
return html`
<ha-paper-dialog
modal
with-backdrop
.opened=${this.opened}
@opened-changed="${this._openedChanged}"
@ -93,55 +137,66 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
</h3>
${placeholders.map((placeholder) => {
if (placeholder.fields.includes("device_id")) {
const extraInfo = getPath(this._extraInfo, [
type,
placeholder.index,
]);
return html`
<ha-device-picker
<ha-area-devices-picker
.type=${type}
.placeholder=${placeholder}
@change=${this._devicePicked}
@value-changed=${this._devicePicked}
.hass=${this.hass}
.area=${extraInfo ? extraInfo.area_id : undefined}
.devices=${extraInfo && extraInfo.device_ids
? extraInfo.device_ids
: undefined}
.includeDomains=${placeholder.domains}
.includeDeviceClasses=${placeholder.device_classes}
.label=${this._getLabel(
placeholder.domains,
placeholder.device_classes
)}
></ha-device-picker>
${(getPath(this._placeholderValues, [
type,
placeholder.index,
"device_id",
]) &&
placeholder.fields.includes("entity_id") &&
getPath(this._placeholderValues, [
type,
placeholder.index,
"entity_id",
]) === undefined) ||
getPath(this._manualEntities, [
type,
placeholder.index,
"manual",
]) === true
></ha-area-devices-picker>
${extraInfo && extraInfo.manualEntity
? html`
<ha-entity-picker
id="device-entity-picker"
.type=${type}
.placeholder=${placeholder}
@change=${this._entityPicked}
.includeDomains=${placeholder.domains}
.includeDeviceClasses=${placeholder.device_classes}
.hass=${this.hass}
.label=${this._getLabel(
placeholder.domains,
placeholder.device_classes
)}
.entityFilter=${(state: HassEntity) =>
this._deviceEntityLookup[
this._placeholderValues[type][
placeholder.index
].device_id
].includes(state.entity_id)}
></ha-entity-picker>
<h3>
One or more devices have more than one matching
entity, please pick the one you want to use.
</h3>
${Object.keys(extraInfo.manualEntity).map(
(idx) => html`
<ha-entity-picker
id="device-entity-picker"
.type=${type}
.placeholder=${placeholder}
.index=${idx}
@change=${this._entityPicked}
.includeDomains=${placeholder.domains}
.includeDeviceClasses=${placeholder.device_classes}
.hass=${this.hass}
.label=${`${this._getLabel(
placeholder.domains,
placeholder.device_classes
)} of device ${this._getDeviceName(
getPath(this._placeholderValues, [
type,
placeholder.index,
idx,
"device_id",
])
)}`}
.entityFilter=${(state: HassEntity) => {
const devId = this._placeholderValues[type][
placeholder.index
][idx].device_id;
return this._deviceEntityLookup[
devId
].includes(state.entity_id);
}}
></ha-entity-picker>
`
)}
`
: ""}
`;
@ -189,17 +244,74 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
`;
}
private _getDeviceName(deviceId: string): string {
if (!this._devices) {
return "";
}
const foundDevice = this._devices.find((device) => device.id === deviceId);
if (!foundDevice) {
return "";
}
return foundDevice.name_by_user || foundDevice.name || "";
}
private _searchNames() {
if (!this._search || !this._areas || !this._devices) {
return;
}
this._search = false;
Object.entries(this.placeholders).forEach(([type, placeholders]) =>
placeholders.forEach((placeholder) => {
if (!placeholder.name) {
return;
}
const name = placeholder.name;
const foundArea = this._areas!.find((area) =>
area.name.toLowerCase().includes(name)
);
if (foundArea) {
applyPatch(
this._extraInfo,
[type, placeholder.index, "area_id"],
foundArea.area_id
);
this.requestUpdate("_extraInfo");
return;
}
const foundDevices = this._devices!.filter((device) => {
const deviceName = device.name_by_user || device.name;
if (!deviceName) {
return false;
}
return deviceName.toLowerCase().includes(name);
});
if (foundDevices.length) {
applyPatch(
this._extraInfo,
[type, placeholder.index, "device_ids"],
foundDevices.map((device) => device.id)
);
this.requestUpdate("_extraInfo");
}
})
);
}
private get _isDone(): boolean {
return Object.entries(this.placeholders).every(([type, placeholders]) =>
placeholders.every((placeholder) =>
placeholder.fields.every(
(field) =>
getPath(this._placeholderValues, [
type,
placeholder.index,
field,
]) !== undefined
)
placeholder.fields.every((field) => {
const entries: {
[key: number]: { device_id?: string; entity_id?: string };
} = getPath(this._placeholderValues, [type, placeholder.index]);
if (!entries) {
return false;
}
const values = Object.values(entries);
return values.every(
(entry) => entry[field] !== undefined && entry[field] !== ""
);
})
)
);
}
@ -212,76 +324,115 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
}`;
}
private _devicePicked(ev: Event): void {
private _devicePicked(ev: CustomEvent): void {
const value: string[] = ev.detail.value;
if (!value) {
return;
}
const target = ev.target as any;
const placeholder = target.placeholder as Placeholder;
const value = target.value;
const type = target.type;
applyPatch(
this._placeholderValues,
[type, placeholder.index, "device_id"],
value
);
if (!placeholder.fields.includes("entity_id")) {
return;
let oldValues = getPath(this._placeholderValues, [type, placeholder.index]);
if (oldValues) {
oldValues = Object.values(oldValues);
}
if (value === "") {
delete this._placeholderValues[type][placeholder.index].entity_id;
if (this._deviceEntityPicker) {
this._deviceEntityPicker.value = undefined;
}
applyPatch(
this._manualEntities,
[type, placeholder.index, "manual"],
false
);
const oldExtraInfo = getPath(this._extraInfo, [type, placeholder.index]);
if (this._placeholderValues[type]) {
delete this._placeholderValues[type][placeholder.index];
}
if (this._extraInfo[type]) {
delete this._extraInfo[type][placeholder.index];
}
if (!value.length) {
this.requestUpdate("_placeholderValues");
return;
}
const devEntities = this._deviceEntityLookup[value];
const entities = devEntities.filter((eid) => {
if (placeholder.device_classes) {
const stateObj = this.hass.states[eid];
if (!stateObj) {
return false;
value.forEach((deviceId, index) => {
let oldIndex;
if (oldValues) {
const oldDevice = oldValues.find((oldVal, idx) => {
oldIndex = idx;
return oldVal.device_id === deviceId;
});
if (oldDevice) {
applyPatch(
this._placeholderValues,
[type, placeholder.index, index],
oldDevice
);
if (oldExtraInfo) {
applyPatch(
this._extraInfo,
[type, placeholder.index, index],
oldExtraInfo[oldIndex]
);
}
return;
}
return (
placeholder.domains.includes(computeDomain(eid)) &&
stateObj.attributes.device_class &&
placeholder.device_classes.includes(stateObj.attributes.device_class)
);
}
return placeholder.domains.includes(computeDomain(eid));
});
if (entities.length === 0) {
// Should not happen because we filter the device picker on domain
this._error = `No ${placeholder.domains
.map((domain) => this.hass.localize(`domain.${domain}`))
.join(", ")} entities found in this device.`;
} else if (entities.length === 1) {
applyPatch(
this._placeholderValues,
[type, placeholder.index, "entity_id"],
entities[0]
[type, placeholder.index, index, "device_id"],
deviceId
);
applyPatch(
this._manualEntities,
[type, placeholder.index, "manual"],
false
);
this.requestUpdate("_placeholderValues");
} else {
delete this._placeholderValues[type][placeholder.index].entity_id;
if (this._deviceEntityPicker) {
this._deviceEntityPicker.value = undefined;
if (!placeholder.fields.includes("entity_id")) {
return;
}
applyPatch(
this._manualEntities,
[type, placeholder.index, "manual"],
true
);
this.requestUpdate("_placeholderValues");
}
const devEntities = this._deviceEntityLookup[deviceId];
const entities = devEntities.filter((eid) => {
if (placeholder.device_classes) {
const stateObj = this.hass.states[eid];
if (!stateObj) {
return false;
}
return (
placeholder.domains.includes(computeDomain(eid)) &&
stateObj.attributes.device_class &&
placeholder.device_classes.includes(
stateObj.attributes.device_class
)
);
}
return placeholder.domains.includes(computeDomain(eid));
});
if (entities.length === 0) {
// Should not happen because we filter the device picker on domain
this._error = `No ${placeholder.domains
.map((domain) => this.hass.localize(`domain.${domain}`))
.join(", ")} entities found in this device.`;
} else if (entities.length === 1) {
applyPatch(
this._placeholderValues,
[type, placeholder.index, index, "entity_id"],
entities[0]
);
this.requestUpdate("_placeholderValues");
} else {
delete this._placeholderValues[type][placeholder.index][index]
.entity_id;
applyPatch(
this._extraInfo,
[type, placeholder.index, "manualEntity", index],
true
);
this.requestUpdate("_placeholderValues");
}
});
fireEvent(
this.shadowRoot!.querySelector("ha-paper-dialog")! as HTMLElement,
"iron-resize"
);
}
private _entityPicked(ev: Event): void {
@ -289,9 +440,10 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
const placeholder = target.placeholder as Placeholder;
const value = target.value;
const type = target.type;
const index = target.index || 0;
applyPatch(
this._placeholderValues,
[type, placeholder.index, "entity_id"],
[type, placeholder.index, index, "entity_id"],
value
);
this.requestUpdate("_placeholderValues");

View File

@ -0,0 +1,345 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
// tslint:disable-next-line
import { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-menu-button/paper-menu-button";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
} from "lit-element";
import { dynamicContentDirective } from "../../../../common/dom/dynamic-content-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card";
import { HomeAssistant } from "../../../../types";
import "./types/ha-automation-trigger-device";
import "./types/ha-automation-trigger-event";
import "./types/ha-automation-trigger-state";
import "./types/ha-automation-trigger-geo_location";
import "./types/ha-automation-trigger-homeassistant";
import "./types/ha-automation-trigger-mqtt";
import "./types/ha-automation-trigger-numeric_state";
import "./types/ha-automation-trigger-sun";
import "./types/ha-automation-trigger-template";
import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
import { DeviceTrigger } from "../../../../data/device_automation";
const OPTIONS = [
"device",
"event",
"state",
"geo_location",
"homeassistant",
"mqtt",
"numeric_state",
"sun",
"template",
"time",
"time_pattern",
"webhook",
"zone",
];
export interface ForDict {
hours?: number | string;
minutes?: number | string;
seconds?: number | string;
}
export interface StateTrigger {
platform: "state";
entity_id: string;
from?: string | number;
to?: string | number;
for?: string | number | ForDict;
}
export interface MqttTrigger {
platform: "mqtt";
topic: string;
payload?: string;
}
export interface GeoLocationTrigger {
platform: "geo_location";
source: "string";
zone: "string";
event: "enter" | "leave";
}
export interface HassTrigger {
platform: "homeassistant";
event: "start" | "shutdown";
}
export interface NumericStateTrigger {
platform: "numeric_state";
entity_id: string;
above?: number;
below?: number;
value_template?: string;
for?: string | number | ForDict;
}
export interface SunTrigger {
platform: "sun";
offset: number;
event: "sunrise" | "sunset";
}
export interface TimePatternTrigger {
platform: "time_pattern";
hours?: number | string;
minutes?: number | string;
seconds?: number | string;
}
export interface WebhookTrigger {
platform: "webhook";
webhook_id: string;
}
export interface ZoneTrigger {
platform: "zone";
entity_id: string;
zone: string;
event: "enter" | "leave";
}
export interface TimeTrigger {
platform: "time";
at: string;
}
export interface TemplateTrigger {
platform: "template";
value_template: string;
}
export interface EventTrigger {
platform: "event";
event_type: string;
event_data: any;
}
export type Trigger =
| StateTrigger
| MqttTrigger
| GeoLocationTrigger
| HassTrigger
| NumericStateTrigger
| SunTrigger
| TimePatternTrigger
| WebhookTrigger
| ZoneTrigger
| TimeTrigger
| TemplateTrigger
| EventTrigger
| DeviceTrigger;
export interface TriggerElement extends LitElement {
trigger: Trigger;
}
export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => {
ev.stopPropagation();
const name = (ev.target as any)?.name;
if (!name) {
return;
}
const newVal = ev.detail.value;
if ((element.trigger[name] || "") === newVal) {
return;
}
let newTrigger: Trigger;
if (!newVal) {
newTrigger = { ...element.trigger };
delete newTrigger[name];
} else {
newTrigger = { ...element.trigger, [name]: newVal };
}
fireEvent(element, "value-changed", { value: newTrigger });
};
@customElement("ha-automation-trigger-row")
export default class HaAutomationTriggerRow extends LitElement {
@property() public hass!: HomeAssistant;
@property() public trigger!: Trigger;
@property() private _yamlMode = false;
protected render() {
if (!this.trigger) {
return html``;
}
const hasEditor = OPTIONS.includes(this.trigger.platform);
if (!hasEditor) {
this._yamlMode = true;
}
const selected = OPTIONS.indexOf(this.trigger.platform);
return html`
<ha-card>
<div class="card-content">
<div class="card-menu">
<paper-menu-button
no-animations
horizontal-align="right"
horizontal-offset="-5"
vertical-offset="-5"
close-on-activate
>
<paper-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item @click=${this._switchYamlMode}>
${this._yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</paper-item>
<paper-item disabled>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.duplicate"
)}
</paper-item>
<paper-item @click=${this._onDelete}>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.delete"
)}
</paper-item>
</paper-listbox>
</paper-menu-button>
</div>
${this._yamlMode
? html`
<div style="margin-right: 24px;">
${!hasEditor
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.unsupported_platform",
"platform",
this.trigger.platform
)}
`
: ""}
<ha-yaml-editor
.value=${this.trigger}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
</div>
`
: html`
<paper-dropdown-menu-light
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type_select"
)}
no-animations
>
<paper-listbox
slot="dropdown-content"
.selected=${selected}
@iron-select=${this._typeChanged}
>
${OPTIONS.map(
(opt) => html`
<paper-item .platform=${opt}>
${this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.${opt}.label`
)}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
<div>
${dynamicContentDirective(
`ha-automation-trigger-${this.trigger.platform}`,
{ hass: this.hass, trigger: this.trigger }
)}
</div>
`}
</div>
</ha-card>
`;
}
private _onDelete() {
if (
confirm(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.delete_confirm"
)
)
) {
fireEvent(this, "value-changed", { value: null });
}
}
private _typeChanged(ev: CustomEvent) {
const type = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.platform;
if (!type) {
return;
}
const elClass = customElements.get(`ha-automation-trigger-${type}`);
if (type !== this.trigger.platform) {
fireEvent(this, "value-changed", {
value: {
platform: type,
...elClass.defaultConfig,
},
});
}
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", { value: ev.detail.value });
}
private _switchYamlMode() {
this._yamlMode = !this._yamlMode;
}
static get styles(): CSSResult {
return css`
.card-menu {
position: absolute;
top: 0;
right: 0;
z-index: 3;
color: var(--primary-text-color);
}
.rtl .card-menu {
right: auto;
left: 0;
}
.card-menu paper-item {
cursor: pointer;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-row": HaAutomationTriggerRow;
}
}

View File

@ -0,0 +1,93 @@
import {
LitElement,
customElement,
html,
property,
CSSResult,
css,
} from "lit-element";
import "@material/mwc-button";
import "../../../../components/ha-card";
import { fireEvent } from "../../../../common/dom/fire_event";
import { HomeAssistant } from "../../../../types";
import "./ha-automation-trigger-row";
@customElement("ha-automation-trigger")
export default class HaAutomationTrigger extends LitElement {
@property() public hass!: HomeAssistant;
@property() public triggers;
protected render() {
return html`
<div class="triggers">
${this.triggers.map(
(trg, idx) => html`
<ha-automation-trigger-row
.index=${idx}
.trigger=${trg}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
></ha-automation-trigger-row>
`
)}
<ha-card>
<div class="card-actions add-card">
<mwc-button @click=${this._addTrigger}>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.add"
)}
</mwc-button>
</div>
</ha-card>
</div>
`;
}
private _addTrigger() {
const triggers = this.triggers.concat({
platform: "state",
});
fireEvent(this, "value-changed", { value: triggers });
}
private _triggerChanged(ev: CustomEvent) {
ev.stopPropagation();
const triggers = [...this.triggers];
const newValue = ev.detail.value;
const index = (ev.target as any).index;
if (newValue === null) {
triggers.splice(index, 1);
} else {
triggers[index] = newValue;
}
fireEvent(this, "value-changed", { value: triggers });
}
static get styles(): CSSResult {
return css`
.triggers,
.script {
margin-top: -16px;
}
.triggers ha-card,
.script ha-card {
margin-top: 16px;
}
.add-card mwc-button {
display: block;
text-align: center;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger": HaAutomationTrigger;
}
}

View File

@ -0,0 +1,128 @@
import "../../../../../components/device/ha-device-picker";
import "../../../../../components/device/ha-device-trigger-picker";
import "../../../../../components/ha-form/ha-form";
import {
fetchDeviceTriggerCapabilities,
deviceAutomationsEqual,
DeviceTrigger,
} from "../../../../../data/device_automation";
import { LitElement, customElement, property, html } from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { HomeAssistant } from "../../../../../types";
@customElement("ha-automation-trigger-device")
export class HaDeviceTrigger extends LitElement {
@property() public hass!: HomeAssistant;
@property() public trigger!: DeviceTrigger;
@property() private _deviceId?: string;
@property() private _capabilities?;
private _origTrigger?: DeviceTrigger;
public static get defaultConfig() {
return {
device_id: "",
domain: "",
entity_id: "",
};
}
protected render() {
if (this._deviceId === undefined) {
this._deviceId = this.trigger.device_id;
}
const extraFieldsData =
this._capabilities && this._capabilities.extra_fields
? this._capabilities.extra_fields.map((item) => {
return { [item.name]: this.trigger[item.name] };
})
: undefined;
return html`
<ha-device-picker
.value=${this._deviceId}
@value-changed=${this._devicePicked}
.hass=${this.hass}
label="Device"
></ha-device-picker>
<ha-device-trigger-picker
.value=${this.trigger}
.deviceId=${this._deviceId}
@value-changed=${this._deviceTriggerPicked}
.hass=${this.hass}
label="Trigger"
></ha-device-trigger-picker>
${extraFieldsData
? html`
<ha-form
.data=${Object.assign({}, ...extraFieldsData)}
.schema=${this._capabilities.extra_fields}
.computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize
)}
@value-changed=${this._extraFieldsChanged}
></ha-form>
`
: ""}
`;
}
protected firstUpdated() {
if (!this._capabilities) {
this._getCapabilities();
}
if (this.trigger) {
this._origTrigger = this.trigger;
}
}
protected updated(changedPros) {
const prevTrigger = changedPros.get("trigger");
if (prevTrigger && !deviceAutomationsEqual(prevTrigger, this.trigger)) {
this._getCapabilities();
}
}
private async _getCapabilities() {
const trigger = this.trigger;
this._capabilities = trigger.domain
? await fetchDeviceTriggerCapabilities(this.hass, trigger)
: null;
}
private _devicePicked(ev) {
ev.stopPropagation();
this._deviceId = ev.target.value;
}
private _deviceTriggerPicked(ev) {
ev.stopPropagation();
let trigger = ev.detail.value;
if (
this._origTrigger &&
deviceAutomationsEqual(this._origTrigger, trigger)
) {
trigger = this._origTrigger;
}
fireEvent(this, "value-changed", { value: trigger });
}
private _extraFieldsChanged(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.trigger,
...ev.detail.value,
},
});
}
private _extraFieldsComputeLabelCallback(localize) {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
localize(
`ui.panel.config.automation.editor.triggers.type.device.extra_fields.${schema.name}`
) || schema.name;
}
}

View File

@ -0,0 +1,53 @@
import "@polymer/paper-input/paper-input";
import "../../../../../components/ha-yaml-editor";
import { LitElement, property, customElement } from "lit-element";
import {
TriggerElement,
EventTrigger,
handleChangeEvent,
} from "../ha-automation-trigger-row";
import { HomeAssistant } from "../../../../../types";
import { html } from "lit-html";
@customElement("ha-automation-trigger-event")
export class HaEventTrigger extends LitElement implements TriggerElement {
@property() public hass!: HomeAssistant;
@property() public trigger!: EventTrigger;
public static get defaultConfig() {
return { event_type: "", event_data: {} };
}
public render() {
const { event_type, event_data } = this.trigger;
return html`
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.event.event_type"
)}
name="event_type"
.value="${event_type}"
@value-changed="${this._valueChanged}"
></paper-input>
<ha-yaml-editor
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.event.event_data"
)}
.name=${"event_data"}
.value=${event_data}
@value-changed=${this._valueChanged}
></ha-yaml-editor>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-event": HaEventTrigger;
}
}

View File

@ -0,0 +1,99 @@
import "@polymer/paper-radio-button/paper-radio-button";
import "@polymer/paper-radio-group/paper-radio-group";
// tslint:disable-next-line
import { PaperRadioGroupElement } from "@polymer/paper-radio-group/paper-radio-group";
import "../../../../../components/entity/ha-entity-picker";
import { LitElement, customElement, property, html } from "lit-element";
import { HomeAssistant } from "../../../../../types";
import {
GeoLocationTrigger,
handleChangeEvent,
} from "../ha-automation-trigger-row";
import { fireEvent } from "../../../../../common/dom/fire_event";
@customElement("ha-automation-trigger-geo_location")
export default class HaGeolocationTrigger extends LitElement {
@property() public hass!: HomeAssistant;
@property() public trigger!: GeoLocationTrigger;
public static get defaultConfig() {
return {
source: "",
zone: "",
event: "enter",
};
}
protected render() {
const { source, zone, event } = this.trigger;
return html`
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.source"
)}
name="source"
.value=${source}
@value-changed="${this._valueChanged}"
></paper-input>
<ha-entity-picker
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.zone"
)}
.value=${zone}
@value-changed=${this._zonePicked}
.hass=${this.hass}
allow-custom-entity
.includeDomains=${["zone"]}
></ha-entity-picker>
<label id="eventlabel">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.event"
)}
</label>
<paper-radio-group
.selected=${event}
aria-labelledby="eventlabel"
@paper-radio-group-changed=${this._radioGroupPicked}
>
<paper-radio-button name="enter">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.enter"
)}
</paper-radio-button>
<paper-radio-button name="leave">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.leave"
)}
</paper-radio-button>
</paper-radio-group>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
private _zonePicked(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.trigger, zone: ev.detail.value },
});
}
private _radioGroupPicked(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.trigger,
event: (ev.target as PaperRadioGroupElement).selected,
},
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-geo_location": HaGeolocationTrigger;
}
}

View File

@ -0,0 +1,63 @@
import "@polymer/paper-radio-button/paper-radio-button";
import "@polymer/paper-radio-group/paper-radio-group";
// tslint:disable-next-line
import { PaperRadioGroupElement } from "@polymer/paper-radio-group/paper-radio-group";
import { LitElement, html, property, customElement } from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { HomeAssistant } from "../../../../../types";
import { HassTrigger } from "../ha-automation-trigger-row";
@customElement("ha-automation-trigger-homeassistant")
export default class HaHassTrigger extends LitElement {
@property() public hass!: HomeAssistant;
@property() public trigger!: HassTrigger;
public static get defaultConfig() {
return {
event: "start",
};
}
public render() {
const { event } = this.trigger;
return html`
<label id="eventlabel">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.homeassistant.event"
)}
</label>
<paper-radio-group
.selected=${event}
aria-labelledby="eventlabel"
@paper-radio-group-changed="${this._radioGroupPicked}"
>
<paper-radio-button name="start">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.homeassistant.start"
)}
</paper-radio-button>
<paper-radio-button name="shutdown">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.homeassistant.shutdown"
)}
</paper-radio-button>
</paper-radio-group>
`;
}
private _radioGroupPicked(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.trigger,
event: (ev.target as PaperRadioGroupElement).selected,
},
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-homeassistant": HaHassTrigger;
}
}

View File

@ -0,0 +1,50 @@
import "@polymer/paper-input/paper-input";
import { LitElement, customElement, property, html } from "lit-element";
import { HomeAssistant } from "../../../../../types";
import {
handleChangeEvent,
TriggerElement,
MqttTrigger,
} from "../ha-automation-trigger-row";
@customElement("ha-automation-trigger-mqtt")
export class HaMQTTTrigger extends LitElement implements TriggerElement {
@property() public hass!: HomeAssistant;
@property() public trigger!: MqttTrigger;
public static get defaultConfig() {
return { topic: "" };
}
protected render() {
const { topic, payload } = this.trigger;
return html`
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.mqtt.topic"
)}
name="topic"
.value=${topic}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.mqtt.payload"
)}
name="payload"
.value=${payload}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-mqtt": HaMQTTTrigger;
}
}

View File

@ -0,0 +1,103 @@
import "@polymer/paper-input/paper-input";
import "../../../../../components/ha-textarea";
import "../../../../../components/entity/ha-entity-picker";
import { LitElement, html, customElement, property } from "lit-element";
import { HomeAssistant } from "../../../../../types";
import { fireEvent } from "../../../../../common/dom/fire_event";
import {
NumericStateTrigger,
ForDict,
handleChangeEvent,
} from "../ha-automation-trigger-row";
@customElement("ha-automation-trigger-numeric_state")
export default class HaNumericStateTrigger extends LitElement {
@property() public hass!: HomeAssistant;
@property() public trigger!: NumericStateTrigger;
public static get defaultConfig() {
return {
entity_id: "",
};
}
public render() {
const { value_template, entity_id, below, above } = this.trigger;
let trgFor = this.trigger.for;
if (
trgFor &&
((trgFor as ForDict).hours ||
(trgFor as ForDict).minutes ||
(trgFor as ForDict).seconds)
) {
// If the trigger was defined using the yaml dict syntax, convert it to
// the equivalent string format
let { hours = 0, minutes = 0, seconds = 0 } = trgFor as ForDict;
hours = hours.toString();
minutes = minutes.toString().padStart(2, "0");
seconds = seconds.toString().padStart(2, "0");
trgFor = `${hours}:${minutes}:${seconds}`;
}
return html`
<ha-entity-picker
.value="${entity_id}"
@value-changed="${this._entityPicked}"
.hass="${this.hass}"
allow-custom-entity
></ha-entity-picker>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.above"
)}
name="above"
.value=${above}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.below"
)}
name="below"
.value=${below}
@value-changed=${this._valueChanged}
></paper-input>
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.value_template"
)}
name="value_template"
.value=${value_template}
@value-changed=${this._valueChanged}
dir="ltr"
></ha-textarea>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.for"
)}
name="for"
.value=${trgFor}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
private _entityPicked(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.trigger, entity_id: ev.detail.value },
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-numeric_state": HaNumericStateTrigger;
}
}

View File

@ -0,0 +1,93 @@
import "@polymer/paper-input/paper-input";
import { customElement, html, LitElement, property } from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker";
import { HomeAssistant } from "../../../../../types";
import {
handleChangeEvent,
TriggerElement,
StateTrigger,
ForDict,
} from "../ha-automation-trigger-row";
import { PolymerChangedEvent } from "../../../../../polymer-types";
@customElement("ha-automation-trigger-state")
export class HaStateTrigger extends LitElement implements TriggerElement {
@property() public hass!: HomeAssistant;
@property() public trigger!: StateTrigger;
public static get defaultConfig() {
return { entity_id: "" };
}
protected render() {
const { entity_id, to, from } = this.trigger;
let trgFor = this.trigger.for;
if (
trgFor &&
((trgFor as ForDict).hours ||
(trgFor as ForDict).minutes ||
(trgFor as ForDict).seconds)
) {
// If the trigger was defined using the yaml dict syntax, convert it to
// the equivalent string format
let { hours = 0, minutes = 0, seconds = 0 } = trgFor as ForDict;
hours = hours.toString();
minutes = minutes.toString().padStart(2, "0");
seconds = seconds.toString().padStart(2, "0");
trgFor = `${hours}:${minutes}:${seconds}`;
}
return html`
<ha-entity-picker
.value=${entity_id}
@value-changed=${this._entityPicked}
.hass=${this.hass}
allow-custom-entity
></ha-entity-picker>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.from"
)}
.name=${"from"}
.value=${from}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.to"
)}
.name=${"to"}
.value=${to}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.for"
)}
.name=${"for"}
.value=${trgFor}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
private _entityPicked(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.trigger, entity_id: ev.detail.value },
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-state": HaStateTrigger;
}
}

View File

@ -0,0 +1,75 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-radio-button/paper-radio-button";
import "@polymer/paper-radio-group/paper-radio-group";
// tslint:disable-next-line
import { PaperRadioGroupElement } from "@polymer/paper-radio-group/paper-radio-group";
import { LitElement, customElement, property, html } from "lit-element";
import { HomeAssistant } from "../../../../../types";
import {
SunTrigger,
handleChangeEvent,
TriggerElement,
} from "../ha-automation-trigger-row";
import { fireEvent } from "../../../../../common/dom/fire_event";
@customElement("ha-automation-trigger-sun")
export class HaSunTrigger extends LitElement implements TriggerElement {
@property() public hass!: HomeAssistant;
@property() public trigger!: SunTrigger;
public static get defaultConfig() {
return {
event: "sunrise",
};
}
protected render() {
const { offset, event } = this.trigger;
return html`
<label id="eventlabel">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.sun.event"
)}
</label>
<paper-radio-group
.selected=${event}
aria-labelledby="eventlabel"
@paper-radio-group-changed=${this._radioGroupPicked}
>
<paper-radio-button name="sunrise">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.sun.sunrise"
)}
</paper-radio-button>
<paper-radio-button name="sunset">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.sun.sunset"
)}
</paper-radio-button>
</paper-radio-group>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.sun.offset"
)}
name="offset"
.value=${offset}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
private _radioGroupPicked(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.trigger,
event: (ev.target as PaperRadioGroupElement).selected,
},
});
}
}

View File

@ -0,0 +1,36 @@
import "../../../../../components/ha-textarea";
import { LitElement, property, html, customElement } from "lit-element";
import { HomeAssistant } from "../../../../../types";
import {
TemplateTrigger,
handleChangeEvent,
} from "../ha-automation-trigger-row";
@customElement("ha-automation-trigger-template")
export class HaTemplateTrigger extends LitElement {
@property() public hass!: HomeAssistant;
@property() public trigger!: TemplateTrigger;
public static get defaultConfig() {
return { value_template: "" };
}
protected render() {
const { value_template } = this.trigger;
return html`
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.template.value_template"
)}
name="value_template"
.value=${value_template}
@value-changed=${this._valueChanged}
dir="ltr"
></ha-textarea>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
}

View File

@ -0,0 +1,36 @@
import "@polymer/paper-input/paper-input";
import { LitElement, html, property, customElement } from "lit-element";
import { HomeAssistant } from "../../../../../types";
import {
TimeTrigger,
handleChangeEvent,
TriggerElement,
} from "../ha-automation-trigger-row";
@customElement("ha-automation-trigger-time")
export class HaTimeTrigger extends LitElement implements TriggerElement {
@property() public hass!: HomeAssistant;
@property() public trigger!: TimeTrigger;
public static get defaultConfig() {
return { at: "" };
}
protected render() {
const { at } = this.trigger;
return html`
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.time.at"
)}
name="at"
.value=${at}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
}

View File

@ -0,0 +1,58 @@
import "@polymer/paper-input/paper-input";
import { LitElement, property, html, customElement } from "lit-element";
import {
TriggerElement,
handleChangeEvent,
TimePatternTrigger,
} from "../ha-automation-trigger-row";
import { HomeAssistant } from "../../../../../types";
@customElement("ha-automation-trigger-time_pattern")
export class HaTimePatternTrigger extends LitElement implements TriggerElement {
@property() public hass!: HomeAssistant;
@property() public trigger!: TimePatternTrigger;
public static get defaultConfig() {
return {};
}
protected render() {
const { hours, minutes, seconds } = this.trigger;
return html`
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.time_pattern.hours"
)}
name="hours"
.value=${hours}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.time_pattern.minutes"
)}
name="minutes"
.value=${minutes}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.time_pattern.seconds"
)}
name="seconds"
.value=${seconds}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-time_pattern": HaTimePatternTrigger;
}
}

View File

@ -0,0 +1,43 @@
import "@polymer/paper-input/paper-input";
import { LitElement, customElement, property, html } from "lit-element";
import { HomeAssistant } from "../../../../../types";
import {
WebhookTrigger,
handleChangeEvent,
} from "../ha-automation-trigger-row";
@customElement("ha-automation-trigger-webhook")
export class HaWebhookTrigger extends LitElement {
@property() public hass!: HomeAssistant;
@property() public trigger!: WebhookTrigger;
public static get defaultConfig() {
return {
webhook_id: "",
};
}
protected render() {
const { webhook_id: webhookId } = this.trigger;
return html`
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.webhook.webhook_id"
)}
name="webhook_id"
.value=${webhookId}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-webhook": HaWebhookTrigger;
}
}

View File

@ -0,0 +1,108 @@
import "@polymer/paper-radio-button/paper-radio-button";
import "@polymer/paper-radio-group/paper-radio-group";
// tslint:disable-next-line
import { PaperRadioGroupElement } from "@polymer/paper-radio-group/paper-radio-group";
import "../../../../../components/entity/ha-entity-picker";
import { hasLocation } from "../../../../../common/entity/has_location";
import { computeStateDomain } from "../../../../../common/entity/compute_state_domain";
import { LitElement, property, html, customElement } from "lit-element";
import { HomeAssistant } from "../../../../../types";
import { ZoneTrigger } from "../ha-automation-trigger-row";
import { PolymerChangedEvent } from "../../../../../polymer-types";
import { fireEvent } from "../../../../../common/dom/fire_event";
function zoneAndLocationFilter(stateObj) {
return hasLocation(stateObj) && computeStateDomain(stateObj) !== "zone";
}
@customElement("ha-automation-trigger-zone")
export class HaZoneTrigger extends LitElement {
@property() public hass!: HomeAssistant;
@property() public trigger!: ZoneTrigger;
public static get defaultConfig() {
return {
entity_id: "",
zone: "",
event: "enter",
};
}
protected render() {
const { entity_id, zone, event } = this.trigger;
return html`
<ha-entity-picker
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.zone.entity"
)}
.value=${entity_id}
@value-changed=${this._entityPicked}
.hass=${this.hass}
allow-custom-entity
.entityFilter=${zoneAndLocationFilter}
></ha-entity-picker>
<ha-entity-picker
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.zone.zone"
)}
.value=${zone}
@value-changed=${this._zonePicked}
.hass=${this.hass}
allow-custom-entity
.includeDomains=${["zone"]}
></ha-entity-picker>
<label id="eventlabel">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.zone.event"
)}
</label>
<paper-radio-group
.selected=${event}
aria-labelledby="eventlabel"
@paper-radio-group-changed=${this._radioGroupPicked}
>
<paper-radio-button name="enter">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.zone.enter"
)}
</paper-radio-button>
<paper-radio-button name="leave">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.zone.leave"
)}
</paper-radio-button>
</paper-radio-group>
`;
}
private _entityPicked(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.trigger, entity_id: ev.detail.value },
});
}
private _zonePicked(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.trigger, zone: ev.detail.value },
});
}
private _radioGroupPicked(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.trigger,
event: (ev.target as PaperRadioGroupElement).selected,
},
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-zone": HaZoneTrigger;
}
}

View File

@ -12,7 +12,9 @@ export const showCloudCertificateDialog = (
fireEvent(element, "show-dialog", {
dialogTag: "dialog-cloud-certificate",
dialogImport: () =>
import(/* webpackChunkName: "dialog-cloud-certificate" */ "./dialog-cloud-certificate"),
import(
/* webpackChunkName: "dialog-cloud-certificate" */ "./dialog-cloud-certificate"
),
dialogParams: webhookDialogParams,
});
};

View File

@ -15,7 +15,9 @@ export const showManageCloudhookDialog = (
fireEvent(element, "show-dialog", {
dialogTag: "dialog-manage-cloudhook",
dialogImport: () =>
import(/* webpackChunkName: "cloud-webhook-manage-dialog" */ "./dialog-manage-cloudhook"),
import(
/* webpackChunkName: "cloud-webhook-manage-dialog" */ "./dialog-manage-cloudhook"
),
dialogParams: webhookDialogParams,
});
};

View File

@ -46,12 +46,16 @@ class HaConfigCloud extends HassRouterPage {
register: {
tag: "cloud-register",
load: () =>
import(/* webpackChunkName: "cloud-register" */ "./register/cloud-register"),
import(
/* webpackChunkName: "cloud-register" */ "./register/cloud-register"
),
},
"forgot-password": {
tag: "cloud-forgot-password",
load: () =>
import(/* webpackChunkName: "cloud-forgot-password" */ "./forgot-password/cloud-forgot-password"),
import(
/* webpackChunkName: "cloud-forgot-password" */ "./forgot-password/cloud-forgot-password"
),
},
account: {
tag: "cloud-account",
@ -59,7 +63,9 @@ class HaConfigCloud extends HassRouterPage {
"google-assistant": {
tag: "cloud-google-assistant",
load: () =>
import(/* webpackChunkName: "cloud-google-assistant" */ "./google-assistant/cloud-google-assistant"),
import(
/* webpackChunkName: "cloud-google-assistant" */ "./google-assistant/cloud-google-assistant"
),
},
alexa: {
tag: "cloud-alexa",

View File

@ -138,6 +138,9 @@ class HaConfigDashboard extends LitElement {
return [
haStyle,
css`
ha-config-navigation:last-child {
margin-bottom: 24px;
}
ha-card {
overflow: hidden;
}
@ -148,6 +151,7 @@ class HaConfigDashboard extends LitElement {
.promo-advanced {
text-align: center;
color: var(--secondary-text-color);
margin-bottom: 24px;
}
.promo-advanced a {
color: var(--secondary-text-color);

View File

@ -6,7 +6,9 @@ export interface EntityRegistryDetailDialogParams {
}
export const loadEntityRegistryDetailDialog = () =>
import(/* webpackChunkName: "entity-registry-detail-dialog" */ "./dialog-entity-registry-detail");
import(
/* webpackChunkName: "entity-registry-detail-dialog" */ "./dialog-entity-registry-detail"
);
export const showEntityRegistryDetailDialog = (
element: HTMLElement,

View File

@ -31,82 +31,114 @@ class HaPanelConfig extends HassRouterPage {
area_registry: {
tag: "ha-config-area-registry",
load: () =>
import(/* webpackChunkName: "panel-config-area-registry" */ "./area_registry/ha-config-area-registry"),
import(
/* webpackChunkName: "panel-config-area-registry" */ "./area_registry/ha-config-area-registry"
),
},
automation: {
tag: "ha-config-automation",
load: () =>
import(/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation"),
import(
/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation"
),
},
cloud: {
tag: "ha-config-cloud",
load: () =>
import(/* webpackChunkName: "panel-config-cloud" */ "./cloud/ha-config-cloud"),
import(
/* webpackChunkName: "panel-config-cloud" */ "./cloud/ha-config-cloud"
),
},
core: {
tag: "ha-config-core",
load: () =>
import(/* webpackChunkName: "panel-config-core" */ "./core/ha-config-core"),
import(
/* webpackChunkName: "panel-config-core" */ "./core/ha-config-core"
),
},
devices: {
tag: "ha-config-devices",
load: () =>
import(/* webpackChunkName: "panel-config-devices" */ "./devices/ha-config-devices"),
import(
/* webpackChunkName: "panel-config-devices" */ "./devices/ha-config-devices"
),
},
server_control: {
tag: "ha-config-server-control",
load: () =>
import(/* webpackChunkName: "panel-config-server-control" */ "./server_control/ha-config-server-control"),
import(
/* webpackChunkName: "panel-config-server-control" */ "./server_control/ha-config-server-control"
),
},
customize: {
tag: "ha-config-customize",
load: () =>
import(/* webpackChunkName: "panel-config-customize" */ "./customize/ha-config-customize"),
import(
/* webpackChunkName: "panel-config-customize" */ "./customize/ha-config-customize"
),
},
dashboard: {
tag: "ha-config-dashboard",
load: () =>
import(/* webpackChunkName: "panel-config-dashboard" */ "./dashboard/ha-config-dashboard"),
import(
/* webpackChunkName: "panel-config-dashboard" */ "./dashboard/ha-config-dashboard"
),
},
entity_registry: {
tag: "ha-config-entity-registry",
load: () =>
import(/* webpackChunkName: "panel-config-entity-registry" */ "./entity_registry/ha-config-entity-registry"),
import(
/* webpackChunkName: "panel-config-entity-registry" */ "./entity_registry/ha-config-entity-registry"
),
},
integrations: {
tag: "ha-config-integrations",
load: () =>
import(/* webpackChunkName: "panel-config-integrations" */ "./integrations/ha-config-integrations"),
import(
/* webpackChunkName: "panel-config-integrations" */ "./integrations/ha-config-integrations"
),
},
person: {
tag: "ha-config-person",
load: () =>
import(/* webpackChunkName: "panel-config-person" */ "./person/ha-config-person"),
import(
/* webpackChunkName: "panel-config-person" */ "./person/ha-config-person"
),
},
script: {
tag: "ha-config-script",
load: () =>
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script"),
import(
/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script"
),
},
scene: {
tag: "ha-config-scene",
load: () =>
import(/* webpackChunkName: "panel-config-scene" */ "./scene/ha-config-scene"),
import(
/* webpackChunkName: "panel-config-scene" */ "./scene/ha-config-scene"
),
},
users: {
tag: "ha-config-users",
load: () =>
import(/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users"),
import(
/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users"
),
},
zha: {
tag: "zha-config-panel",
load: () =>
import(/* webpackChunkName: "panel-config-zha" */ "./zha/zha-config-panel"),
import(
/* webpackChunkName: "panel-config-zha" */ "./zha/zha-config-panel"
),
},
zwave: {
tag: "ha-config-zwave",
load: () =>
import(/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave"),
import(
/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave"
),
},
},
};

View File

@ -133,7 +133,11 @@ export class HaConfigManagerDashboard extends LitElement {
)}
</div>
</paper-item-body>
<ha-icon-next></ha-icon-next>
<ha-icon-next
aria-label=${this.hass.localize(
"ui.panel.config.integrations.details"
)}
></ha-icon-next>
</paper-item>
</a>
`
@ -154,6 +158,7 @@ export class HaConfigManagerDashboard extends LitElement {
<ha-fab
icon="hass:plus"
aria-label=${this.hass.localize("ui.panel.config.integrations.new")}
title=${this.hass.localize("ui.panel.config.integrations.new")}
@click=${this._createFlow}
?rtl=${computeRTL(this.hass!)}

View File

@ -0,0 +1,23 @@
import { h, Component, ComponentChild } from "preact";
export class AutomationComponent<P = {}, S = {}> extends Component<P, S> {
// @ts-ignore
protected initialized: boolean;
constructor(props?, context?) {
super(props, context);
this.initialized = false;
}
public componentDidMount() {
this.initialized = true;
}
public componentWillUnmount() {
this.initialized = false;
}
public render(_props?, _state?, _context?: any): ComponentChild {
return <div />;
}
}

View File

@ -5,7 +5,8 @@ import "../ha-config-section";
import "../../../components/ha-card";
import "../../../components/ha-textarea";
import Trigger from "./trigger/index";
import "../automation/trigger/ha-automation-trigger";
import Condition from "./condition/index";
import Script from "./script/index";
@ -26,8 +27,8 @@ export default class Automation extends Component<any> {
});
}
public triggerChanged(trigger) {
this.props.onChange({ ...this.props.automation, trigger });
public triggerChanged(ev: CustomEvent) {
this.props.onChange({ ...this.props.automation, trigger: ev.detail.value });
}
public conditionChanged(condition) {
@ -90,11 +91,10 @@ export default class Automation extends Component<any> {
)}
</a>
</span>
<Trigger
trigger={trigger}
onChange={this.triggerChanged}
<ha-automation-trigger
triggers={trigger}
onvalue-changed={this.triggerChanged}
hass={hass}
localize={localize}
/>
</ha-config-section>

View File

@ -3,6 +3,8 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-item/paper-item";
import YAMLTextArea from "../yaml_textarea";
import DeviceCondition from "./device";
import LogicalCondition from "./logical";
import NumericStateCondition from "./numeric_state";
@ -31,6 +33,7 @@ export default class ConditionEdit extends Component<any> {
super();
this.typeChanged = this.typeChanged.bind(this);
this.onYamlChange = this.onYamlChange.bind(this);
}
public typeChanged(ev) {
@ -44,20 +47,24 @@ export default class ConditionEdit extends Component<any> {
}
}
public render({ index, condition, onChange, hass, localize }) {
public render({ index, condition, onChange, hass, localize, yamlMode }) {
// tslint:disable-next-line: variable-name
const Comp = TYPES[condition.condition];
const selected = OPTIONS.indexOf(condition.condition);
if (!Comp) {
if (yamlMode || !Comp) {
return (
<div>
{localize(
"ui.panel.config.automation.editor.conditions.unsupported_condition",
"condition",
condition.condition
<div style="margin-right: 24px;">
{!Comp && (
<div>
{localize(
"ui.panel.config.automation.editor.conditions.unsupported_condition",
"condition",
condition.condition
)}
</div>
)}
<pre>{JSON.stringify(condition, null, 2)}</pre>
<YAMLTextArea value={condition} onChange={this.onYamlChange} />
</div>
);
}
@ -94,4 +101,8 @@ export default class ConditionEdit extends Component<any> {
</div>
);
}
private onYamlChange(condition) {
this.props.onChange(this.props.index, condition);
}
}

View File

@ -8,10 +8,16 @@ import "../../../../components/ha-card";
import ConditionEdit from "./condition_edit";
export default class ConditionRow extends Component<any> {
public state: { yamlMode: boolean };
constructor() {
super();
this.state = {
yamlMode: false,
};
this.onDelete = this.onDelete.bind(this);
this.switchYamlMode = this.switchYamlMode.bind(this);
}
public onDelete() {
@ -27,22 +33,32 @@ export default class ConditionRow extends Component<any> {
}
}
public render(props) {
public render(props, { yamlMode }) {
return (
<ha-card>
<div class="card-content">
<div class="card-menu">
<div class="card-menu" style="z-index: 3">
<paper-menu-button
no-animations
horizontal-align="right"
horizontal-offset="-5"
vertical-offset="-5"
close-on-activate
>
<paper-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
/>
<paper-listbox slot="dropdown-content">
<paper-item onTap={this.switchYamlMode}>
{yamlMode
? props.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: props.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</paper-item>
<paper-item disabled>
{props.localize(
"ui.panel.config.automation.editor.conditions.duplicate"
@ -56,9 +72,15 @@ export default class ConditionRow extends Component<any> {
</paper-listbox>
</paper-menu-button>
</div>
<ConditionEdit {...props} />
<ConditionEdit {...props} yamlMode={yamlMode} />
</div>
</ha-card>
);
}
private switchYamlMode() {
this.setState({
yamlMode: !this.state.yamlMode,
});
}
}

View File

@ -1,4 +1,4 @@
import { h, Component } from "preact";
import { h } from "preact";
import "../../../../components/device/ha-device-picker";
import "../../../../components/device/ha-device-condition-picker";
@ -8,7 +8,10 @@ import {
fetchDeviceConditionCapabilities,
deviceAutomationsEqual,
} from "../../../../data/device_automation";
export default class DeviceCondition extends Component<any, any> {
import { AutomationComponent } from "../automation-component";
export default class DeviceCondition extends AutomationComponent<any, any> {
private _origCondition;
constructor() {
@ -20,10 +23,16 @@ export default class DeviceCondition extends Component<any, any> {
}
public devicePicked(ev) {
if (!this.initialized) {
return;
}
this.setState({ ...this.state, device_id: ev.target.value });
}
public deviceConditionPicked(ev) {
if (!this.initialized) {
return;
}
let condition = ev.target.value;
if (
this._origCondition &&
@ -74,6 +83,7 @@ export default class DeviceCondition extends Component<any, any> {
}
public componentDidMount() {
this.initialized = true;
if (!this.state.capabilities) {
this._getCapabilities();
}
@ -98,6 +108,9 @@ export default class DeviceCondition extends Component<any, any> {
}
private _extraFieldsChanged(ev) {
if (!this.initialized) {
return;
}
this.props.onChange(this.props.index, {
...this.props.condition,
...ev.detail.value,
@ -108,9 +121,7 @@ export default class DeviceCondition extends Component<any, any> {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
localize(
`ui.panel.config.automation.editor.condition.type.device.extra_fields.${
schema.name
}`
`ui.panel.config.automation.editor.condition.type.device.extra_fields.${schema.name}`
) || schema.name;
}
}

View File

@ -1,29 +1,22 @@
import { h, Component } from "preact";
import { h } from "preact";
import Condition from "./index";
import { AutomationComponent } from "../automation-component";
export default class LogicalCondition extends Component<any, any> {
private _mounted = false;
export default class LogicalCondition extends AutomationComponent<any> {
constructor() {
super();
this.conditionChanged = this.conditionChanged.bind(this);
}
public conditionChanged(conditions) {
if (this._mounted) {
this.props.onChange(this.props.index, {
...this.props.condition,
conditions,
});
if (!this.initialized) {
return;
}
}
public componentWillMount() {
this._mounted = true;
}
public componentWillUnmount() {
this._mounted = false;
this.props.onChange(this.props.index, {
...this.props.condition,
conditions,
});
}
/* eslint-disable camelcase */

View File

@ -1,11 +1,12 @@
import { h, Component } from "preact";
import { h } from "preact";
import "@polymer/paper-input/paper-input";
import "../../../../components/ha-textarea";
import "../../../../components/entity/ha-entity-picker";
import { onChangeEvent } from "../../../../common/preact/event";
import { AutomationComponent } from "../automation-component";
export default class NumericStateCondition extends Component<any> {
export default class NumericStateCondition extends AutomationComponent<any> {
private onChange: (obj: any) => void;
constructor() {
super();
@ -15,6 +16,9 @@ export default class NumericStateCondition extends Component<any> {
}
public entityPicked(ev) {
if (!this.initialized) {
return;
}
this.props.onChange(this.props.index, {
...this.props.condition,
entity_id: ev.target.value,

View File

@ -1,10 +1,11 @@
import { h, Component } from "preact";
import { h } from "preact";
import "@polymer/paper-input/paper-input";
import "../../../../components/entity/ha-entity-picker";
import { onChangeEvent } from "../../../../common/preact/event";
import { AutomationComponent } from "../automation-component";
export default class StateCondition extends Component<any> {
export default class StateCondition extends AutomationComponent<any> {
private onChange: (obj: any) => void;
constructor() {
super();
@ -14,6 +15,9 @@ export default class StateCondition extends Component<any> {
}
public entityPicked(ev) {
if (!this.initialized) {
return;
}
this.props.onChange(this.props.index, {
...this.props.condition,
entity_id: ev.target.value,

Some files were not shown because too many files have changed in this diff Show More