diff --git a/.travis.yml b/.travis.yml index 24433d2e24..bbe715a830 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,15 +13,6 @@ script: - npm run test # - xvfb-run wct --module-resolution=node --npm # - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi' -services: - - docker -before_deploy: - - "docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21" -deploy: - provider: script - script: script/travis_deploy - "on": - branch: master dist: trusty addons: sauce_connect: true diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml new file mode 100644 index 0000000000..133f110ea8 --- /dev/null +++ b/azure-pipelines-translation.yml @@ -0,0 +1,70 @@ +# https://dev.azure.com/home-assistant + +trigger: + batch: true + branches: + include: + - dev + paths: + include: + - translation/en.json +pr: none +schedules: + - cron: "30 0 * * *" + displayName: "translation update" + branches: + include: + - dev + always: true +variables: +- group: translation +resources: + repositories: + - repository: azure + type: github + name: 'home-assistant/ci-azure' + endpoint: 'home-assistant' + + +jobs: + +- job: 'Upload' + pool: + vmImage: 'ubuntu-latest' + steps: + - task: NodeTool@0 + displayName: 'Use Node 12.x' + inputs: + versionSpec: '12.x' + - script: | + export LOKALISE_TOKEN="$(lokaliseToken)" + export AZURE_BRANCH="$(Build.SourceBranchName)" + + ./script/translations_upload_base + displayName: 'Upload Translation' + +- job: 'Download' + dependsOn: + - 'Upload' + condition: or(eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.Reason'], 'Manual')) + pool: + vmImage: 'ubuntu-latest' + steps: + - task: NodeTool@0 + displayName: 'Use Node 12.x' + inputs: + versionSpec: '12.x' + - template: templates/azp-step-git-init.yaml@azure + - script: | + export LOKALISE_TOKEN="$(lokaliseToken)" + export AZURE_BRANCH="$(Build.SourceBranchName)" + + npm install + ./script/translations_download + displayName: 'Download Translation' + - script: | + git checkout dev + git add translation + git commit -am "[ci skip] Translation update" + git push + displayName: 'Update translation' diff --git a/build-scripts/webpack.js b/build-scripts/webpack.js index ac04587333..d3c57b3c2a 100644 --- a/build-scripts/webpack.js +++ b/build-scripts/webpack.js @@ -91,7 +91,7 @@ const createWebpackConfig = ({ ), ].filter(Boolean), resolve: { - extensions: [".ts", ".js", ".json", ".tsx"], + extensions: [".ts", ".js", ".json"], alias: { react: "preact-compat", "react-dom": "preact-compat", diff --git a/hassio/src/addon-view/hassio-addon-info.js b/hassio/src/addon-view/hassio-addon-info.js index 449b85a8cc..1ef1386ecb 100644 --- a/hassio/src/addon-view/hassio-addon-info.js +++ b/hassio/src/addon-view/hassio-addon-info.js @@ -373,19 +373,21 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) { -
-
- Protection mode - - - Grant the add-on elevated system access. - +
@@ -610,6 +612,10 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) { return !addon.ingress || !this._computeHA92plus(hass); } + _computeUsesProtectedOptions(addon) { + return addon.docker_api || addon.full_access || addon.host_pid; + } + _computeHA92plus(hass) { const [major, minor] = hass.config.version.split(".", 2); return Number(major) > 0 || (major === "0" && Number(minor) >= 92); diff --git a/package.json b/package.json index 9f111f9e4e..17670b2b1b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "version": "1.0.0", "scripts": { "build": "script/build_frontend", - "lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'src/**/*.tsx' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts' && tsc", + "lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts' && tsc", "mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts", "test": "npm run lint && npm run mocha", "docker_build": "sh ./script/docker_run.sh build $npm_package_version", @@ -26,7 +26,7 @@ "@material/mwc-fab": "^0.10.0", "@material/mwc-ripple": "^0.10.0", "@material/mwc-switch": "^0.10.0", - "@mdi/svg": "4.6.95", + "@mdi/svg": "4.7.95", "@polymer/app-layout": "^3.0.2", "@polymer/app-localize-behavior": "^3.0.1", "@polymer/app-route": "^3.0.2", @@ -99,7 +99,6 @@ "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", diff --git a/script/translations_upload_base b/script/translations_upload_base index 56593acd17..2c76d300bb 100755 --- a/script/translations_upload_base +++ b/script/translations_upload_base @@ -26,8 +26,8 @@ LANG_ISO=en CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) -if [ "${CURRENT_BRANCH-}" != "master" ] && [ "${TRAVIS_BRANCH-}" != "master" ] ; then - echo "Please only run the translations upload script from a clean checkout of master." +if [ "${CURRENT_BRANCH-}" != "dev" ] && [ "${AZURE_BRANCH-}" != "dev" ] ; then + echo "Please only run the translations upload script from a clean checkout of dev." exit 1 fi diff --git a/script/travis_deploy b/script/travis_deploy deleted file mode 100755 index 4875362c2f..0000000000 --- a/script/travis_deploy +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -# Safe bash settings -# -e Exit on command fail -# -u Exit on unset variable -# -o pipefail Exit if piped command has error code -set -eu -o pipefail - -cd "$(dirname "$0")/.." - -script/translations_upload_base diff --git a/setup.py b/setup.py index c36ba08115..4a82f955d5 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20191204.1", + version="20200107.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/common/dom/dynamic-content-directive.ts b/src/common/dom/dynamic-element-directive.ts similarity index 66% rename from src/common/dom/dynamic-content-directive.ts rename to src/common/dom/dynamic-element-directive.ts index 31a8a1ead7..25ca9cfec8 100644 --- a/src/common/dom/dynamic-content-directive.ts +++ b/src/common/dom/dynamic-element-directive.ts @@ -1,7 +1,7 @@ import { directive, Part, NodePart } from "lit-html"; -export const dynamicContentDirective = directive( - (tag: string, properties: { [key: string]: any }) => (part: Part): void => { +export const dynamicElement = 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" @@ -14,16 +14,20 @@ export const dynamicContentDirective = directive( element !== undefined && tag.toUpperCase() === (element as HTMLElement).tagName ) { - Object.entries(properties).forEach(([key, value]) => { - element![key] = value; - }); + if (properties) { + Object.entries(properties).forEach(([key, value]) => { + element![key] = value; + }); + } return; } element = document.createElement(tag); - Object.entries(properties).forEach(([key, value]) => { - element![key] = value; - }); + if (properties) { + Object.entries(properties).forEach(([key, value]) => { + element![key] = value; + }); + } part.setValue(element); } ); diff --git a/src/common/preact/event.ts b/src/common/preact/event.ts deleted file mode 100644 index a35d1969e4..0000000000 --- a/src/common/preact/event.ts +++ /dev/null @@ -1,28 +0,0 @@ -// interface OnChangeComponent { -// props: { -// index: number; -// onChange(index: number, data: object); -// }; -// } - -// export function onChangeEvent(this: OnChangeComponent, prop, ev) { -export function onChangeEvent(this: any, prop, ev) { - if (!this.initialized) { - return; - } - - const origData = this.props[prop]; - if (ev.target.value === origData[ev.target.name]) { - return; - } - - const data = { ...origData }; - - if (ev.target.value) { - data[ev.target.name] = ev.target.value; - } else { - delete data[ev.target.name]; - } - - this.props.onChange(this.props.index, data); -} diff --git a/src/common/preact/unmount.ts b/src/common/preact/unmount.ts deleted file mode 100644 index 8d7fa510ea..0000000000 --- a/src/common/preact/unmount.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { render } from "preact"; - -export default function unmount(mountEl) { - render( - // @ts-ignore - () => null, - mountEl - ); -} diff --git a/src/common/search/search-input.ts b/src/common/search/search-input.ts index 65b29881e9..2c3e772c42 100644 --- a/src/common/search/search-input.ts +++ b/src/common/search/search-input.ts @@ -14,7 +14,11 @@ import "@material/mwc-button"; @customElement("search-input") class SearchInput extends LitElement { - @property() private filter?: string; + @property() public filter?: string; + + public focus() { + this.shadowRoot!.querySelector("paper-input")!.focus(); + } protected render(): TemplateResult | void { return html` diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index ea7f0e59ac..7827938381 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -75,7 +75,7 @@ export interface DataTableSortColumnData { export interface DataTableColumnData extends DataTableSortColumnData { title: string; type?: "numeric" | "icon"; - template?: (data: any, row: T) => TemplateResult; + template?: (data: any, row: T) => TemplateResult | string; } export interface DataTableRowData { @@ -88,11 +88,11 @@ export class HaDataTable extends BaseElement { @property({ type: Array }) public data: DataTableRowData[] = []; @property({ type: Boolean }) public selectable = false; @property({ type: String }) public id = "id"; + @property({ type: String }) public filter = ""; protected mdcFoundation!: MDCDataTableFoundation; protected readonly mdcFoundationClass = MDCDataTableFoundation; @query(".mdc-data-table") protected mdcRoot!: HTMLElement; @queryAll(".mdc-data-table__row") protected rowElements!: HTMLElement[]; - @query("#header-checkbox") private _headerCheckbox!: HaCheckbox; @property({ type: Boolean }) private _filterable = false; @property({ type: Boolean }) private _headerChecked = false; @property({ type: Boolean }) private _headerIndeterminate = false; @@ -108,13 +108,19 @@ export class HaDataTable extends BaseElement { private _worker: any | undefined; private _debounceSearch = debounce( - (ev) => { - this._filter = ev.detail.value; + (value: string) => { + this._filter = value; }, 200, false ); + public clearSelection(): void { + this._headerChecked = false; + this._headerIndeterminate = false; + this.mdcFoundation.handleHeaderRowCheckboxChange(); + } + protected firstUpdated() { super.firstUpdated(); this._worker = sortFilterWorker(); @@ -146,6 +152,10 @@ export class HaDataTable extends BaseElement { this._sortColumns = clonedColumns; } + if (properties.has("filter")) { + this._debounceSearch(this.filter); + } + if ( properties.has("data") || properties.has("columns") || @@ -159,14 +169,18 @@ export class HaDataTable extends BaseElement { protected render() { return html` - ${this._filterable - ? html` - - ` - : ""}
+ + ${this._filterable + ? html` +
+ +
+ ` + : ""} +
@@ -178,7 +192,6 @@ export class HaDataTable extends BaseElement { scope="col" > string; @property() public computeLabel?: (schema: HaFormSchema) => string; @property() public computeSuffix?: (schema: HaFormSchema) => string; - @query("ha-form") private _childForm?: HaForm; - @query("#element") private _elementContainer?: HTMLDivElement; public focus() { - const input = this._childForm - ? this._childForm - : this._elementContainer - ? this._elementContainer.lastChild - : undefined; - + const input = + this.shadowRoot!.getElementById("child-form") || + this.shadowRoot!.querySelector("ha-form"); if (!input) { return; } - (input as HTMLElement).focus(); } @@ -151,40 +144,16 @@ export class HaForm extends LitElement implements HaFormElement { ` : ""} -
+ ${dynamicElement(`ha-form-${this.schema.type}`, { + schema: this.schema, + data: this.data, + label: this._computeLabel(this.schema), + suffix: this._computeSuffix(this.schema), + id: "child-form", + })} `; } - protected updated(changedProperties: PropertyValues) { - const schemaChanged = changedProperties.has("schema"); - const oldSchema = schemaChanged - ? changedProperties.get("schema") - : undefined; - if ( - !Array.isArray(this.schema) && - schemaChanged && - (!oldSchema || (oldSchema as HaFormSchema).type !== this.schema.type) - ) { - const element = document.createElement( - `ha-form-${this.schema.type}` - ) as HaFormElement; - element.schema = this.schema; - element.data = this.data; - element.label = this._computeLabel(this.schema); - element.suffix = this._computeSuffix(this.schema); - if (this._elementContainer!.lastChild) { - this._elementContainer!.removeChild(this._elementContainer!.lastChild); - } - this._elementContainer!.appendChild(element); - } else if (this._elementContainer && this._elementContainer.lastChild) { - const element = this._elementContainer!.lastChild as HaFormElement; - element.schema = this.schema; - element.data = this.data; - element.label = this._computeLabel(this.schema); - element.suffix = this._computeSuffix(this.schema); - } - } - private _computeLabel(schema: HaFormSchema) { return this.computeLabel ? this.computeLabel(schema) diff --git a/src/components/ha-yaml-editor.ts b/src/components/ha-yaml-editor.ts index dc3a0cb08e..d6b808177e 100644 --- a/src/components/ha-yaml-editor.ts +++ b/src/components/ha-yaml-editor.ts @@ -1,7 +1,10 @@ import { safeDump, safeLoad } from "js-yaml"; import "./ha-code-editor"; -import { LitElement, property, customElement, html } from "lit-element"; +import { LitElement, property, customElement, html, query } from "lit-element"; import { fireEvent } from "../common/dom/fire_event"; +import { afterNextRender } from "../common/util/render-status"; +// tslint:disable-next-line +import { HaCodeEditor } from "./ha-code-editor"; const isEmpty = (obj: object) => { for (const key in obj) { @@ -18,14 +21,23 @@ export class HaYamlEditor extends LitElement { @property() public isValid = true; @property() public label?: string; @property() private _yaml?: string; + @query("ha-code-editor") private _editor?: HaCodeEditor; - protected firstUpdated() { + public setValue(value) { try { - this._yaml = - this.value && !isEmpty(this.value) ? safeDump(this.value) : ""; + this._yaml = value && !isEmpty(value) ? safeDump(value) : ""; } catch (err) { alert(`There was an error converting to YAML: ${err}`); } + afterNextRender(() => { + if (this._editor?.codemirror) { + this._editor.codemirror.refresh(); + } + }); + } + + protected firstUpdated() { + this.setValue(this.value); } protected render() { diff --git a/src/data/automation.ts b/src/data/automation.ts index 263be8b443..6359264eac 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -4,6 +4,8 @@ import { } from "home-assistant-js-websocket"; import { HomeAssistant } from "../types"; import { navigate } from "../common/navigate"; +import { DeviceCondition, DeviceTrigger } from "./device_automation"; +import { Action } from "./script"; export interface AutomationEntity extends HassEntityBase { attributes: HassEntityAttributeBase & { @@ -15,11 +17,162 @@ export interface AutomationEntity extends HassEntityBase { export interface AutomationConfig { alias: string; description: string; - trigger: any[]; - condition?: any[]; - action: any[]; + trigger: Trigger[]; + condition?: Condition[]; + action: Action[]; } +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 LogicalCondition { + condition: "and" | "or"; + conditions: Condition[]; +} + +export interface StateCondition { + condition: "state"; + entity_id: string; + state: string | number; +} + +export interface NumericStateCondition { + condition: "numeric_state"; + entity_id: string; + above?: number; + below?: number; + value_template?: string; +} + +export interface SunCondition { + condition: "sun"; + after_offset: number; + before_offset: number; + after: "sunrise" | "sunset"; + before: "sunrise" | "sunset"; +} + +export interface ZoneCondition { + condition: "zone"; + entity_id: string; + zone: string; +} + +export interface TimeCondition { + condition: "time"; + after: string; + before: string; +} + +export interface TemplateCondition { + condition: "template"; + value_template: string; +} + +export type Condition = + | StateCondition + | NumericStateCondition + | SunCondition + | ZoneCondition + | TimeCondition + | TemplateCondition + | DeviceCondition + | LogicalCondition; + export const deleteAutomation = (hass: HomeAssistant, id: string) => hass.callApi("DELETE", `config/automation/config/${id}`); diff --git a/src/data/config_flow.ts b/src/data/config_flow.ts index d415524d8d..7b922e6850 100644 --- a/src/data/config_flow.ts +++ b/src/data/config_flow.ts @@ -4,6 +4,8 @@ import { debounce } from "../common/util/debounce"; import { getCollection, Connection } from "home-assistant-js-websocket"; import { LocalizeFunc } from "../common/translations/localize"; +export const DISCOVERY_SOURCES = ["unignore", "homekit", "ssdp", "zeroconf"]; + export const createConfigFlow = (hass: HomeAssistant, handler: string) => hass.callApi("POST", "config/config_entries/flow", { handler, @@ -26,6 +28,9 @@ export const handleConfigFlowStep = ( data ); +export const ignoreConfigFlow = (hass: HomeAssistant, flowId: string) => + hass.callWS({ type: "config_entries/ignore_flow", flow_id: flowId }); + export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) => hass.callApi("DELETE", `config/config_entries/flow/${flowId}`); diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 8781959b07..368f814e15 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -69,6 +69,10 @@ export interface NoActionConfig extends BaseActionConfig { action: "none"; } +export interface CustomActionConfig extends BaseActionConfig { + action: "fire-dom-event"; +} + export interface BaseActionConfig { confirmation?: ConfirmationRestrictionConfig; } @@ -88,7 +92,8 @@ export type ActionConfig = | NavigateActionConfig | UrlActionConfig | MoreInfoActionConfig - | NoActionConfig; + | NoActionConfig + | CustomActionConfig; export const fetchConfig = ( conn: Connection, diff --git a/src/data/script.ts b/src/data/script.ts index 5eb2982f2f..781ed79af5 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -1,5 +1,21 @@ import { HomeAssistant } from "../types"; import { computeObjectId } from "../common/entity/compute_object_id"; +import { Condition } from "./automation"; +import { + HassEntityBase, + HassEntityAttributeBase, +} from "home-assistant-js-websocket"; + +export interface ScriptEntity extends HassEntityBase { + attributes: HassEntityAttributeBase & { + last_triggered: string; + }; +} + +export interface ScriptConfig { + alias: string; + sequence: Action[]; +} export interface EventAction { event: string; @@ -7,12 +23,40 @@ export interface EventAction { event_data_template?: { [key: string]: any }; } +export interface ServiceAction { + service: string; + entity_id?: string; + data?: { [key: string]: any }; +} + export interface DeviceAction { device_id: string; domain: string; entity_id: string; } +export interface DelayAction { + delay: number; +} + +export interface SceneAction { + scene: string; +} + +export interface WaitAction { + wait_template: string; + timeout?: number; +} + +export type Action = + | EventAction + | DeviceAction + | ServiceAction + | Condition + | DelayAction + | SceneAction + | WaitAction; + export const triggerScript = ( hass: HomeAssistant, entityId: string, diff --git a/src/data/zha.ts b/src/data/zha.ts index 4ae16c680a..d0d57d38d8 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -51,6 +51,12 @@ export interface ReadAttributeServiceData { manufacturer?: number; } +export interface ZHAGroup { + name: string; + group_id: number; + members: ZHADevice[]; +} + export const reconfigureNode = ( hass: HomeAssistant, ieeeAddress: string @@ -153,3 +159,66 @@ export const fetchClustersForZhaNode = ( type: "zha/devices/clusters", ieee: ieeeAddress, }); + +export const fetchGroups = (hass: HomeAssistant): Promise => + hass.callWS({ + type: "zha/groups", + }); + +export const removeGroups = ( + hass: HomeAssistant, + groupIdsToRemove: number[] +): Promise => + hass.callWS({ + type: "zha/group/remove", + group_ids: groupIdsToRemove, + }); + +export const fetchGroup = ( + hass: HomeAssistant, + groupId: number +): Promise => + hass.callWS({ + type: "zha/group", + group_id: groupId, + }); + +export const fetchGroupableDevices = ( + hass: HomeAssistant +): Promise => + hass.callWS({ + type: "zha/devices/groupable", + }); + +export const addMembersToGroup = ( + hass: HomeAssistant, + groupId: number, + membersToAdd: string[] +): Promise => + hass.callWS({ + type: "zha/group/members/add", + group_id: groupId, + members: membersToAdd, + }); + +export const removeMembersFromGroup = ( + hass: HomeAssistant, + groupId: number, + membersToRemove: string[] +): Promise => + hass.callWS({ + type: "zha/group/members/remove", + group_id: groupId, + members: membersToRemove, + }); + +export const addGroup = ( + hass: HomeAssistant, + groupName: string, + membersToAdd?: string[] +): Promise => + hass.callWS({ + type: "zha/group/add", + group_name: groupName, + members: membersToAdd, + }); diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index 98d5ff33b7..c97c491df5 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -109,6 +109,7 @@ class StepFlowForm extends LitElement { protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); + setTimeout(() => this.shadowRoot!.querySelector("ha-form")!.focus(), 0); this.addEventListener("keypress", (ev) => { if (ev.keyCode === 13) { this._submitStep(); diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index ef410f7529..19963854af 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -101,6 +101,14 @@ class StepFlowPickHandler extends LitElement { `; } + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + setTimeout( + () => this.shadowRoot!.querySelector("search-input")!.focus(), + 0 + ); + } + protected updated(changedProps) { super.updated(changedProps); // Store the width so that when we search, box doesn't jump diff --git a/src/dialogs/more-info/more-info-controls.js b/src/dialogs/more-info/more-info-controls.js index df688abe91..e55219d128 100644 --- a/src/dialogs/more-info/more-info-controls.js +++ b/src/dialogs/more-info/more-info-controls.js @@ -1,6 +1,7 @@ import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@polymer/paper-icon-button/paper-icon-button"; +import "@material/mwc-button"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; @@ -18,6 +19,8 @@ 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"; +import { removeEntityRegistryEntry } from "../../data/entity_registry"; +import { showConfirmationDialog } from "../confirmation/show-dialog-confirmation"; const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"]; /* @@ -57,6 +60,10 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) { padding-bottom: 16px; } + mwc-button.warning { + --mdc-theme-primary: var(--google-red-500); + } + :host([domain="camera"]) paper-dialog-scrollable { margin: 0 -24px -21px; } @@ -117,6 +124,15 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) { state-obj="[[stateObj]]" hass="[[hass]]" > + `; } @@ -172,6 +188,10 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) { return !stateObj || !DOMAINS_NO_INFO.includes(computeStateDomain(stateObj)); } + _computeShowRestored(stateObj) { + return stateObj && stateObj.attributes.restored; + } + _computeShowHistoryComponent(hass, stateObj) { return ( hass && @@ -202,6 +222,21 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) { } } + _removeEntity() { + showConfirmationDialog(this, { + title: this.localize( + "ui.dialogs.more_info_control.restored.confirm_remove_title" + ), + text: this.localize( + "ui.dialogs.more_info_control.restored.confirm_remove_text" + ), + confirmBtnText: this.localize("ui.common.yes"), + cancelBtnText: this.localize("ui.common.no"), + confirm: () => + removeEntityRegistryEntry(this.hass, this.stateObj.entity_id), + }); + } + _gotoSettings() { this.fire("more-info-page", { page: "settings" }); } diff --git a/src/layouts/hass-subpage.ts b/src/layouts/hass-subpage.ts index 2e0befb405..25d244774c 100644 --- a/src/layouts/hass-subpage.ts +++ b/src/layouts/hass-subpage.ts @@ -53,9 +53,9 @@ class HassSubpage extends LitElement { height: 64px; padding: 0 16px; pointer-events: none; - background-color: var(--primary-color); + background-color: var(--app-header-background-color); font-weight: 400; - color: var(--text-primary-color, white); + color: var(--app-header-text-color, white); } ha-menu-button, diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts new file mode 100644 index 0000000000..df0b925ea8 --- /dev/null +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -0,0 +1,272 @@ +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 { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-card"; +import { HomeAssistant } from "../../../../types"; + +import { Action } from "../../../../data/script"; + +import "./types/ha-automation-action-service"; +import "./types/ha-automation-action-device_id"; +import "./types/ha-automation-action-delay"; +import "./types/ha-automation-action-event"; +import "./types/ha-automation-action-condition"; +import "./types/ha-automation-action-scene"; +import "./types/ha-automation-action-wait_template"; + +const OPTIONS = [ + "condition", + "delay", + "device_id", + "event", + "scene", + "service", + "wait_template", +]; + +const getType = (action: Action) => { + return OPTIONS.find((option) => option in action); +}; + +declare global { + // for fire event + interface HASSDomEvents { + "move-action": { direction: "up" | "down" }; + } +} + +export interface ActionElement extends LitElement { + action: Action; +} + +export const handleChangeEvent = (element: ActionElement, ev: CustomEvent) => { + ev.stopPropagation(); + const name = (ev.target as any)?.name; + if (!name) { + return; + } + const newVal = ev.detail.value; + + if ((element.action[name] || "") === newVal) { + return; + } + + let newAction: Action; + if (!newVal) { + newAction = { ...element.action }; + delete newAction[name]; + } else { + newAction = { ...element.action, [name]: newVal }; + } + fireEvent(element, "value-changed", { value: newAction }); +}; + +@customElement("ha-automation-action-row") +export default class HaAutomationActionRow extends LitElement { + @property() public hass!: HomeAssistant; + @property() public action!: Action; + @property() public index!: number; + @property() public totalActions!: number; + @property() private _yamlMode = false; + + protected render() { + const type = getType(this.action); + const selected = type ? OPTIONS.indexOf(type) : -1; + const yamlMode = this._yamlMode || selected === -1; + + return html` + +
+
+ ${this.index !== 0 + ? html` + + ` + : ""} + ${this.index !== this.totalActions - 1 + ? html` + + ` + : ""} + + + + + ${yamlMode + ? this.hass.localize( + "ui.panel.config.automation.editor.edit_ui" + ) + : this.hass.localize( + "ui.panel.config.automation.editor.edit_yaml" + )} + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.duplicate" + )} + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.delete" + )} + + + +
+ ${yamlMode + ? html` +
+ ${selected === -1 + ? html` + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.unsupported_action", + "action", + type + )} + ` + : ""} + +
+ ` + : html` + + + ${OPTIONS.map( + (opt) => html` + + ${this.hass.localize( + `ui.panel.config.automation.editor.actions.type.${opt}.label` + )} + + ` + )} + + +
+ ${dynamicElement(`ha-automation-action-${type}`, { + hass: this.hass, + action: this.action, + })} +
+ `} +
+
+ `; + } + + private _moveUp() { + fireEvent(this, "move-action", { direction: "up" }); + } + + private _moveDown() { + fireEvent(this, "move-action", { direction: "down" }); + } + + private _onDelete() { + if ( + confirm( + this.hass.localize( + "ui.panel.config.automation.editor.actions.delete_confirm" + ) + ) + ) { + fireEvent(this, "value-changed", { value: null }); + } + } + + private _typeChanged(ev: CustomEvent) { + const type = ((ev.target as PaperListboxElement)?.selectedItem as any) + ?.action; + + if (!type) { + return; + } + + if (type !== getType(this.action)) { + const elClass = customElements.get(`ha-automation-action-${type}`); + + fireEvent(this, "value-changed", { + value: { + ...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-action-row": HaAutomationActionRow; + } +} diff --git a/src/panels/config/automation/action/ha-automation-action.ts b/src/panels/config/automation/action/ha-automation-action.ts new file mode 100644 index 0000000000..ac2e45ff10 --- /dev/null +++ b/src/panels/config/automation/action/ha-automation-action.ts @@ -0,0 +1,98 @@ +import "@material/mwc-button"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, +} from "lit-element"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-card"; +import { Action } from "../../../../data/script"; +import { HomeAssistant } from "../../../../types"; +import "./ha-automation-action-row"; + +@customElement("ha-automation-action") +export default class HaAutomationAction extends LitElement { + @property() public hass!: HomeAssistant; + @property() public actions!: Action[]; + + protected render() { + return html` + ${this.actions.map( + (action, idx) => html` + + ` + )} + +
+ + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.add" + )} + +
+
+ `; + } + + private _addAction() { + const actions = this.actions.concat({ + service: "", + }); + + fireEvent(this, "value-changed", { value: actions }); + } + + private _move(ev: CustomEvent) { + const index = (ev.target as any).index; + const newIndex = ev.detail.direction === "up" ? index - 1 : index + 1; + const actions = this.actions.concat(); + const action = actions.splice(index, 1)[0]; + actions.splice(newIndex, 0, action); + fireEvent(this, "value-changed", { value: actions }); + } + + private _actionChanged(ev: CustomEvent) { + ev.stopPropagation(); + const actions = [...this.actions]; + const newValue = ev.detail.value; + const index = (ev.target as any).index; + + if (newValue === null) { + actions.splice(index, 1); + } else { + actions[index] = newValue; + } + + fireEvent(this, "value-changed", { value: actions }); + } + + static get styles(): CSSResult { + return css` + ha-automation-action-row, + ha-card { + display: block; + margin-top: 16px; + } + .add-card mwc-button { + display: block; + text-align: center; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-action": HaAutomationAction; + } +} diff --git a/src/panels/config/automation/action/types/ha-automation-action-condition.ts b/src/panels/config/automation/action/types/ha-automation-action-condition.ts new file mode 100644 index 0000000000..967455ffbb --- /dev/null +++ b/src/panels/config/automation/action/types/ha-automation-action-condition.ts @@ -0,0 +1,41 @@ +import "../../condition/ha-automation-condition-editor"; + +import { LitElement, property, customElement, html } from "lit-element"; +import { ActionElement } from "../ha-automation-action-row"; +import { HomeAssistant } from "../../../../../types"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { Condition } from "../../../../../data/automation"; + +@customElement("ha-automation-action-condition") +export class HaConditionAction extends LitElement implements ActionElement { + @property() public hass!: HomeAssistant; + @property() public action!: Condition; + + public static get defaultConfig() { + return { condition: "state" }; + } + + public render() { + return html` + + `; + } + + private _conditionChanged(ev: CustomEvent) { + ev.stopPropagation(); + + fireEvent(this, "value-changed", { + value: ev.detail.value, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-action-condition": HaConditionAction; + } +} diff --git a/src/panels/config/automation/action/types/ha-automation-action-delay.ts b/src/panels/config/automation/action/types/ha-automation-action-delay.ts new file mode 100644 index 0000000000..6dd9abeafe --- /dev/null +++ b/src/panels/config/automation/action/types/ha-automation-action-delay.ts @@ -0,0 +1,44 @@ +import "@polymer/paper-input/paper-input"; +import "../../../../../components/ha-service-picker"; +import "../../../../../components/entity/ha-entity-picker"; +import "../../../../../components/ha-yaml-editor"; + +import { LitElement, property, customElement, html } from "lit-element"; +import { ActionElement, handleChangeEvent } from "../ha-automation-action-row"; +import { HomeAssistant } from "../../../../../types"; +import { DelayAction } from "../../../../../data/script"; + +@customElement("ha-automation-action-delay") +export class HaDelayAction extends LitElement implements ActionElement { + @property() public hass!: HomeAssistant; + @property() public action!: DelayAction; + + public static get defaultConfig() { + return { delay: "" }; + } + + public render() { + const { delay } = this.action; + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + handleChangeEvent(this, ev); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-action-delay": HaDelayAction; + } +} diff --git a/src/panels/config/automation/action/types/ha-automation-action-device_id.ts b/src/panels/config/automation/action/types/ha-automation-action-device_id.ts new file mode 100644 index 0000000000..003ddd55c0 --- /dev/null +++ b/src/panels/config/automation/action/types/ha-automation-action-device_id.ts @@ -0,0 +1,129 @@ +import "../../../../../components/device/ha-device-picker"; +import "../../../../../components/device/ha-device-action-picker"; +import "../../../../../components/ha-form/ha-form"; + +import { + fetchDeviceActionCapabilities, + deviceAutomationsEqual, + DeviceAction, +} 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-action-device_id") +export class HaDeviceAction extends LitElement { + @property() public hass!: HomeAssistant; + @property() public action!: DeviceAction; + @property() private _deviceId?: string; + @property() private _capabilities?; + private _origAction?: DeviceAction; + + public static get defaultConfig() { + return { + device_id: "", + domain: "", + entity_id: "", + }; + } + + protected render() { + const deviceId = this._deviceId || this.action.device_id; + const extraFieldsData = + this._capabilities && this._capabilities.extra_fields + ? this._capabilities.extra_fields.map((item) => { + return { [item.name]: this.action[item.name] }; + }) + : undefined; + + return html` + + + ${extraFieldsData + ? html` + + ` + : ""} + `; + } + + protected firstUpdated() { + if (!this._capabilities) { + this._getCapabilities(); + } + if (this.action) { + this._origAction = this.action; + } + } + + protected updated(changedPros) { + const prevAction = changedPros.get("action"); + if (prevAction && !deviceAutomationsEqual(prevAction, this.action)) { + this._getCapabilities(); + } + } + + private async _getCapabilities() { + const action = this.action; + + this._capabilities = action.domain + ? await fetchDeviceActionCapabilities(this.hass, action) + : null; + } + + private _devicePicked(ev) { + ev.stopPropagation(); + this._deviceId = ev.target.value; + } + + private _deviceActionPicked(ev) { + ev.stopPropagation(); + let action = ev.detail.value; + if (this._origAction && deviceAutomationsEqual(this._origAction, action)) { + action = this._origAction; + } + fireEvent(this, "value-changed", { value: action }); + } + + private _extraFieldsChanged(ev) { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { + ...this.action, + ...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.actions.type.device.extra_fields.${schema.name}` + ) || schema.name; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-action-device_id": HaDeviceAction; + } +} diff --git a/src/panels/config/automation/action/types/ha-automation-action-event.ts b/src/panels/config/automation/action/types/ha-automation-action-event.ts new file mode 100644 index 0000000000..b1c1759788 --- /dev/null +++ b/src/panels/config/automation/action/types/ha-automation-action-event.ts @@ -0,0 +1,53 @@ +import "@polymer/paper-input/paper-input"; +import "../../../../../components/ha-service-picker"; +import "../../../../../components/entity/ha-entity-picker"; +import "../../../../../components/ha-yaml-editor"; + +import { LitElement, property, customElement } from "lit-element"; +import { ActionElement, handleChangeEvent } from "../ha-automation-action-row"; +import { HomeAssistant } from "../../../../../types"; +import { html } from "lit-html"; +import { EventAction } from "../../../../../data/script"; + +@customElement("ha-automation-action-event") +export class HaEventAction extends LitElement implements ActionElement { + @property() public hass!: HomeAssistant; + @property() public action!: EventAction; + + public static get defaultConfig(): EventAction { + return { event: "", event_data: {} }; + } + + public render() { + const { event, event_data } = this.action; + + return html` + + + `; + } + + private _valueChanged(ev: CustomEvent): void { + handleChangeEvent(this, ev); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-action-event": HaEventAction; + } +} diff --git a/src/panels/config/automation/action/types/ha-automation-action-scene.ts b/src/panels/config/automation/action/types/ha-automation-action-scene.ts new file mode 100644 index 0000000000..112ab89b81 --- /dev/null +++ b/src/panels/config/automation/action/types/ha-automation-action-scene.ts @@ -0,0 +1,45 @@ +import "../../../../../components/entity/ha-entity-picker"; + +import { LitElement, property, customElement, html } from "lit-element"; +import { ActionElement } from "../ha-automation-action-row"; +import { HomeAssistant } from "../../../../../types"; +import { PolymerChangedEvent } from "../../../../../polymer-types"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { SceneAction } from "../../../../../data/script"; + +@customElement("ha-automation-action-scene") +export class HaSceneAction extends LitElement implements ActionElement { + @property() public hass!: HomeAssistant; + @property() public action!: SceneAction; + + public static get defaultConfig(): SceneAction { + return { scene: "" }; + } + + protected render() { + const { scene } = this.action; + + return html` + + `; + } + + private _entityPicked(ev: PolymerChangedEvent) { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { ...this.action, scene: ev.detail.value }, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-action-scene": HaSceneAction; + } +} diff --git a/src/panels/config/automation/action/types/ha-automation-action-service.ts b/src/panels/config/automation/action/types/ha-automation-action-service.ts new file mode 100644 index 0000000000..f00f854eae --- /dev/null +++ b/src/panels/config/automation/action/types/ha-automation-action-service.ts @@ -0,0 +1,107 @@ +import "@polymer/paper-input/paper-input"; +import "../../../../../components/ha-service-picker"; +import "../../../../../components/entity/ha-entity-picker"; +import "../../../../../components/ha-yaml-editor"; + +import { LitElement, property, customElement } from "lit-element"; +import { ActionElement, handleChangeEvent } from "../ha-automation-action-row"; +import { HomeAssistant } from "../../../../../types"; +import { html } from "lit-html"; +import memoizeOne from "memoize-one"; +import { computeDomain } from "../../../../../common/entity/compute_domain"; +import { computeObjectId } from "../../../../../common/entity/compute_object_id"; +import { PolymerChangedEvent } from "../../../../../polymer-types"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { ServiceAction } from "../../../../../data/script"; + +@customElement("ha-automation-action-service") +export class HaServiceAction extends LitElement implements ActionElement { + @property() public hass!: HomeAssistant; + @property() public action!: ServiceAction; + + public static get defaultConfig() { + return { service: "", data: {} }; + } + + private _getServiceData = memoizeOne((service: string) => { + if (!service) { + return []; + } + const domain = computeDomain(service); + const serviceName = computeObjectId(service); + const serviceDomains = this.hass.services; + if (!(domain in serviceDomains)) { + return []; + } + if (!(serviceName in serviceDomains[domain])) { + return []; + } + + const fields = serviceDomains[domain][serviceName].fields; + return Object.keys(fields).map((field) => { + return { key: field, ...fields[field] }; + }); + }); + + public render() { + const { service, data, entity_id } = this.action; + + const serviceData = this._getServiceData(service); + const entity = serviceData.find((attr) => attr.key === "entity_id"); + + return html` + + ${entity + ? html` + + ` + : ""} + + `; + } + + private _valueChanged(ev: CustomEvent): void { + handleChangeEvent(this, ev); + } + + private _serviceChanged(ev: PolymerChangedEvent) { + ev.stopPropagation(); + if (ev.detail.value === this.action.service) { + return; + } + fireEvent(this, "value-changed", { + value: { ...this.action, service: ev.detail.value }, + }); + } + + private _entityPicked(ev: PolymerChangedEvent) { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { ...this.action, entity_id: ev.detail.value }, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-action-service": HaServiceAction; + } +} diff --git a/src/panels/config/automation/action/types/ha-automation-action-wait_template.ts b/src/panels/config/automation/action/types/ha-automation-action-wait_template.ts new file mode 100644 index 0000000000..5d54469acd --- /dev/null +++ b/src/panels/config/automation/action/types/ha-automation-action-wait_template.ts @@ -0,0 +1,51 @@ +import "@polymer/paper-input/paper-input"; + +import { LitElement, property, customElement } from "lit-element"; +import { ActionElement, handleChangeEvent } from "../ha-automation-action-row"; +import { HomeAssistant } from "../../../../../types"; +import { html } from "lit-html"; +import { WaitAction } from "../../../../../data/script"; + +@customElement("ha-automation-action-wait_template") +export class HaWaitAction extends LitElement implements ActionElement { + @property() public hass!: HomeAssistant; + @property() public action!: WaitAction; + + public static get defaultConfig() { + return { wait_template: "", timeout: "" }; + } + + protected render() { + const { wait_template, timeout } = this.action; + + return html` + + + `; + } + + private _valueChanged(ev: CustomEvent): void { + handleChangeEvent(this, ev); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-action-wait_template": HaWaitAction; + } +} diff --git a/src/panels/config/automation/condition/ha-automation-condition-editor.ts b/src/panels/config/automation/condition/ha-automation-condition-editor.ts new file mode 100644 index 0000000000..57a9726bc3 --- /dev/null +++ b/src/panels/config/automation/condition/ha-automation-condition-editor.ts @@ -0,0 +1,125 @@ +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 { customElement, html, LitElement, property } from "lit-element"; +import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-card"; +import { HomeAssistant } from "../../../../types"; + +import "./types/ha-automation-condition-device"; +import "./types/ha-automation-condition-state"; +import "./types/ha-automation-condition-numeric_state"; +import "./types/ha-automation-condition-sun"; +import "./types/ha-automation-condition-template"; +import "./types/ha-automation-condition-time"; +import "./types/ha-automation-condition-zone"; +import "./types/ha-automation-condition-and"; +import "./types/ha-automation-condition-or"; +import { Condition } from "../../../../data/automation"; + +const OPTIONS = [ + "device", + "and", + "or", + "state", + "numeric_state", + "sun", + "template", + "time", + "zone", +]; + +@customElement("ha-automation-condition-editor") +export default class HaAutomationConditionEditor extends LitElement { + @property() public hass!: HomeAssistant; + @property() public condition!: Condition; + @property() public yamlMode = false; + + protected render() { + const selected = OPTIONS.indexOf(this.condition.condition); + const yamlMode = this.yamlMode || selected === -1; + return html` + ${yamlMode + ? html` +
+ ${selected === -1 + ? html` + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.unsupported_condition", + "condition", + this.condition.condition + )} + ` + : ""} + +
+ ` + : html` + + + ${OPTIONS.map( + (opt) => html` + + ${this.hass.localize( + `ui.panel.config.automation.editor.conditions.type.${opt}.label` + )} + + ` + )} + + +
+ ${dynamicElement( + `ha-automation-condition-${this.condition.condition}`, + { hass: this.hass, condition: this.condition } + )} +
+ `} + `; + } + + private _typeChanged(ev: CustomEvent) { + const type = ((ev.target as PaperListboxElement)?.selectedItem as any) + ?.condition; + + if (!type) { + return; + } + + const elClass = customElements.get(`ha-automation-condition-${type}`); + + if (type !== this.condition.condition) { + fireEvent(this, "value-changed", { + value: { + condition: type, + ...elClass.defaultConfig, + }, + }); + } + } + + private _onYamlChange(ev: CustomEvent) { + ev.stopPropagation(); + fireEvent(this, "value-changed", { value: ev.detail.value }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-condition-editor": HaAutomationConditionEditor; + } +} diff --git a/src/panels/config/automation/condition/ha-automation-condition-row.ts b/src/panels/config/automation/condition/ha-automation-condition-row.ts new file mode 100644 index 0000000000..e3552302ce --- /dev/null +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -0,0 +1,146 @@ +import "@polymer/paper-icon-button/paper-icon-button"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-menu-button/paper-menu-button"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, +} from "lit-element"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-card"; +import { HomeAssistant } from "../../../../types"; + +import "./ha-automation-condition-editor"; +import { Condition } from "../../../../data/automation"; + +export interface ConditionElement extends LitElement { + condition: Condition; +} + +export const handleChangeEvent = ( + element: ConditionElement, + ev: CustomEvent +) => { + ev.stopPropagation(); + const name = (ev.target as any)?.name; + if (!name) { + return; + } + const newVal = ev.detail.value; + + if ((element.condition[name] || "") === newVal) { + return; + } + + let newCondition: Condition; + if (!newVal) { + newCondition = { ...element.condition }; + delete newCondition[name]; + } else { + newCondition = { ...element.condition, [name]: newVal }; + } + fireEvent(element, "value-changed", { value: newCondition }); +}; + +@customElement("ha-automation-condition-row") +export default class HaAutomationConditionRow extends LitElement { + @property() public hass!: HomeAssistant; + @property() public condition!: Condition; + @property() private _yamlMode = false; + + protected render() { + if (!this.condition) { + return html``; + } + return html` + +
+
+ + + + + ${this._yamlMode + ? this.hass.localize( + "ui.panel.config.automation.editor.edit_ui" + ) + : this.hass.localize( + "ui.panel.config.automation.editor.edit_yaml" + )} + + + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.duplicate" + )} + + + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.delete" + )} + + + +
+ +
+
+ `; + } + + private _onDelete() { + if ( + confirm( + this.hass.localize( + "ui.panel.config.automation.editor.conditions.delete_confirm" + ) + ) + ) { + fireEvent(this, "value-changed", { value: null }); + } + } + + 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-condition-row": HaAutomationConditionRow; + } +} diff --git a/src/panels/config/automation/condition/ha-automation-condition.ts b/src/panels/config/automation/condition/ha-automation-condition.ts new file mode 100644 index 0000000000..4a25aa36ab --- /dev/null +++ b/src/panels/config/automation/condition/ha-automation-condition.ts @@ -0,0 +1,92 @@ +import { + LitElement, + customElement, + html, + property, + CSSResult, + css, +} from "lit-element"; +import "@material/mwc-button"; +import "../../../../components/ha-card"; + +import { HaStateCondition } from "./types/ha-automation-condition-state"; + +import { fireEvent } from "../../../../common/dom/fire_event"; +import { HomeAssistant } from "../../../../types"; + +import "./ha-automation-condition-row"; +import { Condition } from "../../../../data/automation"; + +@customElement("ha-automation-condition") +export default class HaAutomationCondition extends LitElement { + @property() public hass!: HomeAssistant; + @property() public conditions!: Condition[]; + + protected render() { + return html` + ${this.conditions.map( + (cond, idx) => html` + + ` + )} + +
+ + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.add" + )} + +
+
+ `; + } + + private _addCondition() { + const conditions = this.conditions.concat({ + condition: "state", + ...HaStateCondition.defaultConfig, + }); + + fireEvent(this, "value-changed", { value: conditions }); + } + + private _conditionChanged(ev: CustomEvent) { + ev.stopPropagation(); + const conditions = [...this.conditions]; + const newValue = ev.detail.value; + const index = (ev.target as any).index; + + if (newValue === null) { + conditions.splice(index, 1); + } else { + conditions[index] = newValue; + } + + fireEvent(this, "value-changed", { value: conditions }); + } + + static get styles(): CSSResult { + return css` + ha-automation-condition-row, + ha-card { + display: block; + margin-top: 16px; + } + .add-card mwc-button { + display: block; + text-align: center; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-condition": HaAutomationCondition; + } +} diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-and.ts b/src/panels/config/automation/condition/types/ha-automation-condition-and.ts new file mode 100644 index 0000000000..111bd727b5 --- /dev/null +++ b/src/panels/config/automation/condition/types/ha-automation-condition-and.ts @@ -0,0 +1,11 @@ +import { HaLogicalCondition } from "./ha-automation-condition-logical"; +import { customElement } from "lit-element"; + +@customElement("ha-automation-condition-and") +export class HaAndCondition extends HaLogicalCondition {} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-condition-and": HaAndCondition; + } +} diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-device.ts b/src/panels/config/automation/condition/types/ha-automation-condition-device.ts new file mode 100644 index 0000000000..1d5bfd4ff1 --- /dev/null +++ b/src/panels/config/automation/condition/types/ha-automation-condition-device.ts @@ -0,0 +1,130 @@ +import "../../../../../components/device/ha-device-picker"; +import "../../../../../components/device/ha-device-condition-picker"; +import "../../../../../components/ha-form/ha-form"; + +import { + fetchDeviceConditionCapabilities, + deviceAutomationsEqual, + DeviceCondition, +} 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-condition-device") +export class HaDeviceCondition extends LitElement { + @property() public hass!: HomeAssistant; + @property() public condition!: DeviceCondition; + @property() private _deviceId?: string; + @property() private _capabilities?; + private _origCondition?: DeviceCondition; + + public static get defaultConfig() { + return { + device_id: "", + domain: "", + entity_id: "", + }; + } + + protected render() { + const deviceId = this._deviceId || this.condition.device_id; + + const extraFieldsData = + this._capabilities && this._capabilities.extra_fields + ? this._capabilities.extra_fields.map((item) => { + return { [item.name]: this.condition[item.name] }; + }) + : undefined; + + return html` + + + ${extraFieldsData + ? html` + + ` + : ""} + `; + } + + protected firstUpdated() { + if (!this._capabilities) { + this._getCapabilities(); + } + if (this.condition) { + this._origCondition = this.condition; + } + } + + protected updated(changedPros) { + const prevCondition = changedPros.get("condition"); + if ( + prevCondition && + !deviceAutomationsEqual(prevCondition, this.condition) + ) { + this._getCapabilities(); + } + } + + private async _getCapabilities() { + const condition = this.condition; + + this._capabilities = condition.domain + ? await fetchDeviceConditionCapabilities(this.hass, condition) + : null; + } + + private _devicePicked(ev) { + ev.stopPropagation(); + this._deviceId = ev.target.value; + } + + private _deviceConditionPicked(ev) { + ev.stopPropagation(); + let condition = ev.detail.value; + if ( + this._origCondition && + deviceAutomationsEqual(this._origCondition, condition) + ) { + condition = this._origCondition; + } + fireEvent(this, "value-changed", { value: condition }); + } + + private _extraFieldsChanged(ev) { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { + ...this.condition, + ...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.conditions.type.device.extra_fields.${schema.name}` + ) || schema.name; + } +} diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts b/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts new file mode 100644 index 0000000000..5cff91242d --- /dev/null +++ b/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts @@ -0,0 +1,39 @@ +import { customElement, html, LitElement, property } from "lit-element"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { HomeAssistant } from "../../../../../types"; +import { ConditionElement } from "../ha-automation-condition-row"; +import "../ha-automation-condition"; +import { LogicalCondition } from "../../../../../data/automation"; + +@customElement("ha-automation-condition-logical") +export class HaLogicalCondition extends LitElement implements ConditionElement { + @property() public hass!: HomeAssistant; + @property() public condition!: LogicalCondition; + + public static get defaultConfig() { + return { conditions: [{ condition: "state" }] }; + } + + protected render() { + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { ...this.condition, conditions: ev.detail.value }, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-condition-logical": HaLogicalCondition; + } +} diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-numeric_state.ts b/src/panels/config/automation/condition/types/ha-automation-condition-numeric_state.ts new file mode 100644 index 0000000000..ae4117ec46 --- /dev/null +++ b/src/panels/config/automation/condition/types/ha-automation-condition-numeric_state.ts @@ -0,0 +1,76 @@ +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 { handleChangeEvent } from "../ha-automation-condition-row"; +import { NumericStateCondition } from "../../../../../data/automation"; + +@customElement("ha-automation-condition-numeric_state") +export default class HaNumericStateCondition extends LitElement { + @property() public hass!: HomeAssistant; + @property() public condition!: NumericStateCondition; + + public static get defaultConfig() { + return { + entity_id: "", + }; + } + + public render() { + const { value_template, entity_id, below, above } = this.condition; + + return html` + + + + + `; + } + + private _valueChanged(ev: CustomEvent): void { + handleChangeEvent(this, ev); + } + + private _entityPicked(ev) { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { ...this.condition, entity_id: ev.detail.value }, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-condition-numeric_state": HaNumericStateCondition; + } +} diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-or.ts b/src/panels/config/automation/condition/types/ha-automation-condition-or.ts new file mode 100644 index 0000000000..796b8a012e --- /dev/null +++ b/src/panels/config/automation/condition/types/ha-automation-condition-or.ts @@ -0,0 +1,11 @@ +import { HaLogicalCondition } from "./ha-automation-condition-logical"; +import { customElement } from "lit-element"; + +@customElement("ha-automation-condition-or") +export class HaOrCondition extends HaLogicalCondition {} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-condition-or": HaOrCondition; + } +} diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-state.ts b/src/panels/config/automation/condition/types/ha-automation-condition-state.ts new file mode 100644 index 0000000000..11ff6cc085 --- /dev/null +++ b/src/panels/config/automation/condition/types/ha-automation-condition-state.ts @@ -0,0 +1,59 @@ +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, + ConditionElement, +} from "../ha-automation-condition-row"; +import { PolymerChangedEvent } from "../../../../../polymer-types"; +import { StateCondition } from "../../../../../data/automation"; + +@customElement("ha-automation-condition-state") +export class HaStateCondition extends LitElement implements ConditionElement { + @property() public hass!: HomeAssistant; + @property() public condition!: StateCondition; + + public static get defaultConfig() { + return { entity_id: "", state: "" }; + } + + protected render() { + const { entity_id, state } = this.condition; + + return html` + + + `; + } + + private _valueChanged(ev: CustomEvent): void { + handleChangeEvent(this, ev); + } + + private _entityPicked(ev: PolymerChangedEvent) { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { ...this.condition, entity_id: ev.detail.value }, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-condition-state": HaStateCondition; + } +} diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-sun.ts b/src/panels/config/automation/condition/types/ha-automation-condition-sun.ts new file mode 100644 index 0000000000..8975e889fa --- /dev/null +++ b/src/panels/config/automation/condition/types/ha-automation-condition-sun.ts @@ -0,0 +1,107 @@ +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 { + handleChangeEvent, + ConditionElement, +} from "../ha-automation-condition-row"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { SunCondition } from "../../../../../data/automation"; + +@customElement("ha-automation-condition-sun") +export class HaSunCondition extends LitElement implements ConditionElement { + @property() public hass!: HomeAssistant; + @property() public condition!: SunCondition; + + public static get defaultConfig() { + return {}; + } + + protected render() { + const { after, after_offset, before, before_offset } = this.condition; + return html` + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.type.sun.sunrise" + )} + + + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.type.sun.sunset" + )} + + + + + + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.type.sun.sunrise" + )} + + + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.type.sun.sunset" + )} + + + + + `; + } + + private _valueChanged(ev: CustomEvent): void { + handleChangeEvent(this, ev); + } + + private _radioGroupPicked(ev) { + const key = ev.target.name; + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { + ...this.condition, + [key]: (ev.target as PaperRadioGroupElement).selected, + }, + }); + } +} diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-template.ts b/src/panels/config/automation/condition/types/ha-automation-condition-template.ts new file mode 100644 index 0000000000..b33cfa6d2f --- /dev/null +++ b/src/panels/config/automation/condition/types/ha-automation-condition-template.ts @@ -0,0 +1,34 @@ +import "../../../../../components/ha-textarea"; +import { LitElement, property, html, customElement } from "lit-element"; +import { HomeAssistant } from "../../../../../types"; +import { handleChangeEvent } from "../ha-automation-condition-row"; +import { TemplateCondition } from "../../../../../data/automation"; + +@customElement("ha-automation-condition-template") +export class HaTemplateCondition extends LitElement { + @property() public hass!: HomeAssistant; + @property() public condition!: TemplateCondition; + + public static get defaultConfig() { + return { value_template: "" }; + } + + protected render() { + const { value_template } = this.condition; + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + handleChangeEvent(this, ev); + } +} diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-time.ts b/src/panels/config/automation/condition/types/ha-automation-condition-time.ts new file mode 100644 index 0000000000..b42af4f4af --- /dev/null +++ b/src/panels/config/automation/condition/types/ha-automation-condition-time.ts @@ -0,0 +1,44 @@ +import "@polymer/paper-input/paper-input"; +import { LitElement, html, property, customElement } from "lit-element"; +import { HomeAssistant } from "../../../../../types"; +import { + handleChangeEvent, + ConditionElement, +} from "../ha-automation-condition-row"; +import { TimeCondition } from "../../../../../data/automation"; + +@customElement("ha-automation-condition-time") +export class HaTimeCondition extends LitElement implements ConditionElement { + @property() public hass!: HomeAssistant; + @property() public condition!: TimeCondition; + + public static get defaultConfig() { + return {}; + } + + protected render() { + const { after, before } = this.condition; + return html` + + + `; + } + + private _valueChanged(ev: CustomEvent): void { + handleChangeEvent(this, ev); + } +} diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts b/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts new file mode 100644 index 0000000000..edd2fac186 --- /dev/null +++ b/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts @@ -0,0 +1,78 @@ +import "@polymer/paper-radio-button/paper-radio-button"; +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 { PolymerChangedEvent } from "../../../../../polymer-types"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { ZoneCondition } from "../../../../../data/automation"; + +function zoneAndLocationFilter(stateObj) { + return hasLocation(stateObj) && computeStateDomain(stateObj) !== "zone"; +} + +@customElement("ha-automation-condition-zone") +export class HaZoneCondition extends LitElement { + @property() public hass!: HomeAssistant; + @property() public condition!: ZoneCondition; + + public static get defaultConfig() { + return { + entity_id: "", + zone: "", + }; + } + + protected render() { + const { entity_id, zone } = this.condition; + return html` + + + + `; + } + + private _entityPicked(ev: PolymerChangedEvent) { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { ...this.condition, entity_id: ev.detail.value }, + }); + } + + private _zonePicked(ev: PolymerChangedEvent) { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { ...this.condition, zone: ev.detail.value }, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-condition-zone": HaZoneCondition; + } +} diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index ba8985252b..51980e7c88 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -1,42 +1,37 @@ -import { - LitElement, - TemplateResult, - html, - CSSResult, - css, - PropertyValues, - property, -} from "lit-element"; 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 { classMap } from "lit-html/directives/class-map"; - -import { h, render } from "preact"; - -import "../../../components/ha-fab"; -import "../../../components/ha-paper-icon-button-arrow-prev"; -import "../../../layouts/ha-app-layout"; - -import Automation from "../js/automation"; -import unmountPreact from "../../../common/preact/unmount"; -import { computeStateName } from "../../../common/entity/compute_state_name"; - -import { haStyle } from "../../../resources/styles"; -import { HomeAssistant } from "../../../types"; import { - AutomationEntity, - AutomationConfig, - deleteAutomation, - getAutomationEditorInitData, -} from "../../../data/automation"; + css, + CSSResult, + html, + LitElement, + property, + PropertyValues, + TemplateResult, +} from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; +import { computeStateName } from "../../../common/entity/compute_state_name"; import { navigate } from "../../../common/navigate"; import { computeRTL } from "../../../common/util/compute_rtl"; +import "../../../components/ha-fab"; +import "../../../components/ha-paper-icon-button-arrow-prev"; +import { + AutomationConfig, + AutomationEntity, + Condition, + deleteAutomation, + getAutomationEditorInitData, + Trigger, +} from "../../../data/automation"; +import { Action } from "../../../data/script"; import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation"; - -function AutomationEditor(mountEl, props, mergeEl) { - return render(h(Automation, props), mountEl, mergeEl); -} +import "../../../layouts/ha-app-layout"; +import { haStyle } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import "./action/ha-automation-action"; +import "./condition/ha-automation-condition"; +import "./trigger/ha-automation-trigger"; export class HaAutomationEditor extends LitElement { @property() public hass!: HomeAssistant; @@ -45,26 +40,9 @@ export class HaAutomationEditor extends LitElement { @property() public creatingNew?: boolean; @property() private _config?: AutomationConfig; @property() private _dirty?: boolean; - private _rendered?: unknown; @property() private _errors?: string; - constructor() { - super(); - this._configChanged = this._configChanged.bind(this); - } - - public disconnectedCallback(): void { - super.disconnectedCallback(); - if (this._rendered) { - unmountPreact(this._rendered); - this._rendered = undefined; - } - } - protected render(): TemplateResult | void { - if (!this.hass) { - return; - } return html` @@ -100,11 +78,131 @@ export class HaAutomationEditor extends LitElement { ` : ""}
+ > + ${this._config + ? html` + + ${this._config.alias} + + ${this.hass.localize( + "ui.panel.config.automation.editor.introduction" + )} + + +
+ + + +
+
+
+ + + + ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.header" + )} + + +

+ ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.introduction" + )} +

+ + ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.learn_more" + )} + +
+ +
+ + + + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.header" + )} + + +

+ ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.introduction" + )} +

+ + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.learn_more" + )} + +
+ +
+ + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.header" + )} + + +

+ ${this.hass.localize( + "ui.panel.config.automation.editor.actions.introduction" + )} +

+ + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.learn_more" + )} + +
+ +
+ ` + : ""} +
@@ -202,8 +101,11 @@ export default class HaAutomationTriggerRow extends LitElement { slot="dropdown-trigger" > - - ${this._yamlMode + + ${yamlMode ? this.hass.localize( "ui.panel.config.automation.editor.edit_ui" ) @@ -224,10 +126,10 @@ export default class HaAutomationTriggerRow extends LitElement {
- ${this._yamlMode + ${yamlMode ? html`
- ${!hasEditor + ${selected === -1 ? html` ${this.hass.localize( "ui.panel.config.automation.editor.triggers.unsupported_platform", @@ -266,7 +168,7 @@ export default class HaAutomationTriggerRow extends LitElement {
- ${dynamicContentDirective( + ${dynamicElement( `ha-automation-trigger-${this.trigger.platform}`, { hass: this.hass, trigger: this.trigger } )} diff --git a/src/panels/config/automation/trigger/ha-automation-trigger.ts b/src/panels/config/automation/trigger/ha-automation-trigger.ts index 108c4c0248..71f2e303fd 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger.ts @@ -13,41 +13,42 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { HomeAssistant } from "../../../../types"; import "./ha-automation-trigger-row"; +import { HaStateTrigger } from "./types/ha-automation-trigger-state"; +import { Trigger } from "../../../../data/automation"; @customElement("ha-automation-trigger") export default class HaAutomationTrigger extends LitElement { @property() public hass!: HomeAssistant; - @property() public triggers; + @property() public triggers!: Trigger[]; protected render() { return html` -
- ${this.triggers.map( - (trg, idx) => html` - - ` - )} - -
- - ${this.hass.localize( - "ui.panel.config.automation.editor.triggers.add" - )} - -
-
-
+ ${this.triggers.map( + (trg, idx) => html` + + ` + )} + +
+ + ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.add" + )} + +
+
`; } private _addTrigger() { const triggers = this.triggers.concat({ platform: "state", + ...HaStateTrigger.defaultConfig, }); fireEvent(this, "value-changed", { value: triggers }); @@ -70,12 +71,9 @@ export default class HaAutomationTrigger extends LitElement { static get styles(): CSSResult { return css` - .triggers, - .script { - margin-top: -16px; - } - .triggers ha-card, - .script ha-card { + ha-automation-trigger-row, + ha-card { + display: block; margin-top: 16px; } .add-card mwc-button { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts index 8ea5a96cfe..2dae62ad1b 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts @@ -28,9 +28,8 @@ export class HaDeviceTrigger extends LitElement { } protected render() { - if (this._deviceId === undefined) { - this._deviceId = this.trigger.device_id; - } + const deviceId = this._deviceId || this.trigger.device_id; + const extraFieldsData = this._capabilities && this._capabilities.extra_fields ? this._capabilities.extra_fields.map((item) => { @@ -40,14 +39,14 @@ export class HaDeviceTrigger extends LitElement { return html` ${this.entities.length - ? this.entities.map((entry: EntityRegistryStateEntry) => { - if (!this._showDisabled && entry.disabled_by) { - return ""; - } - const stateObj = this.hass.states[entry.entity_id]; - return html` - - ${stateObj - ? html` - - ` - : html` - - `} - -
${entry.stateName}
-
${entry.entity_id}
-
-
+ ? html` + ${this.entities.map((entry: EntityRegistryStateEntry) => { + if (!this._showDisabled && entry.disabled_by) { + return ""; + } + const stateObj = this.hass.states[entry.entity_id]; + return html` + ${stateObj ? html` - + .stateObj=${stateObj} + slot="item-icon" + > ` - : ""} - -
-
- `; - }) + : html` + + `} + +
${entry.stateName}
+
${entry.entity_id}
+
+
+ ${stateObj + ? html` + + ` + : ""} + +
+ + `; + })} +
+ + ${this.hass.localize( + "ui.panel.config.devices.entities.add_entities_lovelace" + )} + +
+ ` : html`
@@ -125,6 +135,14 @@ export class HaDeviceEntitiesCard extends LitElement { fireEvent(this, "hass-more-info", { entityId: entry.entity_id }); } + private _addToLovelaceView(): void { + addEntitiesToLovelaceView( + this, + this.hass, + this.entities.map((entity) => entity.entity_id) + ); + } + static get styles(): CSSResult { return css` ha-icon { diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 70ba49d703..2bc8d4fb0d 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -156,7 +156,9 @@ export class HaConfigDevicePage extends LitElement { ${entities.length ? html`
- ${this.hass.localize("ui.panel.config.devices.entities")} + ${this.hass.localize( + "ui.panel.config.devices.entities.entities" + )}
${this.hass.localize( "ui.panel.config.entity_registry.editor.delete" @@ -201,13 +200,8 @@ class DialogEntityRegistryDetail extends LitElement { private _confirmDeleteEntry(): void { showConfirmationDialog(this, { - title: this.hass.localize( - "ui.panel.config.entity_registry.editor.confirm_delete" - ), text: this.hass.localize( - "ui.panel.config.entity_registry.editor.confirm_delete2", - "platform", - this._platform + "ui.panel.config.entity_registry.editor.confirm_delete" ), confirm: () => this._deleteEntry(), }); 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 e7854ece8f..3b3fec5eff 100644 --- a/src/panels/config/entity_registry/ha-config-entity-registry.ts +++ b/src/panels/config/entity_registry/ha-config-entity-registry.ts @@ -5,19 +5,28 @@ import { css, CSSResult, property, + query, } from "lit-element"; +import { styleMap } from "lit-html/directives/style-map"; + +import "@polymer/paper-checkbox/paper-checkbox"; +import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; +import "@polymer/paper-item/paper-icon-item"; +import "@polymer/paper-listbox/paper-listbox"; +import "@polymer/paper-tooltip/paper-tooltip"; import { HomeAssistant } from "../../../types"; import { EntityRegistryEntry, computeEntityRegistryName, subscribeEntityRegistry, + removeEntityRegistryEntry, + updateEntityRegistryEntry, } from "../../../data/entity_registry"; import "../../../layouts/hass-subpage"; import "../../../layouts/hass-loading-screen"; import "../../../components/data-table/ha-data-table"; import "../../../components/ha-icon"; -import "../../../components/ha-switch"; import { domainIcon } from "../../../common/entity/domain_icon"; import { stateIcon } from "../../../common/entity/state_icon"; import { computeDomain } from "../../../common/entity/compute_domain"; @@ -26,25 +35,33 @@ import { loadEntityRegistryDetailDialog, } from "./show-dialog-entity-registry-detail"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; -// tslint:disable-next-line -import { HaSwitch } from "../../../components/ha-switch"; import memoize from "memoize-one"; // tslint:disable-next-line import { DataTableColumnContainer, RowClickedEvent, + SelectionChangedEvent, + HaDataTable, + DataTableColumnData, } from "../../../components/data-table/ha-data-table"; +import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation"; class HaConfigEntityRegistry extends LitElement { @property() public hass!: HomeAssistant; - @property() public isWide?: boolean; + @property() public isWide!: boolean; + @property() public narrow!: boolean; @property() private _entities?: EntityRegistryEntry[]; @property() private _showDisabled = false; + @property() private _showUnavailable = true; + @property() private _filter = ""; + @property() private _selectedEntities: string[] = []; + @query("ha-data-table") private _dataTable!: HaDataTable; + private _unsubEntities?: UnsubscribeFunc; private _columns = memoize( - (_language): DataTableColumnContainer => { - return { + (narrow, _language): DataTableColumnContainer => { + const columns: DataTableColumnContainer = { icon: { title: "", type: "icon", @@ -60,58 +77,123 @@ class HaConfigEntityRegistry extends LitElement { filterable: true, direction: "asc", }, - entity_id: { - title: this.hass.localize( - "ui.panel.config.entity_registry.picker.headers.entity_id" - ), - sortable: true, - filterable: true, - }, - platform: { - title: this.hass.localize( - "ui.panel.config.entity_registry.picker.headers.integration" - ), - sortable: true, - filterable: true, - template: (platform) => - html` - ${this.hass.localize(`component.${platform}.config.title`) || - platform} - `, - }, - disabled_by: { - title: this.hass.localize( - "ui.panel.config.entity_registry.picker.headers.enabled" - ), - type: "icon", - template: (disabledBy) => html` - - `, - }, }; + + const statusColumn: DataTableColumnData = { + title: this.hass.localize( + "ui.panel.config.entity_registry.picker.headers.status" + ), + type: "icon", + sortable: true, + filterable: true, + template: (_status, entity: any) => + entity.unavailable || entity.disabled_by + ? html` +
+ + + ${entity.unavailable + ? this.hass.localize( + "ui.panel.config.entity_registry.picker.status.unavailable" + ) + : this.hass.localize( + "ui.panel.config.entity_registry.picker.status.disabled" + )} + +
+ ` + : "", + }; + + if (narrow) { + columns.name.template = (name, entity: any) => { + return html` + ${name}
+ ${entity.entity_id} | + ${this.hass.localize(`component.${entity.platform}.config.title`) || + entity.platform} + `; + }; + columns.status = statusColumn; + return columns; + } + + columns.entity_id = { + title: this.hass.localize( + "ui.panel.config.entity_registry.picker.headers.entity_id" + ), + sortable: true, + filterable: true, + }; + columns.platform = { + title: this.hass.localize( + "ui.panel.config.entity_registry.picker.headers.integration" + ), + sortable: true, + filterable: true, + template: (platform) => + this.hass.localize(`component.${platform}.config.title`) || platform, + }; + columns.status = statusColumn; + + return columns; } ); private _filteredEntities = memoize( - (entities: EntityRegistryEntry[], showDisabled: boolean) => - (showDisabled - ? entities - : entities.filter((entity) => !Boolean(entity.disabled_by)) - ).map((entry) => { + ( + entities: EntityRegistryEntry[], + showDisabled: boolean, + showUnavailable: boolean + ) => { + if (!showDisabled) { + entities = entities.filter((entity) => !Boolean(entity.disabled_by)); + } + + return entities.reduce((result, entry) => { const state = this.hass!.states[entry.entity_id]; - return { + + const unavailable = + state && (state.state === "unavailable" || state.attributes.restored); // if there is not state it is disabled + + if (!showUnavailable && unavailable) { + return result; + } + + result.push({ ...entry, icon: state ? stateIcon(state) : domainIcon(computeDomain(entry.entity_id)), name: computeEntityRegistryName(this.hass!, entry) || - this.hass!.localize("state.default.unavailable"), - }; - }) + this.hass.localize("state.default.unavailable"), + unavailable, + status: unavailable + ? this.hass.localize( + "ui.panel.config.entity_registry.picker.status.unavailable" + ) + : entry.disabled_by + ? this.hass.localize( + "ui.panel.config.entity_registry.picker.status.disabled" + ) + : this.hass.localize( + "ui.panel.config.entity_registry.picker.status.ok" + ), + }); + return result; + }, [] as any); + } ); public disconnectedCallback() { @@ -133,17 +215,19 @@ class HaConfigEntityRegistry extends LitElement { "ui.panel.config.entity_registry.caption" )}" > -
-
-

- ${this.hass.localize( - "ui.panel.config.entity_registry.picker.header" - )} -

-

- ${this.hass.localize( - "ui.panel.config.entity_registry.picker.introduction" - )} +

+
+

+ ${this.hass.localize( + "ui.panel.config.entity_registry.picker.header" + )} +

+

+ ${this.hass.localize( + "ui.panel.config.entity_registry.picker.introduction" + )} +

+

${this.hass.localize( "ui.panel.config.entity_registry.picker.introduction2" @@ -154,22 +238,123 @@ class HaConfigEntityRegistry extends LitElement { "ui.panel.config.entity_registry.picker.integrations_page" )} - ${this.hass.localize( - "ui.panel.config.entity_registry.picker.show_disabled" - )} -

-

- - + +
+ ${this._selectedEntities.length + ? html` +

+ ${this.hass.localize( + "ui.panel.config.entity_registry.picker.selected", + "number", + this._selectedEntities.length + )} +

+
+ ${!this.narrow + ? html` + ${this.hass.localize( + "ui.panel.config.entity_registry.picker.enable_selected.button" + )} + ${this.hass.localize( + "ui.panel.config.entity_registry.picker.disable_selected.button" + )} + ${this.hass.localize( + "ui.panel.config.entity_registry.picker.remove_selected.button" + )} + ` + : html` + + + ${this.hass.localize( + "ui.panel.config.entity_registry.picker.enable_selected.button" + )} + + + + ${this.hass.localize( + "ui.panel.config.entity_registry.picker.disable_selected.button" + )} + + + + ${this.hass.localize( + "ui.panel.config.entity_registry.picker.remove_selected.button" + )} + + `} +
+ ` + : html` + + + + + + + ${this.hass!.localize( + "ui.panel.config.entity_registry.picker.filter.show_disabled" + )} + + + + ${this.hass!.localize( + "ui.panel.config.entity_registry.picker.filter.show_unavailable" + )} + + + + `} +
+
`; @@ -192,8 +377,103 @@ class HaConfigEntityRegistry extends LitElement { } } - private _showDisabledChanged(ev: Event) { - this._showDisabled = (ev.target as HaSwitch).checked; + private _showDisabledChanged() { + this._showDisabled = !this._showDisabled; + } + + private _showRestoredChanged() { + this._showUnavailable = !this._showUnavailable; + } + + private _handleSearchChange(ev: CustomEvent) { + this._filter = ev.detail.value; + } + + private _handleSelectionChanged(ev: CustomEvent): void { + const changedSelection = ev.detail as SelectionChangedEvent; + const entity = changedSelection.id; + if (changedSelection.selected) { + this._selectedEntities = [...this._selectedEntities, entity]; + } else { + this._selectedEntities = this._selectedEntities.filter( + (entityId) => entityId !== entity + ); + } + } + + private _enableSelected() { + showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.entity_registry.picker.enable_selected.confirm_title", + "number", + this._selectedEntities.length + ), + text: this.hass.localize( + "ui.panel.config.entity_registry.picker.enable_selected.confirm_text" + ), + confirmBtnText: this.hass.localize("ui.common.yes"), + cancelBtnText: this.hass.localize("ui.common.no"), + confirm: () => { + this._selectedEntities.forEach((entity) => + updateEntityRegistryEntry(this.hass, entity, { + disabled_by: null, + }) + ); + this._clearSelection(); + }, + }); + } + + private _disableSelected() { + showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.entity_registry.picker.disable_selected.confirm_title", + "number", + this._selectedEntities.length + ), + text: this.hass.localize( + "ui.panel.config.entity_registry.picker.disable_selected.confirm_text" + ), + confirmBtnText: this.hass.localize("ui.common.yes"), + cancelBtnText: this.hass.localize("ui.common.no"), + confirm: () => { + this._selectedEntities.forEach((entity) => + updateEntityRegistryEntry(this.hass, entity, { + disabled_by: "user", + }) + ); + this._clearSelection(); + }, + }); + } + + private _removeSelected() { + const removeableEntities = this._selectedEntities.filter((entity) => { + const stateObj = this.hass.states[entity]; + return stateObj?.attributes.restored; + }); + showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.entity_registry.picker.remove_selected.confirm_title", + "number", + removeableEntities.length + ), + text: this.hass.localize( + "ui.panel.config.entity_registry.picker.remove_selected.confirm_text" + ), + confirmBtnText: this.hass.localize("ui.common.yes"), + cancelBtnText: this.hass.localize("ui.common.no"), + confirm: () => { + removeableEntities.forEach((entity) => + removeEntityRegistryEntry(this.hass, entity) + ); + this._clearSelection(); + }, + }); + } + + private _clearSelection() { + this._dataTable.clearSelection(); } private _openEditEntry(ev: CustomEvent): void { @@ -237,18 +517,35 @@ class HaConfigEntityRegistry extends LitElement { opacity: var(--dark-primary-opacity); } .intro { - padding: 24px 16px 0; + padding: 24px 16px; } .content { padding: 4px; } ha-data-table { - margin-bottom: 24px; - margin-top: 0px; + width: 100%; } ha-switch { margin-top: 16px; } + .table-header { + display: flex; + justify-content: space-between; + align-items: flex-end; + border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12); + } + search-input { + flex-grow: 1; + } + .selected-txt { + font-weight: bold; + margin-top: 38px; + padding-left: 16px; + } + .header-btns > mwc-button, + .header-btns > paper-icon-button { + margin: 8px; + } `; } } diff --git a/src/panels/config/integrations/ha-config-entries-dashboard.ts b/src/panels/config/integrations/ha-config-entries-dashboard.ts index ecb02e9131..3c7f4c3370 100644 --- a/src/panels/config/integrations/ha-config-entries-dashboard.ts +++ b/src/panels/config/integrations/ha-config-entries-dashboard.ts @@ -2,6 +2,7 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes"; import "@polymer/paper-tooltip/paper-tooltip"; import "@material/mwc-button"; import "@polymer/iron-icon/iron-icon"; +import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item-body"; @@ -23,7 +24,11 @@ import { loadConfigFlowDialog, showConfigFlowDialog, } from "../../../dialogs/config-flow/show-dialog-config-flow"; -import { localizeConfigFlowTitle } from "../../../data/config_flow"; +import { + localizeConfigFlowTitle, + ignoreConfigFlow, + DISCOVERY_SOURCES, +} from "../../../data/config_flow"; import { LitElement, TemplateResult, @@ -34,10 +39,11 @@ import { CSSResult, } from "lit-element"; import { HomeAssistant } from "../../../types"; -import { ConfigEntry } from "../../../data/config_entries"; +import { ConfigEntry, deleteConfigEntry } from "../../../data/config_entries"; import { fireEvent } from "../../../common/dom/fire_event"; import { EntityRegistryEntry } from "../../../data/entity_registry"; import { DataEntryFlowProgress } from "../../../data/data_entry_flow"; +import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation"; @customElement("ha-config-entries-dashboard") export class HaConfigManagerDashboard extends LitElement { @@ -56,6 +62,7 @@ export class HaConfigManagerDashboard extends LitElement { * For example, can be discovered devices that require more config. */ @property() private configEntriesInProgress!: DataEntryFlowProgress[]; + @property() private _showIgnored = false; public connectedCallback() { super.connectedCallback(); @@ -67,6 +74,67 @@ export class HaConfigManagerDashboard extends LitElement { + + + + + ${this.hass.localize( + this._showIgnored + ? "ui.panel.config.integrations.ignore.hide_ignored" + : "ui.panel.config.integrations.ignore.show_ignored" + )} + + + + + ${this._showIgnored + ? html` + + ${this.hass.localize( + "ui.panel.config.integrations.ignore.ignored" + )} + + ${this.configEntries + .filter((item) => item.source === "ignore") + .map( + (item: ConfigEntry) => html` + + + ${this.hass.localize( + `component.${item.domain}.config.title` + )} + + + + ` + )} + + + ` + : ""} ${this.configEntriesInProgress.length ? html` @@ -82,9 +150,22 @@ export class HaConfigManagerDashboard extends LitElement { ${localizeConfigFlowTitle(this.hass.localize, flow)} + ${DISCOVERY_SOURCES.includes(flow.context.source) && + flow.context.unique_id + ? html` + + ${this.hass.localize( + "ui.panel.config.integrations.ignore.ignore" + )} + + ` + : ""} ${this.hass.localize( "ui.panel.config.integrations.configure" )} - ${this.hass.localize( - "ui.panel.config.integrations.configured" - )} + + ${this.hass.localize("ui.panel.config.integrations.configured")} + ${this.entityRegistryEntries.length - ? this.configEntries.map( - (item: any, idx) => html` - - - -
- ${this.hass.localize( - `component.${item.domain}.config.title` - )}: - ${item.title} -
-
- ${this._getEntities(item).map( - (entity) => html` - - - ${computeStateName(entity)} - - ` - )} -
-
- -
-
- ` + ? this.configEntries.map((item: any, idx) => + item.source === "ignore" + ? "" + : html` + + + +
+ ${this.hass.localize( + `component.${item.domain}.config.title` + )}: + ${item.title} +
+
+ ${this._getEntities(item).map( + (entity) => html` + + + ${computeStateName( + entity + )} + + ` + )} +
+
+ +
+
+ ` ) : html`
@@ -176,12 +259,64 @@ export class HaConfigManagerDashboard extends LitElement { private _continueFlow(ev: Event) { showConfigFlowDialog(this, { - continueFlowId: - (ev.target as HTMLElement).getAttribute("data-id") || undefined, + continueFlowId: (ev.target! as any).flowId, dialogClosedCallback: () => fireEvent(this, "hass-reload-entries"), }); } + private _ignoreFlow(ev: Event) { + const flow = (ev.target! as any).flow; + showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.config.integrations.ignore.confirm_ignore_title", + "name", + localizeConfigFlowTitle(this.hass.localize, flow) + ), + text: this.hass!.localize( + "ui.panel.config.integrations.ignore.confirm_ignore" + ), + confirmBtnText: this.hass!.localize( + "ui.panel.config.integrations.ignore.ignore" + ), + confirm: () => { + ignoreConfigFlow(this.hass, flow.flow_id); + fireEvent(this, "hass-reload-entries"); + }, + }); + } + + private _toggleShowIgnored() { + this._showIgnored = !this._showIgnored; + } + + private async _removeIgnoredIntegration(ev: Event) { + const entry = (ev.target! as any).entry; + showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.config.integrations.ignore.confirm_delete_ignore_title", + "name", + this.hass.localize(`component.${entry.domain}.config.title`) + ), + text: this.hass!.localize( + "ui.panel.config.integrations.ignore.confirm_delete_ignore" + ), + confirmBtnText: this.hass!.localize( + "ui.panel.config.integrations.ignore.stop_ignore" + ), + confirm: async () => { + const result = await deleteConfigEntry(this.hass, entry.entry_id); + if (result.require_restart) { + alert( + this.hass.localize( + "ui.panel.config.integrations.config_entry.restart_confirm" + ) + ); + } + fireEvent(this, "hass-reload-entries"); + }, + }); + } + private _getEntities(configEntry: ConfigEntry): HassEntity[] { if (!this.entityRegistryEntries) { return []; @@ -203,8 +338,7 @@ export class HaConfigManagerDashboard extends LitElement { overflow: hidden; } mwc-button { - top: 3px; - margin-right: -0.57em; + align-self: center; } .config-entry-row { display: flex; @@ -229,6 +363,9 @@ export class HaConfigManagerDashboard extends LitElement { right: auto; left: 16px; } + .overflow { + width: 56px; + } `; } } diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index 8e97de6e37..ac30b659a1 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -116,7 +116,7 @@ class HaConfigIntegrations extends HassRouterPage { private _loadData() { getConfigEntries(this.hass).then((configEntries) => { this._configEntries = configEntries.sort((conf1, conf2) => - compare(conf1.title, conf2.title) + compare(conf1.domain + conf1.title, conf2.domain + conf2.title) ); }); if (this._unsubs) { diff --git a/src/panels/config/js/automation-component.tsx b/src/panels/config/js/automation-component.tsx deleted file mode 100644 index 1facca8f27..0000000000 --- a/src/panels/config/js/automation-component.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { h, Component, ComponentChild } from "preact"; - -export class AutomationComponent

extends Component { - // @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

; - } -} diff --git a/src/panels/config/js/automation.tsx b/src/panels/config/js/automation.tsx deleted file mode 100644 index fdabbc953b..0000000000 --- a/src/panels/config/js/automation.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { h, Component } from "preact"; - -import "@polymer/paper-input/paper-input"; -import "../ha-config-section"; -import "../../../components/ha-card"; -import "../../../components/ha-textarea"; - -import "../automation/trigger/ha-automation-trigger"; - -import Condition from "./condition/index"; -import Script from "./script/index"; - -export default class Automation extends Component { - constructor() { - super(); - - this.onChange = this.onChange.bind(this); - this.triggerChanged = this.triggerChanged.bind(this); - this.conditionChanged = this.conditionChanged.bind(this); - this.actionChanged = this.actionChanged.bind(this); - } - - public onChange(ev) { - this.props.onChange({ - ...this.props.automation, - [ev.target.name]: ev.target.value, - }); - } - - public triggerChanged(ev: CustomEvent) { - this.props.onChange({ ...this.props.automation, trigger: ev.detail.value }); - } - - public conditionChanged(condition) { - this.props.onChange({ ...this.props.automation, condition }); - } - - public actionChanged(action) { - this.props.onChange({ ...this.props.automation, action }); - } - - public render({ automation, isWide, hass, localize }) { - const { alias, description, trigger, condition, action } = automation; - - return ( -
- - {alias} - - {localize("ui.panel.config.automation.editor.introduction")} - - -
- - -
-
-
- - - - {localize("ui.panel.config.automation.editor.triggers.header")} - - -

- {localize( - "ui.panel.config.automation.editor.triggers.introduction" - )} -

- - {localize( - "ui.panel.config.automation.editor.triggers.learn_more" - )} - -
- -
- - - - {localize("ui.panel.config.automation.editor.conditions.header")} - - -

- {localize( - "ui.panel.config.automation.editor.conditions.introduction" - )} -

- - {localize( - "ui.panel.config.automation.editor.conditions.learn_more" - )} - -
- -
- - - - {localize("ui.panel.config.automation.editor.actions.header")} - - -

- {localize( - "ui.panel.config.automation.editor.actions.introduction" - )} -

- - {localize("ui.panel.config.automation.editor.actions.learn_more")} - -
-