diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6721eb46bd..0aa8195d5a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,6 @@ diff --git a/build-scripts/gulp/gather-static.js b/build-scripts/gulp/gather-static.js index d1b5ff0090..3d80035e17 100644 --- a/build-scripts/gulp/gather-static.js +++ b/build-scripts/gulp/gather-static.js @@ -104,7 +104,10 @@ gulp.task("compress-static", () => compressStatic(paths.static)); gulp.task("copy-static-demo", (done) => { // Copy app static files - fs.copySync(polyPath("public"), paths.demo_root); + fs.copySync( + polyPath("public/static"), + path.resolve(paths.demo_root, "static") + ); // Copy demo static files fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_root); diff --git a/cast/public/_headers b/cast/public/_headers new file mode 100644 index 0000000000..1f191e2720 --- /dev/null +++ b/cast/public/_headers @@ -0,0 +1,20 @@ +/* + Cache-Control: public, max-age: 0, s-maxage=3600, must-revalidate + Content-Security-Policy: form-action https: + Feature-Policy: vibrate 'none'; geolocation 'none'; midi 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; speaker 'none'; vibrate 'none'; payment 'none' + Referrer-Policy: no-referrer-when-downgrade + X-Content-Type-Options: nosniff + X-Frame-Options: DENY + X-XSS-Protection: 1; mode=block + +/images/* + Cache-Control: public, max-age: 604800, s-maxage=604800 + +/manifest.json + Cache-Control: public, max-age: 3600, s-maxage=3600 + +/frontend_es5/* + Cache-Control: public, max-age: 604800, s-maxage=604800 + +/frontend_latest/* + Cache-Control: public, max-age: 604800, s-maxage=604800 diff --git a/demo/public/_headers b/demo/public/_headers new file mode 100644 index 0000000000..1a4137c675 --- /dev/null +++ b/demo/public/_headers @@ -0,0 +1,18 @@ +/* + Cache-Control: public, max-age: 0, s-maxage=3600, must-revalidate + Content-Security-Policy: form-action https: + Referrer-Policy: no-referrer-when-downgrade + X-Content-Type-Options: nosniff + X-XSS-Protection: 1; mode=block + +/api/* + Cache-Control: public, max-age: 604800, s-maxage=604800 + +/assets/* + Cache-Control: public, max-age: 604800, s-maxage=604800 + +/frontend_es5/* + Cache-Control: public, max-age: 604800, s-maxage=604800 + +/frontend_latest/* + Cache-Control: public, max-age: 604800, s-maxage=604800 diff --git a/package.json b/package.json index 97a817f16a..00ead71952 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@material/mwc-fab": "^0.8.0", "@material/mwc-ripple": "^0.8.0", "@material/mwc-switch": "^0.8.0", - "@mdi/svg": "4.4.95", + "@mdi/svg": "4.5.95", "@polymer/app-layout": "^3.0.2", "@polymer/app-localize-behavior": "^3.0.1", "@polymer/app-route": "^3.0.2", @@ -74,7 +74,7 @@ "@webcomponents/webcomponentsjs": "^2.2.7", "chart.js": "~2.8.0", "chartjs-chart-timeline": "^0.3.0", - "codemirror": "^5.45.0", + "codemirror": "^5.49.0", "cpx": "^1.5.0", "deep-clone-simple": "^1.1.1", "es6-object-assign": "^1.1.0", @@ -117,6 +117,7 @@ "@types/chai": "^4.1.7", "@types/chromecast-caf-receiver": "^3.0.12", "@types/chromecast-caf-sender": "^1.0.1", + "@types/codemirror": "^0.0.78", "@types/hls.js": "^0.12.3", "@types/leaflet": "^1.4.3", "@types/memoize-one": "4.1.0", diff --git a/setup.py b/setup.py index 3dcbfa87f7..a71239fc8f 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20191002.2", + version="20191014.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 7765a95ab0..d009bd8c77 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -59,31 +59,31 @@ export interface SortingChangedEvent { export type SortingDirection = "desc" | "asc" | null; -export interface DataTabelColumnContainer { - [key: string]: DataTabelColumnData; +export interface DataTableColumnContainer { + [key: string]: DataTableColumnData; } -export interface DataTabelSortColumnData { +export interface DataTableSortColumnData { sortable?: boolean; filterable?: boolean; filterKey?: string; direction?: SortingDirection; } -export interface DataTabelColumnData extends DataTabelSortColumnData { +export interface DataTableColumnData extends DataTableSortColumnData { title: string; - type?: "numeric"; - template?: (data: any) => TemplateResult; + type?: "numeric" | "icon"; + template?: (data: any, row: T) => TemplateResult; } -export interface DataTabelRowData { +export interface DataTableRowData { [key: string]: any; } @customElement("ha-data-table") export class HaDataTable extends BaseElement { - @property({ type: Object }) public columns: DataTabelColumnContainer = {}; - @property({ type: Array }) public data: DataTabelRowData[] = []; + @property({ type: Object }) public columns: DataTableColumnContainer = {}; + @property({ type: Array }) public data: DataTableRowData[] = []; @property({ type: Boolean }) public selectable = false; @property({ type: String }) public id = "id"; protected mdcFoundation!: MDCDataTableFoundation; @@ -98,9 +98,9 @@ export class HaDataTable extends BaseElement { @property({ type: String }) private _filter = ""; @property({ type: String }) private _sortColumn?: string; @property({ type: String }) private _sortDirection: SortingDirection = null; - @property({ type: Array }) private _filteredData: DataTabelRowData[] = []; + @property({ type: Array }) private _filteredData: DataTableRowData[] = []; private _sortColumns: { - [key: string]: DataTabelSortColumnData; + [key: string]: DataTableSortColumnData; } = {}; private curRequest = 0; private _worker: any | undefined; @@ -134,8 +134,8 @@ export class HaDataTable extends BaseElement { } } - const clonedColumns: DataTabelColumnContainer = deepClone(this.columns); - Object.values(clonedColumns).forEach((column: DataTabelColumnData) => { + const clonedColumns: DataTableColumnContainer = deepClone(this.columns); + Object.values(clonedColumns).forEach((column: DataTableColumnData) => { delete column.title; delete column.type; delete column.template; @@ -190,9 +190,12 @@ export class HaDataTable extends BaseElement { const [key, column] = columnEntry; const sorted = key === this._sortColumn; const classes = { - "mdc-data-table__cell--numeric": Boolean( + "mdc-data-table__header-cell--numeric": Boolean( column.type && column.type === "numeric" ), + "mdc-data-table__header-cell--icon": Boolean( + column.type && column.type === "icon" + ), sortable: Boolean(column.sortable), "not-sorted": Boolean(column.sortable && !sorted), }; @@ -222,8 +225,8 @@ export class HaDataTable extends BaseElement { ${repeat( this._filteredData!, - (row: DataTabelRowData) => row[this.id], - (row: DataTabelRowData) => html` + (row: DataTableRowData) => row[this.id], + (row: DataTableRowData) => html` ${column.template - ? column.template(row[key]) + ? column.template(row[key], row) : row[key]} `; @@ -516,6 +522,11 @@ export class HaDataTable extends BaseElement { text-align: left; } + .mdc-data-table__cell--icon { + color: var(--secondary-text-color); + text-align: center; + } + .mdc-data-table__header-cell { font-family: Roboto, sans-serif; -moz-osx-font-smoothing: grayscale; @@ -543,6 +554,10 @@ export class HaDataTable extends BaseElement { text-align: left; } + .mdc-data-table__header-cell--icon { + text-align: center; + } + /* custom from here */ .mdc-data-table { @@ -554,7 +569,7 @@ export class HaDataTable extends BaseElement { .mdc-data-table__header-cell.sortable { cursor: pointer; } - .mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__cell--numeric) + .mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon) span { position: relative; left: -24px; @@ -565,7 +580,7 @@ export class HaDataTable extends BaseElement { .mdc-data-table__header-cell.not-sorted ha-icon { left: -36px; } - .mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__cell--numeric):hover + .mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon):hover span { left: 0px; } diff --git a/src/components/data-table/sort_filter_worker.ts b/src/components/data-table/sort_filter_worker.ts index 47e36007b9..bd4eb831d3 100644 --- a/src/components/data-table/sort_filter_worker.ts +++ b/src/components/data-table/sort_filter_worker.ts @@ -1,7 +1,7 @@ import { - DataTabelColumnContainer, - DataTabelColumnData, - DataTabelRowData, + DataTableColumnContainer, + DataTableColumnData, + DataTableRowData, SortingDirection, } from "./ha-data-table"; @@ -9,8 +9,8 @@ import memoizeOne from "memoize-one"; export const filterSortData = memoizeOne( async ( - data: DataTabelRowData[], - columns: DataTabelColumnContainer, + data: DataTableRowData[], + columns: DataTableColumnContainer, filter: string, direction: SortingDirection, sortColumn?: string @@ -27,8 +27,8 @@ export const filterSortData = memoizeOne( const _memFilterData = memoizeOne( async ( - data: DataTabelRowData[], - columns: DataTabelColumnContainer, + data: DataTableRowData[], + columns: DataTableColumnContainer, filter: string ) => { if (!filter) { @@ -40,8 +40,8 @@ const _memFilterData = memoizeOne( const _memSortData = memoizeOne( ( - data: DataTabelRowData[], - columns: DataTabelColumnContainer, + data: DataTableRowData[], + columns: DataTableColumnContainer, direction: SortingDirection, sortColumn: string ) => { @@ -50,8 +50,8 @@ const _memSortData = memoizeOne( ); export const filterData = ( - data: DataTabelRowData[], - columns: DataTabelColumnContainer, + data: DataTableRowData[], + columns: DataTableColumnContainer, filter: string ) => data.filter((row) => { @@ -71,8 +71,8 @@ export const filterData = ( }); export const sortData = ( - data: DataTabelRowData[], - column: DataTabelColumnData, + data: DataTableRowData[], + column: DataTableColumnData, direction: SortingDirection, sortColumn: string ) => diff --git a/src/components/ha-code-editor.ts b/src/components/ha-code-editor.ts new file mode 100644 index 0000000000..fcbc24306b --- /dev/null +++ b/src/components/ha-code-editor.ts @@ -0,0 +1,160 @@ +import { loadCodeMirror } from "../resources/codemirror.ondemand"; +import { fireEvent } from "../common/dom/fire_event"; +import { + UpdatingElement, + property, + customElement, + PropertyValues, +} from "lit-element"; +import { Editor } from "codemirror"; + +declare global { + interface HASSDomEvents { + "editor-save": undefined; + } +} + +@customElement("ha-code-editor") +export class HaCodeEditor extends UpdatingElement { + public codemirror?: Editor; + @property() public mode?: string; + @property() public autofocus = false; + @property() public rtl = false; + @property() public error = false; + @property() private _value = ""; + + public set value(value: string) { + this._value = value; + } + + public get value(): string { + return this.codemirror ? this.codemirror.getValue() : this._value; + } + + public get hasComments(): boolean { + return this.shadowRoot!.querySelector("span.cm-comment") ? true : false; + } + + public connectedCallback() { + super.connectedCallback(); + if (!this.codemirror) { + return; + } + this.codemirror.refresh(); + if (this.autofocus !== false) { + this.codemirror.focus(); + } + } + + protected update(changedProps: PropertyValues): void { + super.update(changedProps); + + if (!this.codemirror) { + return; + } + + if (changedProps.has("mode")) { + this.codemirror.setOption("mode", this.mode); + } + if (changedProps.has("autofocus")) { + this.codemirror.setOption("autofocus", this.autofocus !== false); + } + if (changedProps.has("_value") && this._value !== this.value) { + this.codemirror.setValue(this._value); + } + if (changedProps.has("rtl")) { + this.codemirror.setOption("gutters", this._calcGutters()); + this._setScrollBarDirection(); + } + if (changedProps.has("error")) { + this.classList.toggle("error-state", this.error); + } + } + + protected firstUpdated(changedProps: PropertyValues): void { + super.firstUpdated(changedProps); + this._load(); + } + + private async _load(): Promise { + const loaded = await loadCodeMirror(); + + const codeMirror = loaded.codeMirror; + + const shadowRoot = this.attachShadow({ mode: "open" }); + + shadowRoot!.innerHTML = ` + `; + + this.codemirror = codeMirror(shadowRoot, { + value: this._value, + lineNumbers: true, + tabSize: 2, + mode: this.mode, + autofocus: this.autofocus !== false, + viewportMargin: Infinity, + extraKeys: { + Tab: "indentMore", + "Shift-Tab": "indentLess", + }, + gutters: this._calcGutters(), + }); + this._setScrollBarDirection(); + this.codemirror!.on("changes", () => this._onChange()); + } + + private _onChange(): void { + const newValue = this.value; + if (newValue === this._value) { + return; + } + this._value = newValue; + fireEvent(this, "value-changed", { value: this._value }); + } + + private _calcGutters(): string[] { + return this.rtl ? ["rtl-gutter", "CodeMirror-linenumbers"] : []; + } + + private _setScrollBarDirection(): void { + if (this.codemirror) { + this.codemirror.getWrapperElement().classList.toggle("rtl", this.rtl); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-code-editor": HaCodeEditor; + } +} diff --git a/src/components/map/ha-location-editor.ts b/src/components/map/ha-location-editor.ts index 9f3d45c01a..0d4133b832 100644 --- a/src/components/map/ha-location-editor.ts +++ b/src/components/map/ha-location-editor.ts @@ -77,7 +77,12 @@ class LocationEditor extends LitElement { } private _updateLocation(latlng: LatLng) { - this.location = this._ignoreFitToMap = [latlng.lat, latlng.lng]; + let longitude = latlng.lng; + if (Math.abs(longitude) > 180.0) { + // Normalize longitude if map provides values beyond -180 to +180 degrees. + longitude = (((longitude % 360.0) + 540.0) % 360.0) - 180.0; + } + this.location = this._ignoreFitToMap = [latlng.lat, longitude]; fireEvent(this, "change", undefined, { bubbles: false }); } diff --git a/src/data/device_automation.ts b/src/data/device_automation.ts index 093de90b2f..0a308fcdbc 100644 --- a/src/data/device_automation.ts +++ b/src/data/device_automation.ts @@ -39,6 +39,15 @@ export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) => device_id: deviceId, }); +export const fetchDeviceConditionCapabilities = ( + hass: HomeAssistant, + condition: DeviceCondition +) => + hass.callWS({ + type: "device_automation/condition/capabilities", + condition, + }); + export const fetchDeviceTriggerCapabilities = ( hass: HomeAssistant, trigger: DeviceTrigger diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 103b1d147f..5f32b70b4b 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -18,6 +18,11 @@ export interface LovelaceViewConfig { theme?: string; panel?: boolean; background?: string; + visible?: boolean | ShowViewConfig[]; +} + +export interface ShowViewConfig { + user?: string; } export interface LovelaceCardConfig { diff --git a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts index 2fef6124ed..1064d41b36 100644 --- a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts +++ b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts @@ -82,14 +82,16 @@ class DialogConfigEntrySystemOptions extends LitElement { .disabled=${this._submitting} >
- ${this.hass.localize( - "ui.dialogs.config_entry_system_options.enable_new_entities_label" - )} -
-
- ${this.hass.localize( - "ui.dialogs.config_entry_system_options.enable_new_entities_description" - )} +

+ ${this.hass.localize( + "ui.dialogs.config_entry_system_options.enable_new_entities_label" + )} +

+

+ ${this.hass.localize( + "ui.dialogs.config_entry_system_options.enable_new_entities_description" + )} +

@@ -160,7 +162,9 @@ class DialogConfigEntrySystemOptions extends LitElement { padding-bottom: 24px; color: var(--primary-text-color); } - + p { + margin: 0; + } .secondary { color: var(--secondary-text-color); } diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index ef60141ba2..6eaa5a5b01 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -79,6 +79,18 @@ class StepFlowPickHandler extends LitElement { ` )} +

+ ${this.hass.localize( + "ui.panel.config.integrations.note_about_integrations" + )}
+ ${this.hass.localize( + "ui.panel.config.integrations.note_about_website_reference" + )}${this.hass.localize( + "ui.panel.config.integrations.home_assistant_website" + )}. +

`; } @@ -119,6 +131,9 @@ class StepFlowPickHandler extends LitElement { paper-item { cursor: pointer; } + p { + text-align: center; + } `; } } diff --git a/src/panels/config/cloud/account/cloud-account.js b/src/panels/config/cloud/account/cloud-account.js index 0b9c3ed411..19f37a5aff 100644 --- a/src/panels/config/cloud/account/cloud-account.js +++ b/src/panels/config/cloud/account/cloud-account.js @@ -63,19 +63,21 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) { color: var(--primary-color); } - +
- Home Assistant Cloud + [[localize('ui.panel.config.cloud.caption')]]

- Thank you for being part of Home Assistant Cloud. It's because - of people like you that we are able to make a great home - automation experience for everyone. Thank you! + [[localize('ui.panel.config.cloud.account.thank_you_note')]]

- +
Manage Account[[localize('ui.panel.config.cloud.account.manage_account')]] Sign out[[localize('ui.panel.config.cloud.account.sign_out')]]
- Integrations + [[localize('ui.panel.config.cloud.account.integrations')]]

- Integrations for Home Assistant Cloud allow you to connect with - services in the cloud without having to expose your Home - Assistant instance publicly on the internet. + [[localize('ui.panel.config.cloud.account.integrations_introduction')]]

- Check the website for + [[localize('ui.panel.config.cloud.account.integrations_introduction2')]] all available features[[localize('ui.panel.config.cloud.account.integrations_link_all_features')]].

@@ -160,7 +166,9 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) { } _computeRemoteConnected(connected) { - return connected ? "Connected" : "Not Connected"; + return connected + ? this.hass.localize("ui.panel.config.cloud.account.connected") + : this.hass.localize("ui.panel.config.cloud.account.not_connected"); } async _fetchSubscriptionInfo() { @@ -182,7 +190,9 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) { _formatSubscription(subInfo) { if (subInfo === null) { - return "Fetching subscription…"; + return this.hass.localize( + "ui.panel.config.cloud.account.fetching_subscription" + ); } let description = subInfo.human_description; diff --git a/src/panels/config/cloud/account/cloud-alexa-pref.ts b/src/panels/config/cloud/account/cloud-alexa-pref.ts index 1b6ad8eb1a..0f0b227871 100644 --- a/src/panels/config/cloud/account/cloud-alexa-pref.ts +++ b/src/panels/config/cloud/account/cloud-alexa-pref.ts @@ -31,7 +31,11 @@ export class CloudAlexaPref extends LitElement { const { alexa_enabled, alexa_report_state } = this.cloudStatus!.prefs; return html` - +
- With the Alexa integration for Home Assistant Cloud you'll be able to - control all your Home Assistant devices via any Alexa-enabled device. + ${this.hass!.localize("ui.panel.config.cloud.account.alexa.info")} - This integration requires an Alexa-enabled device like the Amazon - Echo. ${alexa_enabled ? html` -

Enable State Reporting

+
+

+ ${this.hass!.localize( + "ui.panel.config.cloud.account.alexa.enable_state_reporting" + )} +

+
+ +
+

- If you enable state reporting, Home Assistant will send - all state changes of exposed entities to Amazon. This - allows you to always see the latest states in the Alexa app - and use the state changes to create routines. + ${this.hass!.localize( + "ui.panel.config.cloud.account.alexa.info_state_reporting" + )}

- ` : ""}
- Sync Entities + ${this.hass!.localize( + "ui.panel.config.cloud.account.alexa.sync_entities" + )}
- Manage Entities + ${this.hass!.localize( + "ui.panel.config.cloud.account.alexa.manage_entities" + )}
@@ -97,7 +113,11 @@ export class CloudAlexaPref extends LitElement { try { await syncCloudAlexaEntities(this.hass!); } catch (err) { - alert(`Failed to sync entities: ${err.body.message}`); + alert( + `${this.hass!.localize( + "ui.panel.config.cloud.account.alexa.sync_entities_error" + )} ${err.body.message}` + ); } finally { this._syncing = false; } @@ -122,9 +142,15 @@ export class CloudAlexaPref extends LitElement { fireEvent(this, "ha-refresh-cloud-status"); } catch (err) { alert( - `Unable to ${toggle.checked ? "enable" : "disable"} report state. ${ - err.message - }` + `${this.hass!.localize( + "ui.panel.config.cloud.account.alexa.state_reporting_error", + "enable_disable", + this.hass!.localize( + toggle.checked + ? "ui.panel.config.cloud.account.alexa.enable" + : "ui.panel.config.cloud.account.alexa.disable" + ) + )} ${err.message}` ); toggle.checked = !toggle.checked; } @@ -149,12 +175,22 @@ export class CloudAlexaPref extends LitElement { .spacer { flex-grow: 1; } - h3 { - margin-bottom: 0; + .state-reporting { + display: flex; + margin-top: 1.5em; } - h3 + p { + .state-reporting + p { margin-top: 0.5em; } + .state-reporting h3 { + flex-grow: 1; + margin: 0; + } + .state-reporting-switch { + margin-top: 0.25em; + margin-right: 7px; + margin-left: 0.5em; + } `; } } diff --git a/src/panels/config/cloud/account/cloud-google-pref.ts b/src/panels/config/cloud/account/cloud-google-pref.ts index b8b105301e..6c9ece442d 100644 --- a/src/panels/config/cloud/account/cloud-google-pref.ts +++ b/src/panels/config/cloud/account/cloud-google-pref.ts @@ -43,7 +43,11 @@ export class CloudGooglePref extends LitElement { } = this.cloudStatus.prefs; return html` - +
- With the Google Assistant integration for Home Assistant Cloud you'll - be able to control all your Home Assistant devices via any Google - Assistant-enabled device. + ${this.hass!.localize("ui.panel.config.cloud.account.google.info")} - This integration requires a Google Assistant-enabled device like - the Google Home or Android phone. ${google_enabled ? html` -

Enable State Reporting

+
+

+ ${this.hass!.localize( + "ui.panel.config.cloud.account.google.enable_state_reporting" + )} +

+
+ +
+

- If you enable state reporting, Home Assistant will send - all state changes of exposed entities to Google. This - allows you to always see the latest states in the Google app. + ${this.hass!.localize( + "ui.panel.config.cloud.account.google.info_state_reporting" + )}

- -
- Please enter a pin to interact with security devices. Security - devices are doors, garage doors and locks. You will be asked - to say/enter this pin when interacting with such devices via - Google Assistant. +

+ ${this.hass!.localize( + "ui.panel.config.cloud.account.google.security_devices" + )} +

+ ${this.hass!.localize( + "ui.panel.config.cloud.account.google.enter_pin_info" + )} @@ -112,11 +129,17 @@ export class CloudGooglePref extends LitElement { .disabled="${!google_enabled}" path="cloud/google_actions/sync" > - Sync entities to Google + ${this.hass!.localize( + "ui.panel.config.cloud.account.google.sync_entities" + )}
- Manage Entities + ${this.hass!.localize( + "ui.panel.config.cloud.account.google.manage_entities" + )}
@@ -159,7 +182,11 @@ export class CloudGooglePref extends LitElement { showSaveSuccessToast(this, this.hass!); fireEvent(this, "ha-refresh-cloud-status"); } catch (err) { - alert(`Unable to store pin: ${err.message}`); + alert( + `${this.hass!.localize( + "ui.panel.config.cloud.account.google.enter_pin_error" + )} ${err.message}` + ); input.value = this.cloudStatus!.prefs.google_secure_devices_pin; } } @@ -179,7 +206,7 @@ export class CloudGooglePref extends LitElement { font-weight: 500; } .secure_devices { - padding-top: 16px; + padding-top: 8px; } paper-input { width: 250px; @@ -193,6 +220,25 @@ export class CloudGooglePref extends LitElement { .spacer { flex-grow: 1; } + .state-reporting { + display: flex; + margin-top: 1.5em; + } + .state-reporting + p { + margin-top: 0.5em; + } + h3 { + margin: 0 0 8px 0; + } + .state-reporting h3 { + flex-grow: 1; + margin: 0; + } + .state-reporting-switch { + margin-top: 0.25em; + margin-right: 7px; + margin-left: 0.5em; + } `; } } diff --git a/src/panels/config/cloud/account/cloud-remote-pref.ts b/src/panels/config/cloud/account/cloud-remote-pref.ts index 715b937fd8..35f309c016 100644 --- a/src/panels/config/cloud/account/cloud-remote-pref.ts +++ b/src/panels/config/cloud/account/cloud-remote-pref.ts @@ -49,16 +49,26 @@ export class CloudRemotePref extends LitElement { if (!remote_certificate) { return html` - +
- Remote access is being prepared. We will notify you when it's ready. + ${this.hass!.localize( + "ui.panel.config.cloud.account.remote.access_is_being_prepared" + )}
`; } return html` - +
- Learn how it works + ${this.hass!.localize( + "ui.panel.config.cloud.account.remote.link_learn_how_it_works" + )} ${remote_certificate ? html`
- Certificate Info + ${this.hass!.localize( + "ui.panel.config.cloud.account.remote.certificate_info" + )} ` : ""} @@ -120,6 +141,9 @@ export class CloudRemotePref extends LitElement { a { color: var(--primary-color); } + .break-word { + overflow-wrap: break-word; + } .switch { position: absolute; right: 24px; diff --git a/src/panels/config/cloud/account/cloud-webhooks.ts b/src/panels/config/cloud/account/cloud-webhooks.ts index ba0d30c4d6..9f2b912283 100644 --- a/src/panels/config/cloud/account/cloud-webhooks.ts +++ b/src/panels/config/cloud/account/cloud-webhooks.ts @@ -51,16 +51,20 @@ export class CloudWebhooks extends LitElement { protected render() { return html` ${this.renderStyle()} - +
- Anything that is configured to be triggered by a webhook can be given - a publicly accessible URL to allow you to send data back to Home - Assistant from anywhere, without exposing your instance to the - internet. ${this._renderBody()} + ${this.hass!.localize("ui.panel.config.cloud.account.webhooks.info")} + ${this._renderBody()}
@@ -78,16 +82,33 @@ export class CloudWebhooks extends LitElement { private _renderBody() { if (!this.cloudStatus || !this._localHooks || !this._cloudHooks) { return html` -
Loading…
+
+ ${this.hass!.localize( + "ui.panel.config.cloud.account.webhooks.loading" + )} +
`; } if (this._localHooks.length === 0) { return html`
- Looks like you have no webhooks yet. Get started by configuring a - webhook-based integration or by - creating a webhook automation. + ${this.hass!.localize( + "ui.panel.config.cloud.account.webhooks.no_hooks_yet" + )} + ${this.hass!.localize( + "ui.panel.config.cloud.account.webhooks.no_hooks_yet_link_integration" + )} + ${this.hass!.localize( + "ui.panel.config.cloud.account.webhooks.no_hooks_yet2" + )} + ${this.hass!.localize( + "ui.panel.config.cloud.account.webhooks.no_hooks_yet_link_automation" + )}.
`; } @@ -113,7 +134,9 @@ export class CloudWebhooks extends LitElement { : this._cloudHooks![entry.webhook_id] ? html` - Manage + ${this.hass!.localize( + "ui.panel.config.cloud.account.webhooks.manage" + )} ` : html` @@ -171,7 +194,11 @@ export class CloudWebhooks extends LitElement { try { await deleteCloudhook(this.hass!, webhookId!); } catch (err) { - alert(`Failed to disable webhook: ${(err as WebhookError).message}`); + alert( + `${this.hass!.localize( + "ui.panel.config.cloud.account.webhooks.disable_hook_error_msg" + )} ${(err as WebhookError).message}` + ); return; } finally { this._progress = this._progress.filter((wid) => wid !== webhookId); diff --git a/src/panels/config/cloud/alexa/cloud-alexa.ts b/src/panels/config/cloud/alexa/cloud-alexa.ts index f9a5cf8e40..b630843afe 100644 --- a/src/panels/config/cloud/alexa/cloud-alexa.ts +++ b/src/panels/config/cloud/alexa/cloud-alexa.ts @@ -136,7 +136,7 @@ class CloudAlexa extends LitElement { .checked=${isExposed} @change=${this._exposeChanged} > - Expose to Alexa + ${this.hass!.localize("ui.panel.config.cloud.alexa.expose")}
@@ -148,7 +148,9 @@ class CloudAlexa extends LitElement { } return html` - + ${selected}${ !this.narrow @@ -173,9 +175,7 @@ class CloudAlexa extends LitElement { !emptyFilter ? html` ` : "" @@ -183,7 +183,11 @@ class CloudAlexa extends LitElement { ${ exposedCards.length > 0 ? html` -

Exposed entities

+

+ ${this.hass!.localize( + "ui.panel.config.cloud.alexa.exposed_entities" + )} +

${exposedCards}
` : "" @@ -191,7 +195,11 @@ class CloudAlexa extends LitElement { ${ notExposedCards.length > 0 ? html` -

Not Exposed entities

+

+ ${this.hass!.localize( + "ui.panel.config.cloud.alexa.not_exposed_entities" + )} +

${notExposedCards}
` : "" diff --git a/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts b/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts index 69982db18a..2964721dbf 100644 --- a/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts +++ b/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts @@ -40,23 +40,38 @@ class DialogCloudCertificate extends LitElement { return html` -

Certificate Information

+

+ ${this.hass!.localize( + "ui.panel.config.cloud.dialog_certificate.certificate_information" + )} +

- Certificate expiration date: + ${this.hass!.localize( + "ui.panel.config.cloud.dialog_certificate.certificate_expiration_date" + )} ${format_date_time( new Date(certificateInfo.expire_date), this.hass!.language )}
- (Will be automatically renewed) + (${this.hass!.localize( + "ui.panel.config.cloud.dialog_certificate.will_be_auto_renewed" + )})

-

- Certificate fingerprint: ${certificateInfo.fingerprint} +

+ ${this.hass!.localize( + "ui.panel.config.cloud.dialog_certificate.fingerprint" + )} + ${certificateInfo.fingerprint}

- CLOSE + ${this.hass!.localize( + "ui.panel.config.cloud.dialog_certificate.close" + )}
`; @@ -77,6 +92,9 @@ class DialogCloudCertificate extends LitElement { ha-paper-dialog { width: 535px; } + .break-word { + overflow-wrap: break-word; + } `, ]; } diff --git a/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts b/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts index 3aa3c0d521..3ecdb04683 100644 --- a/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts +++ b/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts @@ -50,9 +50,19 @@ export class DialogManageCloudhook extends LitElement { : `https://www.home-assistant.io/integrations/${webhook.domain}/`; return html` -

Webhook for ${webhook.name}

+

+ ${this.hass!.localize( + "ui.panel.config.cloud.dialog_cloudhook.webhook_for", + "name", + webhook.name + )} +

-

The webhook is available at the following url:

+

+ ${this.hass!.localize( + "ui.panel.config.cloud.dialog_cloudhook.available_at" + )} +

${cloudhook.managed ? html` - This webhook is managed by an integration and cannot be - disabled. + ${this.hass!.localize( + "ui.panel.config.cloud.dialog_cloudhook.managed_by_integration" + )} ` : html` - If you no longer want to use this webhook, you can + ${this.hass!.localize( + "ui.panel.config.cloud.dialog_cloudhook.info_disable_webhook" + )} . `}

@@ -76,9 +91,17 @@ export class DialogManageCloudhook extends LitElement {
- VIEW DOCUMENTATION + ${this.hass!.localize( + "ui.panel.config.cloud.dialog_cloudhook.view_documentation" + )} - CLOSE + ${this.hass!.localize( + "ui.panel.config.cloud.dialog_cloudhook.close" + )}
`; @@ -97,7 +120,13 @@ export class DialogManageCloudhook extends LitElement { } private async _disableWebhook() { - if (!confirm("Are you sure you want to disable this webhook?")) { + if ( + !confirm( + this.hass!.localize( + "ui.panel.config.cloud.dialog_cloudhook.confirm_disable" + ) + ) + ) { return; } @@ -113,7 +142,9 @@ export class DialogManageCloudhook extends LitElement { input.setSelectionRange(0, input.value.length); try { document.execCommand("copy"); - paperInput.label = "COPIED TO CLIPBOARD"; + paperInput.label = this.hass!.localize( + "ui.panel.config.cloud.dialog_cloudhook.copied_to_clipboard" + ); } catch (err) { // Copying failed. Oh no } diff --git a/src/panels/config/cloud/forgot-password/cloud-forgot-password.js b/src/panels/config/cloud/forgot-password/cloud-forgot-password.js index cab3221e59..b036f3bf65 100644 --- a/src/panels/config/cloud/forgot-password/cloud-forgot-password.js +++ b/src/panels/config/cloud/forgot-password/cloud-forgot-password.js @@ -7,11 +7,12 @@ import "../../../../components/buttons/ha-progress-button"; import "../../../../layouts/hass-subpage"; import "../../../../resources/ha-style"; import { EventsMixin } from "../../../../mixins/events-mixin"; - +import LocalizeMixin from "../../../../mixins/localize-mixin"; /* * @appliesMixin EventsMixin + * @appliesMixin LocalizeMixin */ -class CloudForgotPassword extends EventsMixin(PolymerElement) { +class CloudForgotPassword extends LocalizeMixin(EventsMixin(PolymerElement)) { static get template() { return html` - +
- +

- Enter your email address and we will send you a link to reset - your password. + [[localize('ui.panel.config.cloud.forgot_password.instructions')]]

[[_error]]
Send reset email[[localize('ui.panel.config.cloud.forgot_password.send_reset_email')]]
@@ -126,7 +126,7 @@ class CloudForgotPassword extends EventsMixin(PolymerElement) { this._requestInProgress = false; this.fire("cloud-done", { flashMessage: - "Check your email for instructions on how to reset your password.", + "[[localize('ui.panel.config.cloud.forgot_password.check_your_email')]]", }); }, (err) => diff --git a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts index e15ff78815..c7c752a478 100644 --- a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts +++ b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts @@ -132,7 +132,7 @@ class CloudGoogleAssistant extends LitElement { .checked=${isExposed} @change=${this._exposeChanged} > - Expose to Google Assistant + ${this.hass!.localize("ui.panel.config.cloud.google.expose")} ${entity.might_2fa ? html` @@ -141,7 +141,9 @@ class CloudGoogleAssistant extends LitElement { .checked=${Boolean(config.disable_2fa)} @change=${this._disable2FAChanged} > - Disable two factor authentication + ${this.hass!.localize( + "ui.panel.config.cloud.google.disable_2FA" + )} ` : ""} @@ -155,7 +157,9 @@ class CloudGoogleAssistant extends LitElement { } return html` - + ${selected}${ !this.narrow @@ -180,9 +184,7 @@ class CloudGoogleAssistant extends LitElement { !emptyFilter ? html` ` : "" @@ -190,7 +192,11 @@ class CloudGoogleAssistant extends LitElement { ${ exposedCards.length > 0 ? html` -

Exposed entities

+

+ ${this.hass!.localize( + "ui.panel.config.cloud.google.exposed_entities" + )} +

${exposedCards}
` : "" @@ -198,7 +204,11 @@ class CloudGoogleAssistant extends LitElement { ${ notExposedCards.length > 0 ? html` -

Not Exposed entities

+

+ ${this.hass!.localize( + "ui.panel.config.cloud.google.not_exposed_entities" + )} +

${notExposedCards}
` : "" @@ -323,7 +333,11 @@ class CloudGoogleAssistant extends LitElement { window.addEventListener( "popstate", () => { - showToast(parent, { message: "Synchronizing changes to Google." }); + showToast(parent, { + message: this.hass!.localize( + "ui.panel.config.cloud.googe.sync_to_google" + ), + }); cloudSyncGoogleAssistant(this.hass); }, { once: true } diff --git a/src/panels/config/cloud/login/cloud-login.js b/src/panels/config/cloud/login/cloud-login.js index fcd6dfc6f1..af39b1ccdb 100644 --- a/src/panels/config/cloud/login/cloud-login.js +++ b/src/panels/config/cloud/login/cloud-login.js @@ -16,11 +16,15 @@ import "../../ha-config-section"; import { EventsMixin } from "../../../../mixins/events-mixin"; import NavigateMixin from "../../../../mixins/navigate-mixin"; import "../../../../components/ha-icon-next"; +import LocalizeMixin from "../../../../mixins/localize-mixin"; /* * @appliesMixin NavigateMixin * @appliesMixin EventsMixin + * @appliesMixin LocalizeMixin */ -class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) { +class CloudLogin extends LocalizeMixin( + NavigateMixin(EventsMixin(PolymerElement)) +) { static get template() { return html` - +
- Home Assistant Cloud + [[localize('ui.panel.config.cloud.caption')]]

- Home Assistant Cloud provides you with a secure remote - connection to your instance while away from home. It also allows - you to connect with cloud-only services: Amazon Alexa and Google - Assistant. + [[localize('ui.panel.config.cloud.login.introduction')]]

- This service is run by our partner + [[localize('ui.panel.config.cloud.login.introduction2')]] Nabu Casa, Inc, a company founded by the founders of Home Assistant and - Hass.io. + > + [[localize('ui.panel.config.cloud.login.introduction2a')]]

- Home Assistant Cloud is a subscription service with a free one - month trial. No payment information necessary. + [[localize('ui.panel.config.cloud.login.introduction3')]]

Learn more about Home Assistant Cloud[[localize('ui.panel.config.cloud.login.learn_more_link')]]

@@ -101,44 +103,46 @@ class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
[[flashMessage]] Dismiss[[localize('ui.panel.config.cloud.login.dismiss')]]
- +
[[_error]]
Sign in[[localize('ui.panel.config.cloud.login.sign_in')]]
@@ -146,8 +150,10 @@ class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) { - Start your free 1 month trial -
No payment information necessary
+ [[localize('ui.panel.config.cloud.login.start_trial')]] +
+ [[localize('ui.panel.config.cloud.login.trial_info')]] +
@@ -251,7 +257,9 @@ class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) { const errCode = err && err.body && err.body.code; if (errCode === "PasswordChangeRequired") { - alert("You need to change your password before logging in."); + alert( + "[[localize('ui.panel.config.cloud.login.alert_password_change_required')]]" + ); this.navigate("/config/cloud/forgot-password"); return; } @@ -265,7 +273,8 @@ class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) { }; if (errCode === "UserNotConfirmed") { - props._error = "You need to confirm your email before logging in."; + props._error = + "[[localize('ui.panel.config.cloud.login.alert_email_confirm_necessary')]]"; } this.setProperties(props); diff --git a/src/panels/config/cloud/register/cloud-register.js b/src/panels/config/cloud/register/cloud-register.js index 4f44f97485..e12ed7c75c 100644 --- a/src/panels/config/cloud/register/cloud-register.js +++ b/src/panels/config/cloud/register/cloud-register.js @@ -8,11 +8,13 @@ import "../../../../layouts/hass-subpage"; import "../../../../resources/ha-style"; import "../../ha-config-section"; import { EventsMixin } from "../../../../mixins/events-mixin"; +import LocalizeMixin from "../../../../mixins/localize-mixin"; /* * @appliesMixin EventsMixin + * @appliesMixin LocalizeMixin */ -class CloudRegister extends EventsMixin(PolymerElement) { +class CloudRegister extends LocalizeMixin(EventsMixin(PolymerElement)) { static get template() { return html` - +
- Start your free trial + [[localize('ui.panel.config.cloud.register.headline')]]

- Create an account to start your free one month trial with Home Assistant Cloud. No payment information necessary. + [[localize('ui.panel.config.cloud.register.information')]]

- The trial will give you access to all the benefits of Home Assistant Cloud, including: + [[localize('ui.panel.config.cloud.register.information2')]]

    -
  • Control of Home Assistant away from home
  • -
  • Integration with Google Assistant
  • -
  • Integration with Amazon Alexa
  • -
  • Easy integration with webhook-based apps like OwnTracks
  • +
  • [[localize('ui.panel.config.cloud.register.feature_remote_control')]]
  • +
  • [[localize('ui.panel.config.cloud.register.feature_google_home')]]
  • +
  • [[localize('ui.panel.config.cloud.register.feature_amazon_alexa')]]
  • +
  • [[localize('ui.panel.config.cloud.register.feature_webhook_apps')]]

- This service is run by our partner Nabu Casa, Inc, a company founded by the founders of Home Assistant and Hass.io. + [[localize('ui.panel.config.cloud.register.information3')]] Nabu Casa, Inc[[localize('ui.panel.config.cloud.register.information3a')]]

- By registering an account you agree to the following terms and conditions. + [[localize('ui.panel.config.cloud.register.information4')]]

- +
[[_error]]
- - + +
- Start trial - + [[localize('ui.panel.config.cloud.register.start_trial')]] +
@@ -211,8 +213,9 @@ class CloudRegister extends EventsMixin(PolymerElement) { _password: "", }); this.fire("cloud-done", { - flashMessage: - "Account created! Check your email for instructions on how to activate your account.", + flashMessage: this.hass.localize( + "ui.panel.config.cloud.register.account_created" + ), }); } } diff --git a/src/panels/config/devices/device-detail/ha-device-card.js b/src/panels/config/devices/device-detail/ha-device-card.js index 4ef8dfad9c..94ee330e7e 100644 --- a/src/panels/config/devices/device-detail/ha-device-card.js +++ b/src/panels/config/devices/device-detail/ha-device-card.js @@ -1,35 +1,17 @@ -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item-body"; -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import "../../../../components/ha-card"; -import "../../../../layouts/hass-subpage"; import { EventsMixin } from "../../../../mixins/events-mixin"; import LocalizeMixin from "../../../../mixins/localize-mixin"; -import { computeStateName } from "../../../../common/entity/compute_state_name"; -import "../../../../components/entity/state-badge"; import { compare } from "../../../../common/string/compare"; -import { - subscribeDeviceRegistry, - updateDeviceRegistryEntry, -} from "../../../../data/device_registry"; -import { subscribeAreaRegistry } from "../../../../data/area_registry"; +import { updateDeviceRegistryEntry } from "../../../../data/device_registry"; import { loadDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog, } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail"; -function computeEntityName(hass, entity) { - if (entity.name) return entity.name; - const state = hass.states[entity.entity_id]; - return state ? computeStateName(state) : null; -} - /* * @appliesMixin EventsMixin */ @@ -37,10 +19,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) { static get template() { return html` -
- -
[[device.model]]
@@ -122,27 +86,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
- -
`; } @@ -152,14 +95,11 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) { device: Object, devices: Array, areas: Array, - entities: Array, hass: Object, narrow: { type: Boolean, reflectToAttribute: true, }, - hideSettings: { type: Boolean, value: false }, - hideEntities: { type: Boolean, value: false }, _childDevices: { type: Array, computed: "_computeChildDevices(device, devices)", @@ -172,30 +112,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) { loadDeviceRegistryDetailDialog(); } - connectedCallback() { - super.connectedCallback(); - this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => { - this._areas = areas; - }); - this._unsubDevices = subscribeDeviceRegistry( - this.hass.connection, - (devices) => { - this.devices = devices; - this.device = devices.find((device) => device.id === this.device.id); - } - ); - } - - disconnectedCallback() { - super.disconnectedCallback(); - if (this._unsubAreas) { - this._unsubAreas(); - } - if (this._unsubDevices) { - this._unsubDevices(); - } - } - _computeArea(areas, device) { if (!areas || !device || !device.area_id) { return "No Area"; @@ -210,30 +126,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) { .sort((dev1, dev2) => compare(dev1.name, dev2.name)); } - _computeDeviceEntities(hass, device, entities) { - return entities - .filter((entity) => entity.device_id === device.id) - .sort((ent1, ent2) => - compare( - computeEntityName(hass, ent1) || `zzz${ent1.entity_id}`, - computeEntityName(hass, ent2) || `zzz${ent2.entity_id}` - ) - ); - } - - _computeStateObj(entity, hass) { - return hass.states[entity.entity_id]; - } - - _computeEntityName(entity, hass) { - return ( - computeEntityName(hass, entity) || - `(${this.localize( - "ui.panel.config.integrations.config_entry.entity_unavailable" - )})` - ); - } - _deviceName(device) { return device.name_by_user || device.name; } diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 07a53b6d8d..e238d511b5 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -13,6 +13,7 @@ import "../../../layouts/hass-subpage"; import "../../../layouts/hass-error-screen"; import "../ha-config-section"; +import "./device-detail/ha-device-card"; import "./device-detail/ha-device-triggers-card"; import "./device-detail/ha-device-conditions-card"; import "./device-detail/ha-device-actions-card"; @@ -144,9 +145,6 @@ export class HaConfigDevicePage extends LitElement { .areas=${this.areas} .devices=${this.devices} .device=${device} - .entities=${this.entities} - hide-settings - hide-entities > ${entities.length diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index d448859b0f..c60bcabe91 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -1,19 +1,5 @@ -import "@polymer/paper-tooltip/paper-tooltip"; -import "@material/mwc-button"; -import "@polymer/iron-icon/iron-icon"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; - -import "../../../components/ha-card"; -import "../../../components/data-table/ha-data-table"; -import "../../../components/entity/ha-state-icon"; import "../../../layouts/hass-subpage"; -import "../../../resources/ha-style"; -import "../../../components/ha-icon-next"; - -import "../ha-config-section"; - -import memoizeOne from "memoize-one"; +import "./ha-devices-data-table"; import { LitElement, @@ -21,33 +7,14 @@ import { TemplateResult, property, customElement, + CSSResult, + css, } from "lit-element"; import { HomeAssistant } from "../../../types"; -// tslint:disable-next-line -import { - DataTabelColumnContainer, - RowClickedEvent, - DataTabelRowData, -} from "../../../components/data-table/ha-data-table"; -// tslint:disable-next-line import { DeviceRegistryEntry } from "../../../data/device_registry"; import { EntityRegistryEntry } from "../../../data/entity_registry"; import { ConfigEntry } from "../../../data/config_entries"; import { AreaRegistryEntry } from "../../../data/area_registry"; -import { navigate } from "../../../common/navigate"; -import { LocalizeFunc } from "../../../common/translations/localize"; -import { computeStateName } from "../../../common/entity/compute_state_name"; - -interface DeviceRowData extends DeviceRegistryEntry { - device?: DeviceRowData; - area?: string; - integration?: string; - battery_entity?: string; -} - -interface DeviceEntityLookup { - [deviceId: string]: EntityRegistryEntry[]; -} @customElement("ha-config-devices-dashboard") export class HaConfigDeviceDashboard extends LitElement { @@ -59,234 +26,35 @@ export class HaConfigDeviceDashboard extends LitElement { @property() public areas!: AreaRegistryEntry[]; @property() public domain!: string; - private _devices = memoizeOne( - ( - devices: DeviceRegistryEntry[], - entries: ConfigEntry[], - entities: EntityRegistryEntry[], - areas: AreaRegistryEntry[], - domain: string, - localize: LocalizeFunc - ) => { - // Some older installations might have devices pointing at invalid entryIDs - // So we guard for that. - - let outputDevices: DeviceRowData[] = devices; - - const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {}; - for (const device of devices) { - deviceLookup[device.id] = device; - } - - const deviceEntityLookup: DeviceEntityLookup = {}; - for (const entity of entities) { - if (!entity.device_id) { - continue; - } - if (!(entity.device_id in deviceEntityLookup)) { - deviceEntityLookup[entity.device_id] = []; - } - deviceEntityLookup[entity.device_id].push(entity); - } - - const entryLookup: { [entryId: string]: ConfigEntry } = {}; - for (const entry of entries) { - entryLookup[entry.entry_id] = entry; - } - - const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; - for (const area of areas) { - areaLookup[area.area_id] = area; - } - - if (domain) { - outputDevices = outputDevices.filter((device) => - device.config_entries.find( - (entryId) => - entryId in entryLookup && entryLookup[entryId].domain === domain - ) - ); - } - - outputDevices = outputDevices.map((device) => { - return { - ...device, - name: - device.name_by_user || - device.name || - this._fallbackDeviceName(device.id, deviceEntityLookup) || - "No name", - model: device.model || "", - manufacturer: device.manufacturer || "", - area: device.area_id ? areaLookup[device.area_id].name : "No area", - integration: device.config_entries.length - ? device.config_entries - .filter((entId) => entId in entryLookup) - .map( - (entId) => - localize( - `component.${entryLookup[entId].domain}.config.title` - ) || entryLookup[entId].domain - ) - .join(", ") - : "No integration", - battery_entity: this._batteryEntity(device.id, deviceEntityLookup), - }; - }); - - return outputDevices; - } - ); - - private _columns = memoizeOne( - (narrow: boolean): DataTabelColumnContainer => - narrow - ? { - device: { - title: "Device", - sortable: true, - filterKey: "name", - filterable: true, - direction: "asc", - template: (device: DeviceRowData) => { - const battery = device.battery_entity - ? this.hass.states[device.battery_entity] - : undefined; - // Have to work on a nice layout for mobile - return html` - ${device.name_by_user || device.name}
- ${device.area} | ${device.integration}
- ${battery - ? html` - ${battery.state}% - - ` - : ""} - `; - }, - }, - } - : { - device_name: { - title: "Device", - sortable: true, - filterable: true, - direction: "asc", - }, - manufacturer: { - title: "Manufacturer", - sortable: true, - filterable: true, - }, - model: { - title: "Model", - sortable: true, - filterable: true, - }, - area: { - title: "Area", - sortable: true, - filterable: true, - }, - integration: { - title: "Integration", - sortable: true, - filterable: true, - }, - battery: { - title: "Battery", - sortable: true, - type: "numeric", - template: (batteryEntity: string) => { - const battery = batteryEntity - ? this.hass.states[batteryEntity] - : undefined; - return battery - ? html` - ${battery.state}% - - ` - : html` - - - `; - }, - }, - } - ); - protected render(): TemplateResult { return html` - { - // We don't need a lot of this data for mobile view, but kept it for filtering... - const data: DataTabelRowData = { - device_name: device.name, - id: device.id, - manufacturer: device.manufacturer, - model: device.model, - area: device.area, - integration: device.integration, - }; - if (this.narrow) { - data.device = device; - return data; - } - data.battery = device.battery_entity; - return data; - })} - @row-click=${this._handleRowClicked} - > +
+ +
`; } - private _batteryEntity( - deviceId: string, - deviceEntityLookup: DeviceEntityLookup - ): string | undefined { - const batteryEntity = (deviceEntityLookup[deviceId] || []).find( - (entity) => - this.hass.states[entity.entity_id] && - this.hass.states[entity.entity_id].attributes.device_class === "battery" - ); - - return batteryEntity ? batteryEntity.entity_id : undefined; - } - - private _fallbackDeviceName( - deviceId: string, - deviceEntityLookup: DeviceEntityLookup - ): string | undefined { - for (const entity of deviceEntityLookup[deviceId] || []) { - const stateObj = this.hass.states[entity.entity_id]; - if (stateObj) { - return computeStateName(stateObj); + static get styles(): CSSResult { + return css` + .content { + padding: 4px; } - } - - return undefined; - } - - private _handleRowClicked(ev: CustomEvent) { - const deviceId = (ev.detail as RowClickedEvent).id; - navigate(this, `/config/devices/device/${deviceId}`); + ha-devices-data-table { + width: 100%; + } + `; } } diff --git a/src/panels/config/devices/ha-devices-data-table.ts b/src/panels/config/devices/ha-devices-data-table.ts new file mode 100644 index 0000000000..32ba9d881c --- /dev/null +++ b/src/panels/config/devices/ha-devices-data-table.ts @@ -0,0 +1,265 @@ +import "../../../components/data-table/ha-data-table"; +import "../../../components/entity/ha-state-icon"; + +import memoizeOne from "memoize-one"; + +import { + LitElement, + html, + TemplateResult, + property, + customElement, +} from "lit-element"; +import { HomeAssistant } from "../../../types"; +// tslint:disable-next-line +import { + DataTableColumnContainer, + RowClickedEvent, + DataTableRowData, +} from "../../../components/data-table/ha-data-table"; +// tslint:disable-next-line +import { DeviceRegistryEntry } from "../../../data/device_registry"; +import { EntityRegistryEntry } from "../../../data/entity_registry"; +import { ConfigEntry } from "../../../data/config_entries"; +import { AreaRegistryEntry } from "../../../data/area_registry"; +import { navigate } from "../../../common/navigate"; +import { LocalizeFunc } from "../../../common/translations/localize"; +import { computeStateName } from "../../../common/entity/compute_state_name"; + +export interface DeviceRowData extends DeviceRegistryEntry { + device?: DeviceRowData; + area?: string; + integration?: string; + battery_entity?: string; +} + +export interface DeviceEntityLookup { + [deviceId: string]: EntityRegistryEntry[]; +} + +@customElement("ha-devices-data-table") +export class HaDevicesDataTable extends LitElement { + @property() public hass!: HomeAssistant; + @property() public narrow = false; + @property() public devices!: DeviceRegistryEntry[]; + @property() public entries!: ConfigEntry[]; + @property() public entities!: EntityRegistryEntry[]; + @property() public areas!: AreaRegistryEntry[]; + @property() public domain!: string; + + private _devices = memoizeOne( + ( + devices: DeviceRegistryEntry[], + entries: ConfigEntry[], + entities: EntityRegistryEntry[], + areas: AreaRegistryEntry[], + domain: string, + localize: LocalizeFunc + ) => { + // Some older installations might have devices pointing at invalid entryIDs + // So we guard for that. + + let outputDevices: DeviceRowData[] = devices; + + const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {}; + for (const device of devices) { + deviceLookup[device.id] = device; + } + + const deviceEntityLookup: DeviceEntityLookup = {}; + for (const entity of entities) { + if (!entity.device_id) { + continue; + } + if (!(entity.device_id in deviceEntityLookup)) { + deviceEntityLookup[entity.device_id] = []; + } + deviceEntityLookup[entity.device_id].push(entity); + } + + const entryLookup: { [entryId: string]: ConfigEntry } = {}; + for (const entry of entries) { + entryLookup[entry.entry_id] = entry; + } + + const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; + for (const area of areas) { + areaLookup[area.area_id] = area; + } + + if (domain) { + outputDevices = outputDevices.filter((device) => + device.config_entries.find( + (entryId) => + entryId in entryLookup && entryLookup[entryId].domain === domain + ) + ); + } + + outputDevices = outputDevices.map((device) => { + return { + ...device, + name: + device.name_by_user || + device.name || + this._fallbackDeviceName(device.id, deviceEntityLookup) || + "No name", + model: device.model || "", + manufacturer: device.manufacturer || "", + area: device.area_id ? areaLookup[device.area_id].name : "No area", + integration: device.config_entries.length + ? device.config_entries + .filter((entId) => entId in entryLookup) + .map( + (entId) => + localize( + `component.${entryLookup[entId].domain}.config.title` + ) || entryLookup[entId].domain + ) + .join(", ") + : "No integration", + battery_entity: this._batteryEntity(device.id, deviceEntityLookup), + }; + }); + + return outputDevices; + } + ); + + private _columns = memoizeOne( + (narrow: boolean): DataTableColumnContainer => + narrow + ? { + name: { + title: "Device", + sortable: true, + filterKey: "name", + filterable: true, + direction: "asc", + template: (name, device: DataTableRowData) => { + const battery = device.battery_entity + ? this.hass.states[device.battery_entity] + : undefined; + // Have to work on a nice layout for mobile + return html` + ${name}
+ ${device.area} | ${device.integration}
+ ${battery + ? html` + ${battery.state}% + + ` + : ""} + `; + }, + }, + } + : { + name: { + title: "Device", + sortable: true, + filterable: true, + direction: "asc", + }, + manufacturer: { + title: "Manufacturer", + sortable: true, + filterable: true, + }, + model: { + title: "Model", + sortable: true, + filterable: true, + }, + area: { + title: "Area", + sortable: true, + filterable: true, + }, + integration: { + title: "Integration", + sortable: true, + filterable: true, + }, + battery_entity: { + title: "Battery", + sortable: true, + type: "numeric", + template: (batteryEntity: string) => { + const battery = batteryEntity + ? this.hass.states[batteryEntity] + : undefined; + return battery + ? html` + ${battery.state}% + + ` + : html` + - + `; + }, + }, + } + ); + + protected render(): TemplateResult { + return html` + + `; + } + + private _batteryEntity( + deviceId: string, + deviceEntityLookup: DeviceEntityLookup + ): string | undefined { + const batteryEntity = (deviceEntityLookup[deviceId] || []).find( + (entity) => + this.hass.states[entity.entity_id] && + this.hass.states[entity.entity_id].attributes.device_class === "battery" + ); + + return batteryEntity ? batteryEntity.entity_id : undefined; + } + + private _fallbackDeviceName( + deviceId: string, + deviceEntityLookup: DeviceEntityLookup + ): string | undefined { + for (const entity of deviceEntityLookup[deviceId] || []) { + const stateObj = this.hass.states[entity.entity_id]; + if (stateObj) { + return computeStateName(stateObj); + } + } + + return undefined; + } + + private _handleRowClicked(ev: CustomEvent) { + const deviceId = (ev.detail as RowClickedEvent).id; + navigate(this, `/config/devices/device/${deviceId}`); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-devices-data-table": HaDevicesDataTable; + } +} 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 e720e6e319..b0424d4511 100644 --- a/src/panels/config/entity_registry/ha-config-entity-registry.ts +++ b/src/panels/config/entity_registry/ha-config-entity-registry.ts @@ -6,9 +6,6 @@ import { CSSResult, property, } from "lit-element"; -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; import { HomeAssistant } from "../../../types"; import { @@ -18,7 +15,7 @@ import { } from "../../../data/entity_registry"; import "../../../layouts/hass-subpage"; import "../../../layouts/hass-loading-screen"; -import "../../../components/ha-card"; +import "../../../components/data-table/ha-data-table"; import "../../../components/ha-icon"; import "../../../components/ha-switch"; import { domainIcon } from "../../../common/entity/domain_icon"; @@ -30,11 +27,14 @@ import { loadEntityRegistryDetailDialog, } from "./show-dialog-entity-registry-detail"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { compare } from "../../../common/string/compare"; -import { classMap } from "lit-html/directives/class-map"; // tslint:disable-next-line import { HaSwitch } from "../../../components/ha-switch"; import memoize from "memoize-one"; +// tslint:disable-next-line +import { + DataTableColumnContainer, + RowClickedEvent, +} from "../../../components/data-table/ha-data-table"; class HaConfigEntityRegistry extends LitElement { @property() public hass!: HomeAssistant; @@ -43,11 +43,76 @@ class HaConfigEntityRegistry extends LitElement { @property() private _showDisabled = false; private _unsubEntities?: UnsubscribeFunc; + private _columns = memoize( + (_language): DataTableColumnContainer => { + return { + icon: { + title: "", + type: "icon", + template: (icon) => html` + + `, + }, + name: { + title: this.hass.localize( + "ui.panel.config.entity_registry.picker.headers.name" + ), + sortable: true, + 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` + + `, + }, + }; + } + ); + private _filteredEntities = memoize( (entities: EntityRegistryEntry[], showDisabled: boolean) => - showDisabled + (showDisabled ? entities : entities.filter((entity) => !Boolean(entity.disabled_by)) + ).map((entry) => { + const state = this.hass!.states[entry.entity_id]; + return { + ...entry, + icon: state + ? stateIcon(state) + : domainIcon(computeDomain(entry.entity_id)), + name: + computeEntityRegistryName(this.hass!, entry) || + this.hass!.localize("state.default.unavailable"), + }; + }) ); public disconnectedCallback() { @@ -89,56 +154,21 @@ class HaConfigEntityRegistry extends LitElement { "ui.panel.config.entity_registry.picker.integrations_page" )} - - - - ${this.hass.localize( - "ui.panel.config.entity_registry.picker.show_disabled" - )}${this.hass.localize( + "ui.panel.config.entity_registry.picker.show_disabled" + )} - ${this._filteredEntities(this._entities, this._showDisabled).map( - (entry) => { - const state = this.hass!.states[entry.entity_id]; - return html` - - - -
- ${computeEntityRegistryName(this.hass!, entry) || - `(${this.hass!.localize( - "state.default.unavailable" - )})`} -
-
- ${entry.entity_id} -
-
-
- ${entry.platform} - ${entry.disabled_by - ? html` -
(disabled) - ` - : ""} -
-
- `; - } - )} -
+ + + `; @@ -155,9 +185,7 @@ class HaConfigEntityRegistry extends LitElement { this._unsubEntities = subscribeEntityRegistry( this.hass.connection, (entities) => { - this._entities = entities.sort((ent1, ent2) => - compare(ent1.entity_id, ent2.entity_id) - ); + this._entities = entities; } ); } @@ -167,8 +195,14 @@ class HaConfigEntityRegistry extends LitElement { this._showDisabled = (ev.target as HaSwitch).checked; } - private _openEditEntry(ev: MouseEvent): void { - const entry = (ev.currentTarget! as any).entry; + private _openEditEntry(ev: CustomEvent): void { + const entryId = (ev.detail as RowClickedEvent).id; + const entry = this._entities!.find( + (entity) => entity.entity_id === entryId + ); + if (!entry) { + return; + } showEntityRegistryDetailDialog(this, { entry, }); @@ -179,23 +213,12 @@ class HaConfigEntityRegistry extends LitElement { a { color: var(--primary-color); } - ha-card { + ha-data-table { margin-bottom: 24px; - direction: ltr; + margin-top: 0px; } - paper-icon-item { - cursor: pointer; - color: var(--primary-text-color); - } - ha-icon { - margin-left: 8px; - } - .platform { - text-align: right; - margin: 0 0 0 8px; - } - .disabled-entry { - color: var(--secondary-text-color); + ha-switch { + margin-top: 16px; } `; } diff --git a/src/panels/config/integrations/config-entry/ha-ce-entities-card.js b/src/panels/config/integrations/config-entry/ha-ce-entities-card.js index 10d9e7f2bc..63cd18774d 100644 --- a/src/panels/config/integrations/config-entry/ha-ce-entities-card.js +++ b/src/panels/config/integrations/config-entry/ha-ce-entities-card.js @@ -20,7 +20,7 @@ class HaCeEntitiesCard extends LocalizeMixIn(EventsMixin(PolymerElement)) { return html` - OZW Log - + + [[localize('ui.panel.config.zwave.ozw_log.header')]] + + + [[localize('ui.panel.config.zwave.ozw_log.introduction')]] + +
diff --git a/src/panels/config/zwave/zwave-network.ts b/src/panels/config/zwave/zwave-network.ts index 41dd3457f7..03d0423f61 100644 --- a/src/panels/config/zwave/zwave-network.ts +++ b/src/panels/config/zwave/zwave-network.ts @@ -51,7 +51,7 @@ export class ZwaveNetwork extends LitElement { protected render(): TemplateResult | void { return html` -
+
${this.hass!.localize( "ui.panel.config.zwave.network_management.header" @@ -63,11 +63,19 @@ export class ZwaveNetwork extends LitElement { icon="hass:help-circle" >
- +
${this.hass!.localize( "ui.panel.config.zwave.network_management.introduction" )} - +

+ + ${this.hass!.localize("ui.panel.config.zwave.learn_more")} + +

+
${this._networkStatus ? html` @@ -234,6 +242,11 @@ export class ZwaveNetwork extends LitElement { margin-top: 24px; } + .sectionHeader { + position: relative; + padding-right: 40px; + } + .network-status { text-align: center; } diff --git a/src/panels/developer-tools/event/developer-tools-event.js b/src/panels/developer-tools/event/developer-tools-event.js index 25fd569034..c1b50bd068 100644 --- a/src/panels/developer-tools/event/developer-tools-event.js +++ b/src/panels/developer-tools/event/developer-tools-event.js @@ -1,17 +1,18 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes"; import "@material/mwc-button"; import "@polymer/paper-input/paper-input"; -import "@polymer/paper-input/paper-textarea"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import yaml from "js-yaml"; +import "../../../components/ha-code-editor"; import "../../../resources/ha-style"; import "./events-list"; import "./event-subscribe-card"; import { EventsMixin } from "../../../mixins/events-mixin"; +const ERROR_SENTINEL = {}; /* * @appliesMixin EventsMixin */ @@ -32,6 +33,11 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) { .ha-form { margin-right: 16px; + max-width: 400px; + } + + mwc-button { + margin-top: 8px; } .header { @@ -62,11 +68,16 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) { required value="{{eventType}}" > - - Fire Event +

Event Data (YAML, optional)

+ + Fire Event
@@ -97,6 +108,16 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) { type: String, value: "", }, + + parsedJSON: { + type: Object, + computed: "_computeParsedEventData(eventData)", + }, + + validJSON: { + type: Boolean, + computed: "_computeValidJSON(parsedJSON)", + }, }; } @@ -104,19 +125,28 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) { this.eventType = ev.detail.eventType; } - fireEvent() { - var eventData; - + _computeParsedEventData(eventData) { try { - eventData = this.eventData ? yaml.safeLoad(this.eventData) : {}; + return eventData.trim() ? yaml.safeLoad(eventData) : {}; } catch (err) { - /* eslint-disable no-alert */ - alert("Error parsing YAML: " + err); - /* eslint-enable no-alert */ + return ERROR_SENTINEL; + } + } + + _computeValidJSON(parsedJSON) { + return parsedJSON !== ERROR_SENTINEL; + } + + _yamlChanged(ev) { + this.eventData = ev.detail.value; + } + + fireEvent() { + if (!this.eventType) { + alert("Event type is a mandatory field"); return; } - - this.hass.callApi("POST", "events/" + this.eventType, eventData).then( + this.hass.callApi("POST", "events/" + this.eventType, this.parsedJSON).then( function() { this.fire("hass-notification", { message: "Event " + this.eventType + " successful fired!", diff --git a/src/panels/developer-tools/info/developer-tools-info.ts b/src/panels/developer-tools/info/developer-tools-info.ts index 3c021f7c57..03328ee90c 100644 --- a/src/panels/developer-tools/info/developer-tools-info.ts +++ b/src/panels/developer-tools/info/developer-tools-info.ts @@ -110,9 +110,8 @@ class HaPanelDevInfo extends LitElement {

${nonDefaultLinkText}
- - ${defaultPageText} - + ${defaultPageText}

diff --git a/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts b/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts index 3320eb52c3..93f0215ee9 100644 --- a/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts +++ b/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts @@ -9,12 +9,12 @@ import { } from "lit-element"; import "@material/mwc-button"; import "@polymer/paper-input/paper-input"; -import "@polymer/paper-input/paper-textarea"; import { HomeAssistant } from "../../../types"; import { haStyle } from "../../../resources/styles"; import "../../../components/ha-card"; +import "../../../components/ha-code-editor"; import "./mqtt-subscribe-card"; @customElement("developer-tools-mqtt") @@ -48,12 +48,12 @@ class HaPanelDevMqtt extends LitElement { @value-changed=${this._handleTopic} > - Payload (template allowed)

+
+ >
Publish diff --git a/src/panels/developer-tools/service/developer-tools-service.js b/src/panels/developer-tools/service/developer-tools-service.js index 1656816d12..de04bd4ed9 100644 --- a/src/panels/developer-tools/service/developer-tools-service.js +++ b/src/panels/developer-tools/service/developer-tools-service.js @@ -1,5 +1,4 @@ import "@material/mwc-button"; -import "@polymer/paper-input/paper-textarea"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; @@ -7,6 +6,7 @@ import yaml from "js-yaml"; import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity"; import "../../../components/entity/ha-entity-picker"; +import "../../../components/ha-code-editor"; import "../../../components/ha-service-picker"; import "../../../resources/ha-style"; import "../../../util/app-localstorage-document"; @@ -30,6 +30,10 @@ class HaPanelDevService extends PolymerElement { max-width: 400px; } + mwc-button { + margin-top: 8px; + } + .description { margin-top: 24px; white-space: pre-wrap; @@ -109,20 +113,16 @@ class HaPanelDevService extends PolymerElement { allow-custom-entity > - +

Service Data (YAML, optional)

+ Call Service -