diff --git a/package.json b/package.json
index 4577c34aed..1d604cb278 100644
--- a/package.json
+++ b/package.json
@@ -107,6 +107,7 @@
"@gfx/zopfli": "^1.0.9",
"@types/chai": "^4.1.7",
"@types/codemirror": "^0.0.71",
+ "@types/leaflet": "^1.4.3",
"@types/memoize-one": "^4.1.0",
"@types/mocha": "^5.2.5",
"babel-eslint": "^10",
diff --git a/setup.py b/setup.py
index 2a9f4977d5..cfa3788d19 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
- version="20190220.0",
+ version="20190227.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
diff --git a/src/auth/ha-auth-flow.js b/src/auth/ha-auth-flow.js
index 100b76beab..e07774c90b 100644
--- a/src/auth/ha-auth-flow.js
+++ b/src/auth/ha-auth-flow.js
@@ -94,7 +94,7 @@ class HaAuthFlow extends localizeLiteMixin(PolymerElement) {
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
- this._handleSubmit();
+ this._handleSubmit(ev);
}
});
}
@@ -205,7 +205,8 @@ class HaAuthFlow extends localizeLiteMixin(PolymerElement) {
);
}
- async _handleSubmit() {
+ async _handleSubmit(ev) {
+ ev.preventDefault();
if (this._step.type !== "form") {
this._providerChanged(this.authProvider, null);
return;
diff --git a/src/common/dom/setup-leaflet-map.ts b/src/common/dom/setup-leaflet-map.ts
index b8f7922114..75d7311e3c 100644
--- a/src/common/dom/setup-leaflet-map.ts
+++ b/src/common/dom/setup-leaflet-map.ts
@@ -1,8 +1,13 @@
+import { Map } from "leaflet";
+
// Sets up a Leaflet map on the provided DOM element
-export const setupLeafletMap = async (mapElement) => {
+export type LeafletModuleType = typeof import("leaflet");
+
+export const setupLeafletMap = async (
+ mapElement
+): Promise<[Map, LeafletModuleType]> => {
// tslint:disable-next-line
- const Leaflet = (await import(/* webpackChunkName: "leaflet" */ "leaflet"))
- .default;
+ const Leaflet = (await import(/* webpackChunkName: "leaflet" */ "leaflet")) as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
const map = Leaflet.map(mapElement);
diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts
index b8cf388d5c..5222f79adf 100644
--- a/src/components/entity/ha-state-label-badge.ts
+++ b/src/components/entity/ha-state-label-badge.ts
@@ -143,6 +143,7 @@ export class HaStateLabelBadge extends LitElement {
case "binary_sensor":
case "device_tracker":
case "updater":
+ case "person":
return stateIcon(state);
case "sun":
return state.state === "above_horizon"
@@ -158,11 +159,11 @@ export class HaStateLabelBadge extends LitElement {
private _computeLabel(domain, state, _timerTimeRemaining) {
if (
state.state === "unavailable" ||
- ["device_tracker", "alarm_control_panel"].includes(domain)
+ ["device_tracker", "alarm_control_panel", "person"].includes(domain)
) {
// Localize the state with a special state_badge namespace, which has variations of
// the state translations that are truncated to fit within the badge label. Translations
- // are only added for device_tracker and alarm_control_panel.
+ // are only added for device_tracker, alarm_control_panel and person.
return (
this.hass!.localize(`state_badge.${domain}.${state.state}`) ||
this.hass!.localize(`state_badge.default.${state.state}`) ||
diff --git a/src/components/ha-card.ts b/src/components/ha-card.ts
index 917ec69f8d..3cc329a87f 100644
--- a/src/components/ha-card.ts
+++ b/src/components/ha-card.ts
@@ -18,8 +18,12 @@ class HaCard extends LitElement {
var(--paper-card-background-color, white)
);
border-radius: var(--ha-card-border-radius, 2px);
- box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
- 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
+ box-shadow: var(
+ --ha-card-box-shadow,
+ 0 2px 2px 0 rgba(0, 0, 0, 0.14),
+ 0 1px 5px 0 rgba(0, 0, 0, 0.12),
+ 0 3px 1px -2px rgba(0, 0, 0, 0.2)
+ );
color: var(--primary-text-color);
display: block;
transition: all 0.3s ease-out;
diff --git a/src/components/ha-cards.js b/src/components/ha-cards.js
index c5ecbac486..1e94047d80 100644
--- a/src/components/ha-cards.js
+++ b/src/components/ha-cards.js
@@ -36,12 +36,13 @@ const PRIORITY = {
// badges have priority >= 0
updater: 0,
sun: 1,
- device_tracker: 2,
- alarm_control_panel: 3,
- timer: 4,
- sensor: 5,
- binary_sensor: 6,
- mailbox: 7,
+ person: 2,
+ device_tracker: 3,
+ alarm_control_panel: 4,
+ timer: 5,
+ sensor: 6,
+ binary_sensor: 7,
+ mailbox: 8,
};
const getPriority = (domain) => (domain in PRIORITY ? PRIORITY[domain] : 100);
diff --git a/src/components/ha-color-picker.js b/src/components/ha-color-picker.js
index 04f05866a8..0fd47626f8 100644
--- a/src/components/ha-color-picker.js
+++ b/src/components/ha-color-picker.js
@@ -140,6 +140,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
hueSegments: {
type: Number,
value: 0,
+ observer: "segmentationChange",
},
// the amount segments for the hue
@@ -149,6 +150,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
saturationSegments: {
type: Number,
value: 0,
+ observer: "segmentationChange",
},
// set to true to make the segments purely esthetical
@@ -590,5 +592,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
this.tooltip = svgElement.tooltip;
svgElement.appendChild(svgElement.tooltip);
}
+
+ segmentationChange() {
+ if (this.backgroundLayer) {
+ this.drawColorWheel();
+ }
+ }
}
customElements.define("ha-color-picker", HaColorPicker);
diff --git a/src/components/ha-menu-button.js b/src/components/ha-menu-button.js
deleted file mode 100644
index 80c959f541..0000000000
--- a/src/components/ha-menu-button.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import "@polymer/paper-icon-button/paper-icon-button";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import EventsMixin from "../mixins/events-mixin";
-
-/*
- * @appliesMixin EventsMixin
- */
-class HaMenuButton extends EventsMixin(PolymerElement) {
- static get template() {
- return html`
-
- `;
- }
-
- static get properties() {
- return {
- narrow: {
- type: Boolean,
- value: false,
- },
-
- showMenu: {
- type: Boolean,
- value: false,
- },
-
- hassio: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- toggleMenu(ev) {
- ev.stopPropagation();
- this.fire(this.showMenu ? "hass-close-menu" : "hass-open-menu");
- }
-
- _getIcon(hassio) {
- // hass:menu
- return `${hassio ? "hassio" : "hass"}:menu`;
- }
-}
-
-customElements.define("ha-menu-button", HaMenuButton);
diff --git a/src/components/ha-menu-button.ts b/src/components/ha-menu-button.ts
new file mode 100644
index 0000000000..199d5228ac
--- /dev/null
+++ b/src/components/ha-menu-button.ts
@@ -0,0 +1,44 @@
+import "@polymer/paper-icon-button/paper-icon-button";
+import {
+ property,
+ TemplateResult,
+ LitElement,
+ html,
+ customElement,
+} from "lit-element";
+
+import { fireEvent } from "../common/dom/fire_event";
+
+@customElement("ha-menu-button")
+class HaMenuButton extends LitElement {
+ @property({ type: Boolean })
+ public showMenu = false;
+
+ @property({ type: Boolean })
+ public hassio = false;
+
+ protected render(): TemplateResult | void {
+ return html`
+
+ `;
+ }
+
+ // We are not going to use ShadowDOM as we're rendering a single element
+ // without any CSS used.
+ protected createRenderRoot(): Element | ShadowRoot {
+ return this;
+ }
+
+ private _toggleMenu(): void {
+ fireEvent(this, this.showMenu ? "hass-close-menu" : "hass-open-menu");
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-menu-button": HaMenuButton;
+ }
+}
diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts
new file mode 100644
index 0000000000..2fb4f019a3
--- /dev/null
+++ b/src/data/config_entries.ts
@@ -0,0 +1,63 @@
+import { HomeAssistant } from "../types";
+
+export interface FieldSchema {
+ name: string;
+ default?: any;
+ optional: boolean;
+}
+
+export interface ConfigFlowStepForm {
+ type: "form";
+ flow_id: string;
+ handler: string;
+ step_id: string;
+ data_schema: FieldSchema[];
+ errors: { [key: string]: string };
+ description_placeholders: { [key: string]: string };
+}
+
+export interface ConfigFlowStepCreateEntry {
+ type: "create_entry";
+ version: number;
+ flow_id: string;
+ handler: string;
+ title: string;
+ data: any;
+ description: string;
+ description_placeholders: { [key: string]: string };
+}
+
+export interface ConfigFlowStepAbort {
+ type: "abort";
+ flow_id: string;
+ handler: string;
+ reason: string;
+ description_placeholders: { [key: string]: string };
+}
+
+export type ConfigFlowStep =
+ | ConfigFlowStepForm
+ | ConfigFlowStepCreateEntry
+ | ConfigFlowStepAbort;
+
+export const createConfigFlow = (hass: HomeAssistant, handler: string) =>
+ hass.callApi("POST", "config/config_entries/flow", {
+ handler,
+ });
+
+export const fetchConfigFlow = (hass: HomeAssistant, flowId: string) =>
+ hass.callApi("GET", `config/config_entries/flow/${flowId}`);
+
+export const handleConfigFlowStep = (
+ hass: HomeAssistant,
+ flowId: string,
+ data: { [key: string]: any }
+) =>
+ hass.callApi(
+ "POST",
+ `config/config_entries/flow/${flowId}`,
+ data
+ );
+
+export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
+ hass.callApi("DELETE", `config/config_entries/flow/${flowId}`);
diff --git a/src/data/input_text.ts b/src/data/input_text.ts
index a8ed653111..04ee6c334c 100644
--- a/src/data/input_text.ts
+++ b/src/data/input_text.ts
@@ -1,7 +1,7 @@
import { HomeAssistant } from "../types";
export const setValue = (hass: HomeAssistant, entity: string, value: string) =>
- hass.callService("input_text", "set_value", {
+ hass.callService(entity.split(".", 1)[0], "set_value", {
value,
entity_id: entity,
});
diff --git a/src/dialogs/config-flow/dialog-config-flow.ts b/src/dialogs/config-flow/dialog-config-flow.ts
new file mode 100644
index 0000000000..c72abf0949
--- /dev/null
+++ b/src/dialogs/config-flow/dialog-config-flow.ts
@@ -0,0 +1,378 @@
+import {
+ LitElement,
+ TemplateResult,
+ html,
+ CSSResultArray,
+ css,
+ customElement,
+ property,
+ PropertyValues,
+} from "lit-element";
+import "@material/mwc-button";
+import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
+import "@polymer/paper-tooltip/paper-tooltip";
+import "@polymer/paper-spinner/paper-spinner";
+import "@polymer/paper-dialog/paper-dialog";
+// Not duplicate, is for typing
+// tslint:disable-next-line
+import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
+
+import "../../components/ha-form";
+import "../../components/ha-markdown";
+import "../../resources/ha-style";
+import { haStyleDialog } from "../../resources/styles";
+import {
+ fetchConfigFlow,
+ createConfigFlow,
+ ConfigFlowStep,
+ handleConfigFlowStep,
+ deleteConfigFlow,
+ FieldSchema,
+ ConfigFlowStepForm,
+} from "../../data/config_entries";
+import { PolymerChangedEvent, applyPolymerEvent } from "../../polymer-types";
+import { HaConfigFlowParams } from "./show-dialog-config-flow";
+
+let instance = 0;
+
+@customElement("dialog-config-flow")
+class ConfigFlowDialog extends LitElement {
+ @property()
+ private _params?: HaConfigFlowParams;
+
+ @property()
+ private _loading = true;
+
+ private _instance = instance;
+
+ @property()
+ private _step?: ConfigFlowStep;
+
+ @property()
+ private _stepData?: { [key: string]: any };
+
+ @property()
+ private _errorMsg?: string;
+
+ public async showDialog(params: HaConfigFlowParams): Promise {
+ this._params = params;
+ this._loading = true;
+ this._instance = instance++;
+ this._step = undefined;
+ this._stepData = {};
+ this._errorMsg = undefined;
+
+ const fetchStep = params.continueFlowId
+ ? fetchConfigFlow(params.hass, params.continueFlowId)
+ : params.newFlowForHandler
+ ? createConfigFlow(params.hass, params.newFlowForHandler)
+ : undefined;
+
+ if (!fetchStep) {
+ throw new Error(`Pass in either continueFlowId or newFlorForHandler`);
+ }
+
+ const curInstance = this._instance;
+
+ await this.updateComplete;
+ const step = await fetchStep;
+
+ // Happens if second showDialog called
+ if (curInstance !== this._instance) {
+ return;
+ }
+
+ this._processStep(step);
+ this._loading = false;
+ // When the flow changes, center the dialog.
+ // Don't do it on each step or else the dialog keeps bouncing.
+ setTimeout(() => this._dialog.center(), 0);
+ }
+
+ protected render(): TemplateResult | void {
+ if (!this._params) {
+ return html``;
+ }
+ const localize = this._params.hass.localize;
+
+ const step = this._step;
+ let headerContent: string | undefined;
+ let bodyContent: TemplateResult | undefined;
+ let buttonContent: TemplateResult | undefined;
+ let descriptionKey: string | undefined;
+
+ if (!step) {
+ bodyContent = html`
+
+ `;
+ } else if (step.type === "abort") {
+ descriptionKey = `component.${step.handler}.config.abort.${step.reason}`;
+ headerContent = "Aborted";
+ bodyContent = html``;
+ buttonContent = html`
+ Close
+ `;
+ } else if (step.type === "create_entry") {
+ descriptionKey = `component.${
+ step.handler
+ }.config.create_entry.${step.description || "default"}`;
+ headerContent = "Success!";
+ bodyContent = html`
+ Created config for ${step.title}
+ `;
+ buttonContent = html`
+ Close
+ `;
+ } else {
+ // form
+ descriptionKey = `component.${step.handler}.config.step.${
+ step.step_id
+ }.description`;
+ headerContent = localize(
+ `component.${step.handler}.config.step.${step.step_id}.title`
+ );
+ bodyContent = html`
+
+ `;
+
+ const allRequiredInfoFilledIn =
+ this._stepData &&
+ step.data_schema.every(
+ (field) =>
+ field.optional ||
+ !["", undefined].includes(this._stepData![field.name])
+ );
+
+ buttonContent = this._loading
+ ? html`
+
+ `
+ : html`
+
+
+ Submit
+
+
+ ${!allRequiredInfoFilledIn
+ ? html`
+
+ Not all required fields are filled in.
+
+ `
+ : html``}
+
+ `;
+ }
+
+ let description: string | undefined;
+
+ if (step && descriptionKey) {
+ const args: [string, ...string[]] = [descriptionKey];
+ const placeholders = step.description_placeholders || {};
+ Object.keys(placeholders).forEach((key) => {
+ args.push(key);
+ args.push(placeholders[key]);
+ });
+ description = localize(...args);
+ }
+
+ return html`
+
+
+ ${headerContent}
+
+
+ ${this._errorMsg
+ ? html`
+ ${this._errorMsg}
+ `
+ : ""}
+ ${description
+ ? html`
+
+ `
+ : ""}
+ ${bodyContent}
+
+
+ ${buttonContent}
+
+
+ `;
+ }
+
+ protected firstUpdated(changedProps: PropertyValues) {
+ super.firstUpdated(changedProps);
+ this.addEventListener("keypress", (ev) => {
+ if (ev.keyCode === 13) {
+ this._submitStep();
+ }
+ });
+ }
+
+ private get _dialog(): PaperDialogElement {
+ return this.shadowRoot!.querySelector("paper-dialog")!;
+ }
+
+ private async _submitStep(): Promise {
+ this._loading = true;
+ this._errorMsg = undefined;
+
+ const curInstance = this._instance;
+ const stepData = this._stepData || {};
+
+ const toSendData = {};
+ Object.keys(stepData).forEach((key) => {
+ const value = stepData[key];
+ const isEmpty = [undefined, ""].includes(value);
+
+ if (!isEmpty) {
+ toSendData[key] = value;
+ }
+ });
+
+ try {
+ const step = await handleConfigFlowStep(
+ this._params!.hass,
+ this._step!.flow_id,
+ toSendData
+ );
+
+ if (curInstance !== this._instance) {
+ return;
+ }
+
+ this._processStep(step);
+ } catch (err) {
+ this._errorMsg =
+ (err && err.body && err.body.message) || "Unknown error occurred";
+ } finally {
+ this._loading = false;
+ }
+ }
+
+ private _processStep(step: ConfigFlowStep): void {
+ this._step = step;
+
+ // We got a new form if there are no errors.
+ if (step.type === "form") {
+ if (!step.errors) {
+ step.errors = {};
+ }
+
+ if (Object.keys(step.errors).length === 0) {
+ const data = {};
+ step.data_schema.forEach((field) => {
+ if ("default" in field) {
+ data[field.name] = field.default;
+ }
+ });
+ this._stepData = data;
+ }
+ }
+ }
+
+ private _flowDone(): void {
+ if (!this._params) {
+ return;
+ }
+ const flowFinished = Boolean(
+ this._step && ["success", "abort"].includes(this._step.type)
+ );
+
+ // If we created this flow, delete it now.
+ if (this._step && !flowFinished && this._params.newFlowForHandler) {
+ deleteConfigFlow(this._params.hass, this._step.flow_id);
+ }
+
+ this._params.dialogClosedCallback({
+ flowFinished,
+ });
+
+ this._errorMsg = undefined;
+ this._step = undefined;
+ this._stepData = {};
+ this._params = undefined;
+ }
+
+ private _openedChanged(ev: PolymerChangedEvent): void {
+ // Closed dialog by clicking on the overlay
+ if (this._step && !ev.detail.value) {
+ this._flowDone();
+ }
+ }
+
+ private _stepDataChanged(ev: PolymerChangedEvent): void {
+ this._stepData = applyPolymerEvent(ev, this._stepData);
+ }
+
+ private _labelCallback = (schema: FieldSchema): string => {
+ const step = this._step as ConfigFlowStepForm;
+
+ return this._params!.hass.localize(
+ `component.${step.handler}.config.step.${step.step_id}.data.${
+ schema.name
+ }`
+ );
+ };
+
+ private _errorCallback = (error: string) =>
+ this._params!.hass.localize(
+ `component.${this._step!.handler}.config.error.${error}`
+ );
+
+ static get styles(): CSSResultArray {
+ return [
+ haStyleDialog,
+ css`
+ .error {
+ color: red;
+ }
+ paper-dialog {
+ max-width: 500px;
+ }
+ ha-markdown {
+ word-break: break-word;
+ }
+ ha-markdown a {
+ color: var(--primary-color);
+ }
+ ha-markdown img:first-child:last-child {
+ display: block;
+ margin: 0 auto;
+ }
+ .init-spinner {
+ padding: 10px 100px 34px;
+ text-align: center;
+ }
+ .submit-spinner {
+ margin-right: 16px;
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "dialog-config-flow": ConfigFlowDialog;
+ }
+}
diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts
new file mode 100644
index 0000000000..35f86f8c3e
--- /dev/null
+++ b/src/dialogs/config-flow/show-dialog-config-flow.ts
@@ -0,0 +1,23 @@
+import { HomeAssistant } from "../../types";
+import { fireEvent } from "../../common/dom/fire_event";
+
+export interface HaConfigFlowParams {
+ hass: HomeAssistant;
+ continueFlowId?: string;
+ newFlowForHandler?: string;
+ dialogClosedCallback: (params: { flowFinished: boolean }) => void;
+}
+
+export const loadConfigFlowDialog = () =>
+ import(/* webpackChunkName: "dialog-config-flow" */ "./dialog-config-flow");
+
+export const showConfigFlowDialog = (
+ element: HTMLElement,
+ dialogParams: HaConfigFlowParams
+): void => {
+ fireEvent(element, "show-dialog", {
+ dialogTag: "dialog-config-flow",
+ dialogImport: loadConfigFlowDialog,
+ dialogParams,
+ });
+};
diff --git a/src/dialogs/more-info/controls/more-info-light.js b/src/dialogs/more-info/controls/more-info-light.js
index 8fefea9d6c..99c1bf2c53 100644
--- a/src/dialogs/more-info/controls/more-info-light.js
+++ b/src/dialogs/more-info/controls/more-info-light.js
@@ -48,6 +48,11 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
--paper-slider-knob-start-border-color: var(--primary-color);
}
+ .segmentationContainer {
+ position: relative;
+ width: 100%;
+ }
+
ha-color-picker {
display: block;
width: 100%;
@@ -57,6 +62,29 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
transition: max-height 0.5s ease-in;
}
+ .segmentationButton {
+ position: absolute;
+ top: 11%;
+ transform: translate(0%, 0%);
+ padding: 0px;
+ max-height: 0px;
+ width: 23px;
+ height: 23px;
+ opacity: var(--dark-secondary-opacity);
+ overflow: hidden;
+ transition: max-height 0.5s ease-in;
+ }
+
+ .has-color.is-on .segmentationContainer .segmentationButton {
+ position: absolute;
+ top: 11%;
+ transform: translate(0%, 0%);
+ width: 23px;
+ height: 23px;
+ padding: 0px;
+ opacity: var(--dark-secondary-opacity);
+ }
+
.has-effect_list.is-on .effect_list,
.has-brightness .brightness,
.has-color_temp.is-on .color_temp,
@@ -75,6 +103,11 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
padding-top: 16px;
}
+ .has-color.is-on .segmentationButton {
+ max-height: 100px;
+ overflow: visible;
+ }
+
.has-color.is-on ha-color-picker {
max-height: 500px;
overflow: visible;
@@ -126,16 +159,22 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
on-change="wvSliderChanged"
>
-
-
-
+
@@ -65,44 +64,6 @@ export default (superClass) =>
}
try {
await callService(conn, domain, service, serviceData);
-
- const entityIds = Array.isArray(serviceData.entity_id)
- ? serviceData.entity_id
- : [serviceData.entity_id];
-
- const names = [];
- for (const entityId of entityIds) {
- const stateObj = this.hass.states[entityId];
- if (stateObj) {
- names.push(computeStateName(stateObj));
- }
- }
- if (names.length === 0) {
- names.push(entityIds[0]);
- }
-
- let message;
- const name = names.join(", ");
- if (service === "turn_on" && serviceData.entity_id) {
- message = this.hass.localize(
- "ui.notification_toast.entity_turned_on",
- "entity",
- name
- );
- } else if (service === "turn_off" && serviceData.entity_id) {
- message = this.hass.localize(
- "ui.notification_toast.entity_turned_off",
- "entity",
- name
- );
- } else {
- message = this.hass.localize(
- "ui.notification_toast.service_called",
- "service",
- `${domain}/${service}`
- );
- }
- this.fire("hass-notification", { message });
} catch (err) {
if (__DEV__) {
// eslint-disable-next-line
diff --git a/src/layouts/app/home-assistant.ts b/src/layouts/app/home-assistant.ts
index e7141e3b46..62529ef38b 100644
--- a/src/layouts/app/home-assistant.ts
+++ b/src/layouts/app/home-assistant.ts
@@ -17,6 +17,8 @@ import { dialogManagerMixin } from "./dialog-manager-mixin";
import ConnectionMixin from "./connection-mixin";
import NotificationMixin from "./notification-mixin";
import DisconnectToastMixin from "./disconnect-toast-mixin";
+import { urlSyncMixin } from "./url-sync-mixin";
+
import { Route, HomeAssistant } from "../../types";
import { navigate } from "../../common/navigate";
@@ -36,6 +38,7 @@ export class HomeAssistantAppEl extends ext(HassBaseMixin(LitElement), [
ConnectionMixin,
NotificationMixin,
dialogManagerMixin,
+ urlSyncMixin,
]) {
@property() private _route?: Route;
@property() private _error?: boolean;
diff --git a/src/layouts/app/more-info-mixin.ts b/src/layouts/app/more-info-mixin.ts
index c4a3d8fed5..66c442cf90 100644
--- a/src/layouts/app/more-info-mixin.ts
+++ b/src/layouts/app/more-info-mixin.ts
@@ -6,7 +6,7 @@ declare global {
// for fire event
interface HASSDomEvents {
"hass-more-info": {
- entityId: string;
+ entityId: string | null;
};
}
}
diff --git a/src/layouts/app/url-sync-mixin.ts b/src/layouts/app/url-sync-mixin.ts
new file mode 100644
index 0000000000..c6817760a6
--- /dev/null
+++ b/src/layouts/app/url-sync-mixin.ts
@@ -0,0 +1,90 @@
+import { Constructor, LitElement } from "lit-element";
+import { HassBaseEl } from "./hass-base-mixin";
+import { fireEvent } from "../../common/dom/fire_event";
+
+/* tslint:disable:no-console */
+const DEBUG = false;
+
+export const urlSyncMixin = (
+ superClass: Constructor
+) =>
+ // Disable this functionality in the demo.
+ __DEMO__
+ ? superClass
+ : class extends superClass {
+ private _ignoreNextHassChange = false;
+ private _ignoreNextPopstate = false;
+ private _moreInfoOpenedFromPath?: string;
+
+ public connectedCallback(): void {
+ super.connectedCallback();
+ window.addEventListener("popstate", this._popstateChangeListener);
+ }
+
+ public disconnectedCallback(): void {
+ super.disconnectedCallback();
+ window.removeEventListener("popstate", this._popstateChangeListener);
+ }
+
+ protected hassChanged(newHass, oldHass): void {
+ super.hassChanged(newHass, oldHass);
+
+ if (this._ignoreNextHassChange) {
+ if (DEBUG) {
+ console.log("ignore hasschange");
+ }
+ this._ignoreNextHassChange = false;
+ return;
+ }
+ if (
+ !oldHass ||
+ oldHass.moreInfoEntityId === newHass.moreInfoEntityId
+ ) {
+ if (DEBUG) {
+ console.log("ignoring hass change");
+ }
+ return;
+ }
+
+ if (newHass.moreInfoEntityId) {
+ if (DEBUG) {
+ console.log("pushing state");
+ }
+ // We keep track of where we opened moreInfo from so that we don't
+ // pop the state when we close the modal if the modal has navigated
+ // us away.
+ this._moreInfoOpenedFromPath = window.location.pathname;
+ history.pushState(null, "", window.location.pathname);
+ } else if (
+ window.location.pathname === this._moreInfoOpenedFromPath
+ ) {
+ if (DEBUG) {
+ console.log("history back");
+ }
+ this._ignoreNextPopstate = true;
+ history.back();
+ }
+ }
+
+ private _popstateChangeListener = (ev) => {
+ if (this._ignoreNextPopstate) {
+ if (DEBUG) {
+ console.log("ignore popstate");
+ }
+ this._ignoreNextPopstate = false;
+ return;
+ }
+
+ if (DEBUG) {
+ console.log("popstate", ev);
+ }
+
+ if (this.hass && this.hass.moreInfoEntityId) {
+ if (DEBUG) {
+ console.log("deselect entity");
+ }
+ this._ignoreNextHassChange = true;
+ fireEvent(this, "hass-more-info", { entityId: null });
+ }
+ };
+ };
diff --git a/src/layouts/hass-subpage.js b/src/layouts/hass-subpage.js
deleted file mode 100644
index 692a3a50b0..0000000000
--- a/src/layouts/hass-subpage.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import "@polymer/app-layout/app-header-layout/app-header-layout";
-import "@polymer/app-layout/app-header/app-header";
-import "@polymer/app-layout/app-toolbar/app-toolbar";
-import "@polymer/paper-icon-button/paper-icon-button";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import "../components/ha-paper-icon-button-arrow-prev";
-
-class HassSubpage extends PolymerElement {
- static get template() {
- return html`
-
-
-
-
-
- [[header]]
-
-
-
-
-
-
- `;
- }
-
- static get properties() {
- return {
- header: String,
- };
- }
-
- _backTapped() {
- history.back();
- }
-}
-
-customElements.define("hass-subpage", HassSubpage);
diff --git a/src/layouts/hass-subpage.ts b/src/layouts/hass-subpage.ts
index 8895df0e74..f318a7b47f 100644
--- a/src/layouts/hass-subpage.ts
+++ b/src/layouts/hass-subpage.ts
@@ -22,10 +22,9 @@ class HassSubpage extends LitElement {
-
+ >
${this.header}
diff --git a/src/layouts/home-assistant-main.ts b/src/layouts/home-assistant-main.ts
index 58679ff86a..656064920f 100644
--- a/src/layouts/home-assistant-main.ts
+++ b/src/layouts/home-assistant-main.ts
@@ -15,8 +15,6 @@ import { AppDrawerElement } from "@polymer/app-layout/app-drawer/app-drawer";
import "@polymer/app-route/app-route";
import "@polymer/iron-media-query/iron-media-query";
-import "../util/ha-url-sync";
-
import "./partial-panel-resolver";
import { HomeAssistant, Route } from "../types";
import { fireEvent } from "../common/dom/fire_event";
@@ -47,7 +45,6 @@ class HomeAssistantMain extends LitElement {
const disableSwipe = NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1;
return html`
-
-
+
+ ${this.hass.localize(
+ "ui.panel.config.area_registry.picker.integrations_page"
+ )}
+
${this._items.map((entry) => {
diff --git a/src/panels/config/automation/ha-automation-picker.js b/src/panels/config/automation/ha-automation-picker.js
index 348e7d4fef..a699599bbf 100644
--- a/src/panels/config/automation/ha-automation-picker.js
+++ b/src/panels/config/automation/ha-automation-picker.js
@@ -81,9 +81,11 @@ class HaAutomationPicker extends LocalizeMixin(NavigateMixin(PolymerElement)) {
- import(/* webpackChunkName: "ha-config-flow" */ "./ha-config-flow"),
- });
- }
+ loadConfigFlowDialog();
}
_createFlow(ev) {
- this.fire("show-config-flow", {
+ showConfigFlowDialog(this, {
hass: this.hass,
newFlowForHandler: ev.model.item,
dialogClosedCallback: () => this.fire("hass-reload-entries"),
@@ -186,7 +179,7 @@ class HaConfigManagerDashboard extends LocalizeMixin(
}
_continueFlow(ev) {
- this.fire("show-config-flow", {
+ showConfigFlowDialog(this, {
hass: this.hass,
continueFlowId: ev.model.item.flow_id,
dialogClosedCallback: () => this.fire("hass-reload-entries"),
diff --git a/src/panels/config/config-entries/ha-config-flow.js b/src/panels/config/config-entries/ha-config-flow.js
deleted file mode 100644
index fdb37c66ef..0000000000
--- a/src/panels/config/config-entries/ha-config-flow.js
+++ /dev/null
@@ -1,365 +0,0 @@
-import "@material/mwc-button";
-import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
-import "@polymer/paper-dialog/paper-dialog";
-import "@polymer/paper-tooltip/paper-tooltip";
-import "@polymer/paper-spinner/paper-spinner";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import "../../../components/ha-form";
-import "../../../components/ha-markdown";
-import "../../../resources/ha-style";
-
-import EventsMixin from "../../../mixins/events-mixin";
-import LocalizeMixin from "../../../mixins/localize-mixin";
-
-let instance = 0;
-
-/*
- * @appliesMixin LocalizeMixin
- * @appliesMixin EventsMixin
- */
-class HaConfigFlow extends LocalizeMixin(EventsMixin(PolymerElement)) {
- static get template() {
- return html`
-
-
-
-
- Aborted
-
-
- Success!
-
-
- [[_computeStepTitle(localize, _step)]]
-
-
-
-
- [[_errorMsg]]
-
-
-
-
-
-
- Created config for [[_step.title]]
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
- }
-
- static get properties() {
- return {
- _hass: Object,
- _dialogClosedCallback: Function,
- _instance: Number,
-
- _loading: {
- type: Boolean,
- value: false,
- },
-
- // Error message when can't talk to server etc
- _errorMsg: String,
-
- _canSubmit: {
- type: Boolean,
- computed: "_computeCanSubmit(_step, _stepData, _counter)",
- },
-
- // Bogus counter because observing of `_stepData` doesn't seem to work
- _counter: {
- type: Number,
- value: 0,
- },
-
- _opened: {
- type: Boolean,
- value: false,
- },
-
- _step: {
- type: Object,
- value: null,
- },
-
- /*
- * Store user entered data.
- */
- _stepData: {
- type: Object,
- value: null,
- },
- };
- }
-
- ready() {
- super.ready();
- this.addEventListener("keypress", (ev) => {
- if (ev.keyCode === 13) {
- this._submitStep();
- }
- });
- }
-
- showDialog({
- hass,
- continueFlowId,
- newFlowForHandler,
- dialogClosedCallback,
- }) {
- this.hass = hass;
- this._instance = instance++;
- this._dialogClosedCallback = dialogClosedCallback;
- this._createdFromHandler = !!newFlowForHandler;
- this._loading = true;
- this._opened = true;
-
- const fetchStep = continueFlowId
- ? this.hass.callApi("get", `config/config_entries/flow/${continueFlowId}`)
- : this.hass.callApi("post", "config/config_entries/flow", {
- handler: newFlowForHandler,
- });
-
- const curInstance = this._instance;
-
- fetchStep.then((step) => {
- if (curInstance !== this._instance) return;
-
- this._processStep(step);
- this._loading = false;
- // When the flow changes, center the dialog.
- // Don't do it on each step or else the dialog keeps bouncing.
- setTimeout(() => this.$.dialog.center(), 0);
- });
- }
-
- _submitStep() {
- this._loading = true;
- this._errorMsg = null;
-
- const curInstance = this._instance;
-
- const data = {};
- Object.keys(this._stepData).forEach((key) => {
- const value = this._stepData[key];
- const isEmpty = [undefined, ""].includes(value);
-
- if (!isEmpty) {
- data[key] = value;
- }
- });
-
- this.hass
- .callApi("post", `config/config_entries/flow/${this._step.flow_id}`, data)
- .then(
- (step) => {
- if (curInstance !== this._instance) return;
-
- this._processStep(step);
- this._loading = false;
- },
- (err) => {
- this._errorMsg =
- (err && err.body && err.body.message) || "Unknown error occurred";
- this._loading = false;
- }
- );
- }
-
- _processStep(step) {
- if (!step.errors) step.errors = {};
- this._step = step;
- // We got a new form if there are no errors.
- if (step.type === "form" && Object.keys(step.errors).length === 0) {
- const data = {};
- step.data_schema.forEach((field) => {
- if ("default" in field) {
- data[field.name] = field.default;
- }
- });
- this._stepData = data;
- }
- }
-
- _flowDone() {
- this._opened = false;
- const flowFinished =
- this._step && ["success", "abort"].includes(this._step.type);
-
- if (this._step && !flowFinished && this._createdFromHandler) {
- this.hass.callApi(
- "delete",
- `config/config_entries/flow/${this._step.flow_id}`
- );
- }
-
- this._dialogClosedCallback({
- flowFinished,
- });
-
- this._errorMsg = null;
- this._step = null;
- this._stepData = {};
- this._dialogClosedCallback = null;
- }
-
- _equals(a, b) {
- return a === b;
- }
-
- _openedChanged(ev) {
- // Closed dialog by clicking on the overlay
- if (this._step && !ev.detail.value) {
- this._flowDone();
- }
- }
-
- _computeStepTitle(localize, step) {
- return localize(
- `component.${step.handler}.config.step.${step.step_id}.title`
- );
- }
-
- _computeStepDescription(localize, step) {
- const args = [];
- if (step.type === "form") {
- args.push(
- `component.${step.handler}.config.step.${step.step_id}.description`
- );
- } else if (step.type === "abort") {
- args.push(`component.${step.handler}.config.abort.${step.reason}`);
- } else if (step.type === "create_entry") {
- args.push(
- `component.${step.handler}.config.create_entry.${step.description ||
- "default"}`
- );
- }
-
- const placeholders = step.description_placeholders || {};
- Object.keys(placeholders).forEach((key) => {
- args.push(key);
- args.push(placeholders[key]);
- });
-
- return localize(...args);
- }
-
- _computeLabelCallback(localize, step) {
- // Returns a callback for ha-form to calculate labels per schema object
- return (schema) =>
- localize(
- `component.${step.handler}.config.step.${step.step_id}.data.${
- schema.name
- }`
- );
- }
-
- _computeErrorCallback(localize, step) {
- // Returns a callback for ha-form to calculate error messages
- return (error) =>
- localize(`component.${step.handler}.config.error.${error}`);
- }
-
- _computeCanSubmit(step, stepData) {
- // We can submit if all required fields are filled in
- return (
- step !== null &&
- step.type === "form" &&
- stepData !== null &&
- step.data_schema.every(
- (field) =>
- field.optional || !["", undefined].includes(stepData[field.name])
- )
- );
- }
-
- _increaseCounter() {
- this._counter += 1;
- }
-}
-
-customElements.define("ha-config-flow", HaConfigFlow);
diff --git a/src/panels/config/entity_registry/ha-config-entity-registry.ts b/src/panels/config/entity_registry/ha-config-entity-registry.ts
index 016f03e995..8e5bd8b375 100644
--- a/src/panels/config/entity_registry/ha-config-entity-registry.ts
+++ b/src/panels/config/entity_registry/ha-config-entity-registry.ts
@@ -70,10 +70,12 @@ class HaConfigEntityRegistry extends LitElement {
${this.hass.localize(
"ui.panel.config.entity_registry.picker.introduction2"
)}
-
+
+ ${this.hass.localize(
+ "ui.panel.config.entity_registry.picker.integrations_page"
+ )}
+
${this._items.map((entry) => {
diff --git a/src/panels/config/js/automation.js b/src/panels/config/js/automation.js
index 5c7304c8f6..4f90040444 100644
--- a/src/panels/config/js/automation.js
+++ b/src/panels/config/js/automation.js
@@ -73,7 +73,9 @@ export default class Automation extends Component {
)}
-
+ {localize(
+ "ui.panel.config.automation.editor.triggers.learn_more"
+ )}
-
+ {localize(
+ "ui.panel.config.automation.editor.conditions.learn_more"
+ )}
-
+ {localize("ui.panel.config.automation.editor.actions.learn_more")}