diff --git a/package.json b/package.json index c0f9cb3149..25afbc9b9c 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "roboto-fontface": "^0.10.0", "round-slider": "^1.3.3", "superstruct": "^0.6.1", + "tslib": "^1.10.0", "unfetch": "^4.1.0", "web-animations-js": "^2.3.1", "xss": "^1.0.6" diff --git a/setup.py b/setup.py index ce90a45340..f2bf44985c 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20190828.0", + version="20190901.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/components/buttons/ha-call-service-button.js b/src/components/buttons/ha-call-service-button.js index ab61dc0444..909919c9a1 100644 --- a/src/components/buttons/ha-call-service-button.js +++ b/src/components/buttons/ha-call-service-button.js @@ -42,10 +42,17 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) { type: Object, value: {}, }, + + confirmation: { + type: String, + }, }; } buttonTapped() { + if (this.confirmation && !window.confirm(this.confirmation)) { + return; + } this.progress = true; var el = this; var eventData = { diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts new file mode 100644 index 0000000000..f9c64578a1 --- /dev/null +++ b/src/components/device/ha-device-picker.ts @@ -0,0 +1,119 @@ +import "@polymer/paper-input/paper-input"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; +import "@polymer/paper-listbox/paper-listbox"; +import memoizeOne from "memoize-one"; +import { + LitElement, + TemplateResult, + html, + css, + CSSResult, + customElement, + property, +} from "lit-element"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { HomeAssistant } from "../../types"; +import { fireEvent } from "../../common/dom/fire_event"; +import { + DeviceRegistryEntry, + subscribeDeviceRegistry, +} from "../../data/device_registry"; +import { compare } from "../../common/string/compare"; + +@customElement("ha-device-picker") +class HaDevicePicker extends LitElement { + public hass?: HomeAssistant; + @property() public label?: string; + @property() public value?: string; + @property() public devices?: DeviceRegistryEntry[]; + private _unsubDevices?: UnsubscribeFunc; + + private _sortedDevices = memoizeOne((devices?: DeviceRegistryEntry[]) => { + if (!devices || devices.length === 1) { + return devices || []; + } + const sorted = [...devices]; + sorted.sort((a, b) => compare(a.name || "", b.name || "")); + return sorted; + }); + + public connectedCallback() { + super.connectedCallback(); + this._unsubDevices = subscribeDeviceRegistry( + this.hass!.connection!, + (devices) => { + this.devices = devices; + } + ); + } + + public disconnectedCallback() { + super.disconnectedCallback(); + if (this._unsubDevices) { + this._unsubDevices(); + this._unsubDevices = undefined; + } + } + + protected render(): TemplateResult | void { + return html` + + + + No device + + ${this._sortedDevices(this.devices).map( + (device) => html` + + ${device.name_by_user || device.name} + + ` + )} + + + `; + } + + private get _value() { + return this.value || ""; + } + + private _deviceChanged(ev) { + const newValue = ev.detail.item.dataset.deviceId; + + if (newValue !== this._value) { + this.value = newValue; + setTimeout(() => { + fireEvent(this, "value-changed", { value: newValue }); + fireEvent(this, "change"); + }, 0); + } + } + + static get styles(): CSSResult { + return css` + paper-dropdown-menu-light { + width: 100%; + } + paper-listbox { + min-width: 200px; + } + paper-item { + cursor: pointer; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-device-picker": HaDevicePicker; + } +} diff --git a/src/components/device/ha-device-trigger-picker.ts b/src/components/device/ha-device-trigger-picker.ts new file mode 100644 index 0000000000..7ff3b6d48d --- /dev/null +++ b/src/components/device/ha-device-trigger-picker.ts @@ -0,0 +1,166 @@ +import "@polymer/paper-input/paper-input"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import "@polymer/paper-listbox/paper-listbox"; +import { + LitElement, + TemplateResult, + html, + css, + CSSResult, + customElement, + property, +} from "lit-element"; +import { HomeAssistant } from "../../types"; +import { fireEvent } from "../../common/dom/fire_event"; +import { + DeviceTrigger, + fetchDeviceTriggers, + deviceAutomationTriggersEqual, + localizeDeviceAutomationTrigger, +} from "../../data/device_automation"; +import "../../components/ha-paper-dropdown-menu"; + +const NO_TRIGGER_KEY = "NO_TRIGGER"; +const UNKNOWN_TRIGGER_KEY = "UNKNOWN_TRIGGER"; + +@customElement("ha-device-trigger-picker") +class HaDeviceTriggerPicker extends LitElement { + public hass!: HomeAssistant; + @property() public label?: string; + @property() public deviceId?: string; + @property() public value?: DeviceTrigger; + @property() private _triggers: DeviceTrigger[] = []; + + // Trigger an empty render so we start with a clean DOM. + // paper-listbox does not like changing things around. + @property() private _renderEmpty = false; + + private get _key() { + if (!this.value) { + return NO_TRIGGER_KEY; + } + + const idx = this._triggers.findIndex((trigger) => + deviceAutomationTriggersEqual(trigger, this.value!) + ); + + if (idx === -1) { + return UNKNOWN_TRIGGER_KEY; + } + + return `${this._triggers[idx].device_id}_${idx}`; + } + + protected render(): TemplateResult | void { + if (this._renderEmpty) { + return html``; + } + return html` + + + + + ${this._triggers.map( + (trigger, idx) => html` + + ${localizeDeviceAutomationTrigger(this.hass, trigger)} + + ` + )} + + + `; + } + + protected updated(changedProps) { + super.updated(changedProps); + + if (changedProps.has("deviceId")) { + this._updateDeviceInfo(); + } + + // The value has changed, force the listbox to update + if (changedProps.has("value") || changedProps.has("_renderEmpty")) { + const listbox = this.shadowRoot!.querySelector("paper-listbox")!; + if (listbox) { + listbox._selectSelected(this._key); + } + } + } + + private async _updateDeviceInfo() { + this._triggers = this.deviceId + ? await fetchDeviceTriggers(this.hass!, this.deviceId!) + : // No device, clear the list of triggers + []; + + // If there is no value, or if we have changed the device ID, reset the value. + if (!this.value || this.value.device_id !== this.deviceId) { + this._setValue( + this._triggers.length ? this._triggers[0] : this._noTrigger + ); + } + this._renderEmpty = true; + await this.updateComplete; + this._renderEmpty = false; + } + + private get _noTrigger() { + return { + device_id: this.deviceId || "", + platform: "device", + domain: "", + entity_id: "", + }; + } + + private _triggerChanged(ev) { + this._setValue(ev.detail.item.trigger); + } + + private _setValue(trigger: DeviceTrigger) { + if (this.value && deviceAutomationTriggersEqual(trigger, this.value)) { + return; + } + this.value = trigger; + setTimeout(() => { + fireEvent(this, "change"); + }, 0); + } + + static get styles(): CSSResult { + return css` + ha-paper-dropdown-menu { + width: 100%; + } + paper-listbox { + min-width: 200px; + } + paper-item { + cursor: pointer; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-device-trigger-picker": HaDeviceTriggerPicker; + } +} diff --git a/src/data/device_automation.ts b/src/data/device_automation.ts new file mode 100644 index 0000000000..d3c7370527 --- /dev/null +++ b/src/data/device_automation.ts @@ -0,0 +1,57 @@ +import { HomeAssistant } from "../types"; +import compute_state_name from "../common/entity/compute_state_name"; + +export interface DeviceTrigger { + platform: string; + device_id: string; + domain: string; + entity_id: string; + type?: string; + event?: string; +} + +export interface DeviceTriggerList { + triggers: DeviceTrigger[]; +} + +export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) => + hass + .callWS({ + type: "device_automation/trigger/list", + device_id: deviceId, + }) + .then((response) => response.triggers); + +export const deviceAutomationTriggersEqual = ( + a: DeviceTrigger, + b: DeviceTrigger +) => { + if (typeof a !== typeof b) { + return false; + } + + for (const property in a) { + if (!Object.is(a[property], b[property])) { + return false; + } + } + for (const property in b) { + if (!Object.is(a[property], b[property])) { + return false; + } + } + + return true; +}; + +export const localizeDeviceAutomationTrigger = ( + hass: HomeAssistant, + trigger: DeviceTrigger +) => + hass.localize( + `component.${trigger.domain}.device_automation.trigger_type.${ + trigger.type + }`, + "name", + trigger.entity_id ? compute_state_name(hass!.states[trigger.entity_id]) : "" + ); diff --git a/src/dialogs/more-info/controls/more-info-fan.js b/src/dialogs/more-info/controls/more-info-fan.js index 4df080e319..8bd690ae5b 100644 --- a/src/dialogs/more-info/controls/more-info-fan.js +++ b/src/dialogs/more-info/controls/more-info-fan.js @@ -81,15 +81,15 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
[[localize('ui.card.fan.direction')]]
@@ -164,25 +164,25 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) { }); } - onDirectionLeft() { + onDirectionReverse() { this.hass.callService("fan", "set_direction", { entity_id: this.stateObj.entity_id, direction: "reverse", }); } - onDirectionRight() { + onDirectionForward() { this.hass.callService("fan", "set_direction", { entity_id: this.stateObj.entity_id, direction: "forward", }); } - computeIsRotatingLeft(stateObj) { + computeIsRotatingReverse(stateObj) { return stateObj.attributes.direction === "reverse"; } - computeIsRotatingRight(stateObj) { + computeIsRotatingForward(stateObj) { return stateObj.attributes.direction === "forward"; } } diff --git a/src/dialogs/notifications/persistent-notification-item.ts b/src/dialogs/notifications/persistent-notification-item.ts index b40a743203..9ddb665718 100644 --- a/src/dialogs/notifications/persistent-notification-item.ts +++ b/src/dialogs/notifications/persistent-notification-item.ts @@ -73,6 +73,9 @@ export class HuiPersistentNotificationItem extends LitElement { a { color: var(--primary-color); } + ha-markdown { + overflow-wrap: break-word; + } `; } diff --git a/src/panels/config/integrations/ha-config-entries-dashboard.js b/src/panels/config/integrations/ha-config-entries-dashboard.js index d7c6113afb..0a2159a45a 100644 --- a/src/panels/config/integrations/ha-config-entries-dashboard.js +++ b/src/panels/config/integrations/ha-config-entries-dashboard.js @@ -126,10 +126,7 @@ class HaConfigManagerDashboard extends LocalizeMixin( items="[[_computeConfigEntryEntities(hass, item, entities)]]" > - + [[_computeStateName(item)]] @@ -230,10 +227,6 @@ class HaConfigManagerDashboard extends LocalizeMixin( return computeStateName(stateObj); } - _handleMoreInfo(ev) { - this.fire("hass-more-info", { entityId: ev.model.item.entity_id }); - } - _computeRTL(hass) { return computeRTL(hass); } diff --git a/src/panels/config/js/trigger/device.js b/src/panels/config/js/trigger/device.js new file mode 100644 index 0000000000..504eabdb7a --- /dev/null +++ b/src/panels/config/js/trigger/device.js @@ -0,0 +1,52 @@ +import { h, Component } from "preact"; + +import "../../../../components/device/ha-device-picker"; +import "../../../../components/device/ha-device-trigger-picker"; + +import { onChangeEvent } from "../../../../common/preact/event"; + +export default class DeviceTrigger extends Component { + constructor() { + super(); + this.onChange = onChangeEvent.bind(this, "trigger"); + this.devicePicked = this.devicePicked.bind(this); + this.deviceTriggerPicked = this.deviceTriggerPicked.bind(this); + this.state.device_id = undefined; + } + + devicePicked(ev) { + this.setState({ device_id: ev.target.value }); + } + + deviceTriggerPicked(ev) { + const deviceTrigger = ev.target.value; + this.props.onChange(this.props.index, (this.props.trigger = deviceTrigger)); + } + + /* eslint-disable camelcase */ + render({ trigger, hass }, { device_id }) { + if (device_id === undefined) device_id = trigger.device_id; + + return ( +
+ + +
+ ); + } +} + +DeviceTrigger.defaultConfig = { + device_id: "", +}; diff --git a/src/panels/config/js/trigger/trigger_edit.js b/src/panels/config/js/trigger/trigger_edit.js index 443c094a3a..e0234ed550 100644 --- a/src/panels/config/js/trigger/trigger_edit.js +++ b/src/panels/config/js/trigger/trigger_edit.js @@ -4,6 +4,7 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; +import DeviceTrigger from "./device"; import EventTrigger from "./event"; import GeolocationTrigger from "./geo_location"; import HassTrigger from "./homeassistant"; @@ -18,6 +19,7 @@ import WebhookTrigger from "./webhook"; import ZoneTrigger from "./zone"; const TYPES = { + device: DeviceTrigger, event: EventTrigger, state: StateTrigger, geo_location: GeolocationTrigger, diff --git a/src/panels/config/server_control/ha-config-section-server-control.js b/src/panels/config/server_control/ha-config-section-server-control.js index d1e7fb9c41..6382de6c5e 100644 --- a/src/panels/config/server_control/ha-config-section-server-control.js +++ b/src/panels/config/server_control/ha-config-section-server-control.js @@ -150,6 +150,7 @@ class HaConfigSectionServerControl extends LocalizeMixin(PolymerElement) { hass="[[hass]]" domain="homeassistant" service="restart" + confirmation="[[localize('ui.panel.config.server_control.section.server_management.confirm_restart')]]" >[[localize('ui.panel.config.server_control.section.server_management.restart')]] [[localize('ui.panel.config.server_control.section.server_management.stop')]] diff --git a/src/panels/developer-tools/developer-tools-router.ts b/src/panels/developer-tools/developer-tools-router.ts index 924c82232e..7af7705ac1 100644 --- a/src/panels/developer-tools/developer-tools-router.ts +++ b/src/panels/developer-tools/developer-tools-router.ts @@ -28,6 +28,10 @@ class DeveloperToolsRouter extends HassRouterPage { tag: "developer-tools-info", load: () => import("./info/developer-tools-info"), }, + logs: { + tag: "developer-tools-logs", + load: () => import("./logs/developer-tools-logs"), + }, mqtt: { tag: "developer-tools-mqtt", load: () => import("./mqtt/developer-tools-mqtt"), diff --git a/src/panels/developer-tools/ha-panel-developer-tools.ts b/src/panels/developer-tools/ha-panel-developer-tools.ts index f982cc1f1a..edb2236237 100644 --- a/src/panels/developer-tools/ha-panel-developer-tools.ts +++ b/src/panels/developer-tools/ha-panel-developer-tools.ts @@ -52,6 +52,9 @@ class PanelDeveloperTools extends LitElement { ${this.hass.localize("ui.panel.developer-tools.tabs.info.title")} + + ${this.hass.localize("ui.panel.developer-tools.tabs.logs.title")} + ${this.hass.localize( "ui.panel.developer-tools.tabs.events.title" diff --git a/src/panels/developer-tools/info/developer-tools-info.ts b/src/panels/developer-tools/info/developer-tools-info.ts index f9b1be82f0..3c021f7c57 100644 --- a/src/panels/developer-tools/info/developer-tools-info.ts +++ b/src/panels/developer-tools/info/developer-tools-info.ts @@ -10,8 +10,6 @@ import { import { HomeAssistant } from "../../../types"; import { haStyle } from "../../../resources/styles"; -import "./system-log-card"; -import "./error-log-card"; import "./system-health-card"; const JS_TYPE = __BUILD__; @@ -119,8 +117,6 @@ class HaPanelDevInfo extends LitElement {
- -
`; } @@ -181,6 +177,7 @@ class HaPanelDevInfo extends LitElement { display: block; max-width: 600px; margin: 0 auto; + padding-bottom: 16px; } `, ]; diff --git a/src/panels/developer-tools/logs/developer-tools-logs.ts b/src/panels/developer-tools/logs/developer-tools-logs.ts new file mode 100644 index 0000000000..6bc7190ffd --- /dev/null +++ b/src/panels/developer-tools/logs/developer-tools-logs.ts @@ -0,0 +1,52 @@ +import { + LitElement, + html, + CSSResult, + css, + TemplateResult, + property, +} from "lit-element"; + +import { HomeAssistant } from "../../../types"; +import { haStyle } from "../../../resources/styles"; + +import "../logs/system-log-card"; +import "../logs/error-log-card"; + +class HaPanelDevLogs extends LitElement { + @property() public hass!: HomeAssistant; + + protected render(): TemplateResult | void { + return html` +
+ + +
+ `; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + :host { + -ms-user-select: initial; + -webkit-user-select: initial; + -moz-user-select: initial; + } + + .content { + direction: ltr; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "developer-tools-logs": HaPanelDevLogs; + } +} + +customElements.define("developer-tools-logs", HaPanelDevLogs); diff --git a/src/panels/developer-tools/info/dialog-system-log-detail.ts b/src/panels/developer-tools/logs/dialog-system-log-detail.ts similarity index 100% rename from src/panels/developer-tools/info/dialog-system-log-detail.ts rename to src/panels/developer-tools/logs/dialog-system-log-detail.ts diff --git a/src/panels/developer-tools/info/error-log-card.ts b/src/panels/developer-tools/logs/error-log-card.ts similarity index 100% rename from src/panels/developer-tools/info/error-log-card.ts rename to src/panels/developer-tools/logs/error-log-card.ts diff --git a/src/panels/developer-tools/info/show-dialog-system-log-detail.ts b/src/panels/developer-tools/logs/show-dialog-system-log-detail.ts similarity index 100% rename from src/panels/developer-tools/info/show-dialog-system-log-detail.ts rename to src/panels/developer-tools/logs/show-dialog-system-log-detail.ts diff --git a/src/panels/developer-tools/info/system-log-card.ts b/src/panels/developer-tools/logs/system-log-card.ts similarity index 98% rename from src/panels/developer-tools/info/system-log-card.ts rename to src/panels/developer-tools/logs/system-log-card.ts index e3800dcfec..1d3e264e46 100644 --- a/src/panels/developer-tools/info/system-log-card.ts +++ b/src/panels/developer-tools/logs/system-log-card.ts @@ -141,8 +141,6 @@ class SystemLogCard extends LitElement { .system-log-intro { margin: 16px; - border-top: 1px solid var(--light-primary-color); - padding-top: 16px; } .loading-container { diff --git a/src/panels/lovelace/cards/hui-alarm-panel-card.ts b/src/panels/lovelace/cards/hui-alarm-panel-card.ts index 8b99421f7e..899a2c6eb9 100644 --- a/src/panels/lovelace/cards/hui-alarm-panel-card.ts +++ b/src/panels/lovelace/cards/hui-alarm-panel-card.ts @@ -114,7 +114,11 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { } return html` - +