mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-31 14:39:38 +00:00 
			
		
		
		
	Compare commits
	
		
			70 Commits
		
	
	
		
			20201126.0
			...
			layout-str
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | cefb3c3f01 | ||
|   | 909f3a3005 | ||
|   | 4930532c7b | ||
|   | 8a42e65c6a | ||
|   | 5d4121a9b4 | ||
|   | 3d83d5f4b5 | ||
|   | f9dece0743 | ||
|   | ac0871d0e8 | ||
|   | ffc19e591d | ||
|   | c53380ca3d | ||
|   | 7c74a2026a | ||
|   | adaed438d9 | ||
|   | baf38305cb | ||
|   | 8254712521 | ||
|   | 53214781e3 | ||
|   | 88cbbbdf65 | ||
|   | 7f2ebb4bde | ||
|   | f1abb60e4a | ||
|   | e014c7aff6 | ||
|   | b79c03433e | ||
|   | 34eb4d974d | ||
|   | 3264be3c5e | ||
|   | 655f4f75fb | ||
|   | 4383f31696 | ||
|   | 99eb15d15e | ||
|   | 3a5d854e6d | ||
|   | 1e90c6387c | ||
|   | 2cca25f4d0 | ||
|   | 565724d201 | ||
|   | 3e4955becd | ||
|   | 7b560c727f | ||
|   | 35abd9dfdb | ||
|   | 0d9ab8fdd0 | ||
|   | 303f9290a8 | ||
|   | e0c4dc08a1 | ||
|   | 8c655883fe | ||
|   | ba90785115 | ||
|   | 7b392b626b | ||
|   | 8e4ceb7d48 | ||
|   | 2ab1c6e9a9 | ||
|   | dbdced0971 | ||
|   | 5e481880bd | ||
|   | faec063f34 | ||
|   | bbea38d227 | ||
|   | a0ef60de49 | ||
|   | 3313572606 | ||
|   | c4f850cb14 | ||
|   | 3bdab738c6 | ||
|   | faaef31b9f | ||
|   | ca7b8b8b4c | ||
|   | 9ca84e0694 | ||
|   | daaf2b1796 | ||
|   | 25f7cbea5a | ||
|   | c485ea9d7b | ||
|   | 295390c8e9 | ||
|   | 3ebf816ce2 | ||
|   | 0e362b851b | ||
|   | 8d7ba19a08 | ||
|   | 08f4aa9d10 | ||
|   | 98175d5c72 | ||
|   | 7d4cad90bc | ||
|   | 335354d962 | ||
|   | fe31d15d27 | ||
|   | 7ceb6eb50d | ||
|   | 4c4db46aa8 | ||
|   | b5724ed343 | ||
|   | cae94175fe | ||
|   | 0494a9d410 | ||
|   | c261b5c1ce | ||
|   | c89e17ac00 | 
| @@ -7,8 +7,8 @@ export const createMediaPlayerEntities = () => [ | |||||||
|     media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", |     media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", | ||||||
|     media_artist: "Technohead", |     media_artist: "Technohead", | ||||||
|     // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + |     // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + | ||||||
|     // Select Source + Stop + Clear + Play + Shuffle Set + Browse Media |     // Select Source + Stop + Clear + Play + Shuffle Set | ||||||
|     supported_features: 195135, |     supported_features: 64063, | ||||||
|     entity_picture: "/images/album_cover_2.jpg", |     entity_picture: "/images/album_cover_2.jpg", | ||||||
|     media_duration: 300, |     media_duration: 300, | ||||||
|     media_position: 50, |     media_position: 50, | ||||||
| @@ -24,8 +24,8 @@ export const createMediaPlayerEntities = () => [ | |||||||
|     media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", |     media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", | ||||||
|     media_artist: "Technohead", |     media_artist: "Technohead", | ||||||
|     // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + |     // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + | ||||||
|     // Select Source + Stop + Clear + Play + Shuffle Set |     // Select Source + Stop + Clear + Play + Shuffle Set + Browse Media | ||||||
|     supported_features: 64063, |     supported_features: 195135, | ||||||
|     entity_picture: "/images/album_cover.jpg", |     entity_picture: "/images/album_cover.jpg", | ||||||
|     media_duration: 300, |     media_duration: 300, | ||||||
|     media_position: 0, |     media_position: 0, | ||||||
|   | |||||||
| @@ -146,6 +146,16 @@ const CONFIGS = [ | |||||||
|     entity: media_player.receiver_off |     entity: media_player.receiver_off | ||||||
|     `, |     `, | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     heading: "Grid Full Size", | ||||||
|  |     config: ` | ||||||
|  |   - type: grid | ||||||
|  |     columns: 1 | ||||||
|  |     cards: | ||||||
|  |     - type: media-control | ||||||
|  |       entity: media_player.music_paused | ||||||
|  |     `, | ||||||
|  |   }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| class DemoHuiMediControlCard extends PolymerElement { | class DemoHuiMediControlCard extends PolymerElement { | ||||||
|   | |||||||
| @@ -74,9 +74,7 @@ export class HassioUpdate extends LitElement { | |||||||
|             "Supervisor", |             "Supervisor", | ||||||
|             this.supervisor.supervisor, |             this.supervisor.supervisor, | ||||||
|             "hassio/supervisor/update", |             "hassio/supervisor/update", | ||||||
|             `https://github.com//home-assistant/hassio/releases/tag/${ |             `https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}` | ||||||
|               this.supervisor.supervisor.version_latest |  | ||||||
|             }` |  | ||||||
|           )} |           )} | ||||||
|           ${this.supervisor.host.features.includes("hassos") |           ${this.supervisor.host.features.includes("hassos") | ||||||
|             ? this._renderUpdateCard( |             ? this._renderUpdateCard( | ||||||
|   | |||||||
| @@ -137,8 +137,7 @@ export class DialogHassioNetwork extends LitElement | |||||||
|         )} |         )} | ||||||
|         ${this._interface?.type === "wireless" |         ${this._interface?.type === "wireless" | ||||||
|           ? html` |           ? html` | ||||||
|               <ha-expansion-panel outlined> |               <ha-expansion-panel header="Wi-Fi" outlined> | ||||||
|                 <span slot="title">Wi-Fi</span> |  | ||||||
|                 ${this._interface?.wifi?.ssid |                 ${this._interface?.wifi?.ssid | ||||||
|                   ? html`<p>Connected to: ${this._interface?.wifi?.ssid}</p>` |                   ? html`<p>Connected to: ${this._interface?.wifi?.ssid}</p>` | ||||||
|                   : ""} |                   : ""} | ||||||
| @@ -281,8 +280,10 @@ export class DialogHassioNetwork extends LitElement | |||||||
|  |  | ||||||
|   private _renderIPConfiguration(version: string) { |   private _renderIPConfiguration(version: string) { | ||||||
|     return html` |     return html` | ||||||
|       <ha-expansion-panel outlined> |       <ha-expansion-panel | ||||||
|         <span slot="title">IPv${version.charAt(version.length - 1)}</span> |         .header=${`IPv${version.charAt(version.length - 1)}`} | ||||||
|  |         outlined | ||||||
|  |       > | ||||||
|         <div class="radio-row"> |         <div class="radio-row"> | ||||||
|           <ha-formfield label="DHCP"> |           <ha-formfield label="DHCP"> | ||||||
|             <ha-radio |             <ha-radio | ||||||
| @@ -591,6 +592,7 @@ export class DialogHassioNetwork extends LitElement | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         ha-expansion-panel { |         ha-expansion-panel { | ||||||
|  |           --expansion-panel-summary-padding: 0 16px; | ||||||
|           margin: 4px 0; |           margin: 4px 0; | ||||||
|         } |         } | ||||||
|         paper-input { |         paper-input { | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ from setuptools import setup, find_packages | |||||||
|  |  | ||||||
| setup( | setup( | ||||||
|     name="home-assistant-frontend", |     name="home-assistant-frontend", | ||||||
|     version="20201126.0", |     version="20201212.0", | ||||||
|     description="The Home Assistant frontend", |     description="The Home Assistant frontend", | ||||||
|     url="https://github.com/home-assistant/home-assistant-polymer", |     url="https://github.com/home-assistant/home-assistant-polymer", | ||||||
|     author="The Home Assistant Authors", |     author="The Home Assistant Authors", | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								src/common/ensure-array.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/common/ensure-array.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | export const ensureArray = (value?: any) => { | ||||||
|  |   if (!value || Array.isArray(value)) { | ||||||
|  |     return value; | ||||||
|  |   } | ||||||
|  |   return [value]; | ||||||
|  | }; | ||||||
| @@ -67,6 +67,10 @@ export const computeStateDisplay = ( | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   if (domain === "counter") { | ||||||
|  |     return formatNumber(compareState, language); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     // Return device class translation |     // Return device class translation | ||||||
|     (stateObj.attributes.device_class && |     (stateObj.attributes.device_class && | ||||||
|   | |||||||
| @@ -1,8 +1,12 @@ | |||||||
| export const copyToClipboard = (str) => { | export const copyToClipboard = (str) => { | ||||||
|   const el = document.createElement("textarea"); |   if (navigator.clipboard) { | ||||||
|   el.value = str; |     navigator.clipboard.writeText(str); | ||||||
|   document.body.appendChild(el); |   } else { | ||||||
|   el.select(); |     const el = document.createElement("textarea"); | ||||||
|   document.execCommand("copy"); |     el.value = str; | ||||||
|   document.body.removeChild(el); |     document.body.appendChild(el); | ||||||
|  |     el.select(); | ||||||
|  |     document.execCommand("copy"); | ||||||
|  |     document.body.removeChild(el); | ||||||
|  |   } | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -98,6 +98,12 @@ export class HaDataTable extends LitElement { | |||||||
|  |  | ||||||
|   @property({ type: Boolean }) public hasFab = false; |   @property({ type: Boolean }) public hasFab = false; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Add an extra rows at the bottom of the datatabel | ||||||
|  |    * @type {TemplateResult} | ||||||
|  |    */ | ||||||
|  |   @property({ attribute: false }) public appendRow?; | ||||||
|  |  | ||||||
|   @property({ type: Boolean, attribute: "auto-height" }) |   @property({ type: Boolean, attribute: "auto-height" }) | ||||||
|   public autoHeight = false; |   public autoHeight = false; | ||||||
|  |  | ||||||
| @@ -126,6 +132,8 @@ export class HaDataTable extends LitElement { | |||||||
|  |  | ||||||
|   @query("slot[name='header']") private _header!: HTMLSlotElement; |   @query("slot[name='header']") private _header!: HTMLSlotElement; | ||||||
|  |  | ||||||
|  |   private _items: DataTableRowData[] = []; | ||||||
|  |  | ||||||
|   private _checkableRowsCount?: number; |   private _checkableRowsCount?: number; | ||||||
|  |  | ||||||
|   private _checkedRows: string[] = []; |   private _checkedRows: string[] = []; | ||||||
| @@ -318,10 +326,13 @@ export class HaDataTable extends LitElement { | |||||||
|                   @scroll=${this._saveScrollPos} |                   @scroll=${this._saveScrollPos} | ||||||
|                 > |                 > | ||||||
|                   ${scroll({ |                   ${scroll({ | ||||||
|                     items: !this.hasFab |                     items: this._items, | ||||||
|                       ? this._filteredData |  | ||||||
|                       : [...this._filteredData, ...[{ empty: true }]], |  | ||||||
|                     renderItem: (row: DataTableRowData, index) => { |                     renderItem: (row: DataTableRowData, index) => { | ||||||
|  |                       if (row.append) { | ||||||
|  |                         return html` | ||||||
|  |                           <div class="mdc-data-table__row">${row.content}</div> | ||||||
|  |                         `; | ||||||
|  |                       } | ||||||
|                       if (row.empty) { |                       if (row.empty) { | ||||||
|                         return html` <div class="mdc-data-table__row"></div> `; |                         return html` <div class="mdc-data-table__row"></div> `; | ||||||
|                       } |                       } | ||||||
| @@ -447,6 +458,20 @@ export class HaDataTable extends LitElement { | |||||||
|     if (this.curRequest !== curRequest) { |     if (this.curRequest !== curRequest) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (this.appendRow || this.hasFab) { | ||||||
|  |       this._items = [...data]; | ||||||
|  |  | ||||||
|  |       if (this.appendRow) { | ||||||
|  |         this._items.push({ append: true, content: this.appendRow }); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (this.hasFab) { | ||||||
|  |         this._items.push({ empty: true }); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       this._items = data; | ||||||
|  |     } | ||||||
|     this._filteredData = data; |     this._filteredData = data; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -139,7 +139,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   private _filteredDevices: DeviceRegistryEntry[] = []; |   private _filteredDevices: DeviceRegistryEntry[] = []; | ||||||
|  |  | ||||||
|   private _getDevices = memoizeOne( |   private _getAreasWithDevices = memoizeOne( | ||||||
|     ( |     ( | ||||||
|       devices: DeviceRegistryEntry[], |       devices: DeviceRegistryEntry[], | ||||||
|       areas: AreaRegistryEntry[], |       areas: AreaRegistryEntry[], | ||||||
| @@ -277,7 +277,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { | |||||||
|     if (!this._devices || !this._areas || !this._entities) { |     if (!this._devices || !this._areas || !this._entities) { | ||||||
|       return html``; |       return html``; | ||||||
|     } |     } | ||||||
|     const areas = this._getDevices( |     const areas = this._getAreasWithDevices( | ||||||
|       this._devices, |       this._devices, | ||||||
|       this._areas, |       this._areas, | ||||||
|       this._entities, |       this._entities, | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import "../ha-icon-button"; | import "../ha-svg-icon"; | ||||||
|  | import "@material/mwc-icon-button/mwc-icon-button"; | ||||||
| import "@polymer/paper-input/paper-input"; | import "@polymer/paper-input/paper-input"; | ||||||
| import "@polymer/paper-item/paper-item"; | import "@polymer/paper-item/paper-item"; | ||||||
| import "@polymer/paper-item/paper-item-body"; | import "@polymer/paper-item/paper-item-body"; | ||||||
| @@ -12,6 +13,8 @@ import { | |||||||
|   html, |   html, | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   PropertyValues, | ||||||
|  |   query, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| @@ -35,6 +38,7 @@ import { | |||||||
| import { SubscribeMixin } from "../../mixins/subscribe-mixin"; | import { SubscribeMixin } from "../../mixins/subscribe-mixin"; | ||||||
| import { PolymerChangedEvent } from "../../polymer-types"; | import { PolymerChangedEvent } from "../../polymer-types"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
|  | import { mdiClose, mdiMenuUp, mdiMenuDown } from "@mdi/js"; | ||||||
|  |  | ||||||
| interface Device { | interface Device { | ||||||
|   name: string; |   name: string; | ||||||
| @@ -111,6 +115,10 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | |||||||
|   @property({ type: Boolean }) |   @property({ type: Boolean }) | ||||||
|   private _opened?: boolean; |   private _opened?: boolean; | ||||||
|  |  | ||||||
|  |   @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement; | ||||||
|  |  | ||||||
|  |   private _init = false; | ||||||
|  |  | ||||||
|   private _getDevices = memoizeOne( |   private _getDevices = memoizeOne( | ||||||
|     ( |     ( | ||||||
|       devices: DeviceRegistryEntry[], |       devices: DeviceRegistryEntry[], | ||||||
| @@ -122,18 +130,27 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | |||||||
|       deviceFilter: this["deviceFilter"] |       deviceFilter: this["deviceFilter"] | ||||||
|     ): Device[] => { |     ): Device[] => { | ||||||
|       if (!devices.length) { |       if (!devices.length) { | ||||||
|         return []; |         return [ | ||||||
|  |           { | ||||||
|  |             id: "", | ||||||
|  |             area: "", | ||||||
|  |             name: this.hass.localize("ui.components.device-picker.no_devices"), | ||||||
|  |           }, | ||||||
|  |         ]; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       const deviceEntityLookup: DeviceEntityLookup = {}; |       const deviceEntityLookup: DeviceEntityLookup = {}; | ||||||
|       for (const entity of entities) { |  | ||||||
|         if (!entity.device_id) { |       if (includeDomains || excludeDomains || includeDeviceClasses) { | ||||||
|           continue; |         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); | ||||||
|         } |         } | ||||||
|         if (!(entity.device_id in deviceEntityLookup)) { |  | ||||||
|           deviceEntityLookup[entity.device_id] = []; |  | ||||||
|         } |  | ||||||
|         deviceEntityLookup[entity.device_id].push(entity); |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; |       const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; | ||||||
| @@ -141,7 +158,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | |||||||
|         areaLookup[area.area_id] = area; |         areaLookup[area.area_id] = area; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       let inputDevices = [...devices]; |       let inputDevices = devices.filter( | ||||||
|  |         (device) => device.id === this.value || !device.disabled_by | ||||||
|  |       ); | ||||||
|  |  | ||||||
|       if (includeDomains) { |       if (includeDomains) { | ||||||
|         inputDevices = inputDevices.filter((device) => { |         inputDevices = inputDevices.filter((device) => { | ||||||
| @@ -208,6 +227,15 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | |||||||
|             : this.hass.localize("ui.components.device-picker.no_area"), |             : this.hass.localize("ui.components.device-picker.no_area"), | ||||||
|         }; |         }; | ||||||
|       }); |       }); | ||||||
|  |       if (!outputDevices.length) { | ||||||
|  |         return [ | ||||||
|  |           { | ||||||
|  |             id: "", | ||||||
|  |             area: "", | ||||||
|  |             name: this.hass.localize("ui.components.device-picker.no_match"), | ||||||
|  |           }, | ||||||
|  |         ]; | ||||||
|  |       } | ||||||
|       if (outputDevices.length === 1) { |       if (outputDevices.length === 1) { | ||||||
|         return outputDevices; |         return outputDevices; | ||||||
|       } |       } | ||||||
| @@ -215,6 +243,18 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | |||||||
|     } |     } | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  |   public open() { | ||||||
|  |     this.updateComplete.then(() => { | ||||||
|  |       (this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public focus() { | ||||||
|  |     this.updateComplete.then(() => { | ||||||
|  |       this.shadowRoot?.querySelector("paper-input")?.focus(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public hassSubscribe(): UnsubscribeFunc[] { |   public hassSubscribe(): UnsubscribeFunc[] { | ||||||
|     return [ |     return [ | ||||||
|       subscribeDeviceRegistry(this.hass.connection!, (devices) => { |       subscribeDeviceRegistry(this.hass.connection!, (devices) => { | ||||||
| @@ -229,25 +269,33 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | |||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   protected updated(changedProps: PropertyValues) { | ||||||
|  |     if ( | ||||||
|  |       (!this._init && this.devices && this.areas && this.entities) || | ||||||
|  |       (changedProps.has("_opened") && this._opened) | ||||||
|  |     ) { | ||||||
|  |       this._init = true; | ||||||
|  |       (this._comboBox as any).items = this._getDevices( | ||||||
|  |         this.devices!, | ||||||
|  |         this.areas!, | ||||||
|  |         this.entities!, | ||||||
|  |         this.includeDomains, | ||||||
|  |         this.excludeDomains, | ||||||
|  |         this.includeDeviceClasses, | ||||||
|  |         this.deviceFilter | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     if (!this.devices || !this.areas || !this.entities) { |     if (!this.devices || !this.areas || !this.entities) { | ||||||
|       return html``; |       return html``; | ||||||
|     } |     } | ||||||
|     const devices = this._getDevices( |  | ||||||
|       this.devices, |  | ||||||
|       this.areas, |  | ||||||
|       this.entities, |  | ||||||
|       this.includeDomains, |  | ||||||
|       this.excludeDomains, |  | ||||||
|       this.includeDeviceClasses, |  | ||||||
|       this.deviceFilter |  | ||||||
|     ); |  | ||||||
|     return html` |     return html` | ||||||
|       <vaadin-combo-box-light |       <vaadin-combo-box-light | ||||||
|         item-value-path="id" |         item-value-path="id" | ||||||
|         item-id-path="id" |         item-id-path="id" | ||||||
|         item-label-path="name" |         item-label-path="name" | ||||||
|         .items=${devices} |  | ||||||
|         .value=${this._value} |         .value=${this._value} | ||||||
|         .renderer=${rowRenderer} |         .renderer=${rowRenderer} | ||||||
|         @opened-changed=${this._openedChanged} |         @opened-changed=${this._openedChanged} | ||||||
| @@ -265,34 +313,30 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | |||||||
|         > |         > | ||||||
|           ${this.value |           ${this.value | ||||||
|             ? html` |             ? html` | ||||||
|                 <ha-icon-button |                 <mwc-icon-button | ||||||
|                   aria-label=${this.hass.localize( |                   .label=${this.hass.localize( | ||||||
|                     "ui.components.device-picker.clear" |                     "ui.components.device-picker.clear" | ||||||
|                   )} |                   )} | ||||||
|                   slot="suffix" |                   slot="suffix" | ||||||
|                   class="clear-button" |                   class="clear-button" | ||||||
|                   icon="hass:close" |  | ||||||
|                   @click=${this._clearValue} |                   @click=${this._clearValue} | ||||||
|                   no-ripple |  | ||||||
|                 > |                 > | ||||||
|                   Clear |                   <ha-svg-icon .path=${mdiClose}></ha-svg-icon> | ||||||
|                 </ha-icon-button> |                 </mwc-icon-button> | ||||||
|               ` |  | ||||||
|             : ""} |  | ||||||
|           ${devices.length > 0 |  | ||||||
|             ? html` |  | ||||||
|                 <ha-icon-button |  | ||||||
|                   aria-label=${this.hass.localize( |  | ||||||
|                     "ui.components.device-picker.show_devices" |  | ||||||
|                   )} |  | ||||||
|                   slot="suffix" |  | ||||||
|                   class="toggle-button" |  | ||||||
|                   .icon=${this._opened ? "hass:menu-up" : "hass:menu-down"} |  | ||||||
|                 > |  | ||||||
|                   Toggle |  | ||||||
|                 </ha-icon-button> |  | ||||||
|               ` |               ` | ||||||
|             : ""} |             : ""} | ||||||
|  |  | ||||||
|  |           <mwc-icon-button | ||||||
|  |             .label=${this.hass.localize( | ||||||
|  |               "ui.components.device-picker.show_devices" | ||||||
|  |             )} | ||||||
|  |             slot="suffix" | ||||||
|  |             class="toggle-button" | ||||||
|  |           > | ||||||
|  |             <ha-svg-icon | ||||||
|  |               .path=${this._opened ? mdiMenuUp : mdiMenuDown} | ||||||
|  |             ></ha-svg-icon> | ||||||
|  |           </mwc-icon-button> | ||||||
|         </paper-input> |         </paper-input> | ||||||
|       </vaadin-combo-box-light> |       </vaadin-combo-box-light> | ||||||
|     `; |     `; | ||||||
| @@ -329,7 +373,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   static get styles(): CSSResult { |   static get styles(): CSSResult { | ||||||
|     return css` |     return css` | ||||||
|       paper-input > ha-icon-button { |       paper-input > mwc-icon-button { | ||||||
|         --mdc-icon-button-size: 24px; |         --mdc-icon-button-size: 24px; | ||||||
|         padding: 2px; |         padding: 2px; | ||||||
|         color: var(--secondary-text-color); |         color: var(--secondary-text-color); | ||||||
|   | |||||||
| @@ -101,6 +101,18 @@ export class HaEntityPicker extends LitElement { | |||||||
|  |  | ||||||
|   @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement; |   @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement; | ||||||
|  |  | ||||||
|  |   public open() { | ||||||
|  |     this.updateComplete.then(() => { | ||||||
|  |       (this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public focus() { | ||||||
|  |     this.updateComplete.then(() => { | ||||||
|  |       this.shadowRoot?.querySelector("paper-input")?.focus(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _initedStates = false; |   private _initedStates = false; | ||||||
|  |  | ||||||
|   private _states: HassEntity[] = []; |   private _states: HassEntity[] = []; | ||||||
| @@ -153,6 +165,24 @@ export class HaEntityPicker extends LitElement { | |||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       if (!states.length) { | ||||||
|  |         return [ | ||||||
|  |           { | ||||||
|  |             entity_id: "", | ||||||
|  |             state: "", | ||||||
|  |             last_changed: "", | ||||||
|  |             last_updated: "", | ||||||
|  |             context: { id: "", user_id: null }, | ||||||
|  |             attributes: { | ||||||
|  |               friendly_name: this.hass!.localize( | ||||||
|  |                 "ui.components.entity.entity-picker.no_match" | ||||||
|  |               ), | ||||||
|  |               icon: "mdi:magnify", | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         ]; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       return states; |       return states; | ||||||
|     } |     } | ||||||
|   ); |   ); | ||||||
| @@ -203,7 +233,6 @@ export class HaEntityPicker extends LitElement { | |||||||
|           .label=${this.label === undefined |           .label=${this.label === undefined | ||||||
|             ? this.hass.localize("ui.components.entity.entity-picker.entity") |             ? this.hass.localize("ui.components.entity.entity-picker.entity") | ||||||
|             : this.label} |             : this.label} | ||||||
|           .value=${this._value} |  | ||||||
|           .disabled=${this.disabled} |           .disabled=${this.disabled} | ||||||
|           class="input" |           class="input" | ||||||
|           autocapitalize="none" |           autocapitalize="none" | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import "./ha-icon-button"; | import "./ha-svg-icon"; | ||||||
|  | import "@material/mwc-icon-button/mwc-icon-button"; | ||||||
| import "@polymer/paper-input/paper-input"; | import "@polymer/paper-input/paper-input"; | ||||||
| import "@polymer/paper-item/paper-item"; | import "@polymer/paper-item/paper-item"; | ||||||
| import "@polymer/paper-item/paper-item-body"; | import "@polymer/paper-item/paper-item-body"; | ||||||
| @@ -14,6 +15,8 @@ import { | |||||||
|   property, |   property, | ||||||
|   internalProperty, |   internalProperty, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
|  |   PropertyValues, | ||||||
|  |   query, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import { | import { | ||||||
| @@ -28,6 +31,19 @@ import { | |||||||
| import { SubscribeMixin } from "../mixins/subscribe-mixin"; | import { SubscribeMixin } from "../mixins/subscribe-mixin"; | ||||||
| import { PolymerChangedEvent } from "../polymer-types"; | import { PolymerChangedEvent } from "../polymer-types"; | ||||||
| import { HomeAssistant } from "../types"; | import { HomeAssistant } from "../types"; | ||||||
|  | import memoizeOne from "memoize-one"; | ||||||
|  | import { | ||||||
|  |   DeviceEntityLookup, | ||||||
|  |   DeviceRegistryEntry, | ||||||
|  |   subscribeDeviceRegistry, | ||||||
|  | } from "../data/device_registry"; | ||||||
|  | import { | ||||||
|  |   EntityRegistryEntry, | ||||||
|  |   subscribeEntityRegistry, | ||||||
|  | } from "../data/entity_registry"; | ||||||
|  | import { computeDomain } from "../common/entity/compute_domain"; | ||||||
|  | import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||||
|  | import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; | ||||||
|  |  | ||||||
| const rowRenderer = ( | const rowRenderer = ( | ||||||
|   root: HTMLElement, |   root: HTMLElement, | ||||||
| @@ -68,31 +84,252 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @property() public value?: string; |   @property() public value?: string; | ||||||
|  |  | ||||||
|   @property() public _areas?: AreaRegistryEntry[]; |   @property() public placeholder?: string; | ||||||
|  |  | ||||||
|   @property({ type: Boolean, attribute: "no-add" }) |   @property({ type: Boolean, attribute: "no-add" }) | ||||||
|   public noAdd?: boolean; |   public noAdd?: boolean; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Show only areas with entities from specific domains. | ||||||
|  |    * @type {Array} | ||||||
|  |    * @attr include-domains | ||||||
|  |    */ | ||||||
|  |   @property({ type: Array, attribute: "include-domains" }) | ||||||
|  |   public includeDomains?: string[]; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Show no areas with entities of these domains. | ||||||
|  |    * @type {Array} | ||||||
|  |    * @attr exclude-domains | ||||||
|  |    */ | ||||||
|  |   @property({ type: Array, attribute: "exclude-domains" }) | ||||||
|  |   public excludeDomains?: string[]; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Show only areas with entities of these device classes. | ||||||
|  |    * @type {Array} | ||||||
|  |    * @attr include-device-classes | ||||||
|  |    */ | ||||||
|  |   @property({ type: Array, attribute: "include-device-classes" }) | ||||||
|  |   public includeDeviceClasses?: string[]; | ||||||
|  |  | ||||||
|  |   @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; | ||||||
|  |  | ||||||
|  |   @property() public entityFilter?: (entity: EntityRegistryEntry) => boolean; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _areas?: AreaRegistryEntry[]; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _devices?: DeviceRegistryEntry[]; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _entities?: EntityRegistryEntry[]; | ||||||
|  |  | ||||||
|   @internalProperty() private _opened?: boolean; |   @internalProperty() private _opened?: boolean; | ||||||
|  |  | ||||||
|  |   @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement; | ||||||
|  |  | ||||||
|  |   private _init = false; | ||||||
|  |  | ||||||
|   public hassSubscribe(): UnsubscribeFunc[] { |   public hassSubscribe(): UnsubscribeFunc[] { | ||||||
|     return [ |     return [ | ||||||
|       subscribeAreaRegistry(this.hass.connection!, (areas) => { |       subscribeAreaRegistry(this.hass.connection!, (areas) => { | ||||||
|         this._areas = this.noAdd |         this._areas = areas; | ||||||
|           ? areas |       }), | ||||||
|           : [ |       subscribeDeviceRegistry(this.hass.connection!, (devices) => { | ||||||
|               ...areas, |         this._devices = devices; | ||||||
|               { |       }), | ||||||
|                 area_id: "add_new", |       subscribeEntityRegistry(this.hass.connection!, (entities) => { | ||||||
|                 name: this.hass.localize("ui.components.area-picker.add_new"), |         this._entities = entities; | ||||||
|               }, |  | ||||||
|             ]; |  | ||||||
|       }), |       }), | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public open() { | ||||||
|  |     this.updateComplete.then(() => { | ||||||
|  |       (this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public focus() { | ||||||
|  |     this.updateComplete.then(() => { | ||||||
|  |       this.shadowRoot?.querySelector("paper-input")?.focus(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _getAreas = memoizeOne( | ||||||
|  |     ( | ||||||
|  |       areas: AreaRegistryEntry[], | ||||||
|  |       devices: DeviceRegistryEntry[], | ||||||
|  |       entities: EntityRegistryEntry[], | ||||||
|  |       includeDomains: this["includeDomains"], | ||||||
|  |       excludeDomains: this["excludeDomains"], | ||||||
|  |       includeDeviceClasses: this["includeDeviceClasses"], | ||||||
|  |       deviceFilter: this["deviceFilter"], | ||||||
|  |       entityFilter: this["entityFilter"], | ||||||
|  |       noAdd: this["noAdd"] | ||||||
|  |     ): AreaRegistryEntry[] => { | ||||||
|  |       if (!areas.length) { | ||||||
|  |         return [ | ||||||
|  |           { | ||||||
|  |             area_id: "", | ||||||
|  |             name: this.hass.localize("ui.components.area-picker.no_areas"), | ||||||
|  |           }, | ||||||
|  |         ]; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const deviceEntityLookup: DeviceEntityLookup = {}; | ||||||
|  |       let inputDevices: DeviceRegistryEntry[] | undefined; | ||||||
|  |       let inputEntities: EntityRegistryEntry[] | undefined; | ||||||
|  |  | ||||||
|  |       if (includeDomains || excludeDomains || includeDeviceClasses) { | ||||||
|  |         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); | ||||||
|  |         } | ||||||
|  |         inputDevices = devices; | ||||||
|  |         inputEntities = entities.filter((entity) => entity.area_id); | ||||||
|  |       } else if (deviceFilter) { | ||||||
|  |         inputDevices = devices; | ||||||
|  |       } else if (entityFilter) { | ||||||
|  |         inputEntities = entities.filter((entity) => entity.area_id); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (includeDomains) { | ||||||
|  |         inputDevices = inputDevices!.filter((device) => { | ||||||
|  |           const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |           if (!devEntities || !devEntities.length) { | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|  |           return deviceEntityLookup[device.id].some((entity) => | ||||||
|  |             includeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |           ); | ||||||
|  |         }); | ||||||
|  |         inputEntities = inputEntities!.filter((entity) => | ||||||
|  |           includeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (excludeDomains) { | ||||||
|  |         inputDevices = inputDevices!.filter((device) => { | ||||||
|  |           const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |           if (!devEntities || !devEntities.length) { | ||||||
|  |             return true; | ||||||
|  |           } | ||||||
|  |           return entities.every( | ||||||
|  |             (entity) => | ||||||
|  |               !excludeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |           ); | ||||||
|  |         }); | ||||||
|  |         inputEntities = inputEntities!.filter( | ||||||
|  |           (entity) => !excludeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (includeDeviceClasses) { | ||||||
|  |         inputDevices = inputDevices!.filter((device) => { | ||||||
|  |           const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |           if (!devEntities || !devEntities.length) { | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|  |           return deviceEntityLookup[device.id].some((entity) => { | ||||||
|  |             const stateObj = this.hass.states[entity.entity_id]; | ||||||
|  |             if (!stateObj) { | ||||||
|  |               return false; | ||||||
|  |             } | ||||||
|  |             return ( | ||||||
|  |               stateObj.attributes.device_class && | ||||||
|  |               includeDeviceClasses.includes(stateObj.attributes.device_class) | ||||||
|  |             ); | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |         inputEntities = inputEntities!.filter((entity) => { | ||||||
|  |           const stateObj = this.hass.states[entity.entity_id]; | ||||||
|  |           return ( | ||||||
|  |             stateObj.attributes.device_class && | ||||||
|  |             includeDeviceClasses.includes(stateObj.attributes.device_class) | ||||||
|  |           ); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (deviceFilter) { | ||||||
|  |         inputDevices = inputDevices!.filter((device) => deviceFilter!(device)); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (entityFilter) { | ||||||
|  |         inputEntities = inputEntities!.filter((entity) => | ||||||
|  |           entityFilter!(entity) | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       let outputAreas = areas; | ||||||
|  |  | ||||||
|  |       let areaIds: string[] | undefined; | ||||||
|  |  | ||||||
|  |       if (inputDevices) { | ||||||
|  |         areaIds = inputDevices | ||||||
|  |           .filter((device) => device.area_id) | ||||||
|  |           .map((device) => device.area_id!); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (inputEntities) { | ||||||
|  |         areaIds = (areaIds ?? []).concat( | ||||||
|  |           inputEntities | ||||||
|  |             .filter((entity) => entity.area_id) | ||||||
|  |             .map((entity) => entity.area_id!) | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (areaIds) { | ||||||
|  |         outputAreas = areas.filter((area) => areaIds!.includes(area.area_id)); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (!outputAreas.length) { | ||||||
|  |         outputAreas = [ | ||||||
|  |           { | ||||||
|  |             area_id: "", | ||||||
|  |             name: this.hass.localize("ui.components.area-picker.no_match"), | ||||||
|  |           }, | ||||||
|  |         ]; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return noAdd | ||||||
|  |         ? outputAreas | ||||||
|  |         : [ | ||||||
|  |             ...outputAreas, | ||||||
|  |             { | ||||||
|  |               area_id: "add_new", | ||||||
|  |               name: this.hass.localize("ui.components.area-picker.add_new"), | ||||||
|  |             }, | ||||||
|  |           ]; | ||||||
|  |     } | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   protected updated(changedProps: PropertyValues) { | ||||||
|  |     if ( | ||||||
|  |       (!this._init && this._devices && this._areas && this._entities) || | ||||||
|  |       (changedProps.has("_opened") && this._opened) | ||||||
|  |     ) { | ||||||
|  |       this._init = true; | ||||||
|  |       (this._comboBox as any).items = this._getAreas( | ||||||
|  |         this._areas!, | ||||||
|  |         this._devices!, | ||||||
|  |         this._entities!, | ||||||
|  |         this.includeDomains, | ||||||
|  |         this.excludeDomains, | ||||||
|  |         this.includeDeviceClasses, | ||||||
|  |         this.deviceFilter, | ||||||
|  |         this.entityFilter, | ||||||
|  |         this.noAdd | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     if (!this._areas) { |     if (!this._devices || !this._areas || !this._entities) { | ||||||
|       return html``; |       return html``; | ||||||
|     } |     } | ||||||
|     return html` |     return html` | ||||||
| @@ -100,7 +337,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|         item-value-path="area_id" |         item-value-path="area_id" | ||||||
|         item-id-path="area_id" |         item-id-path="area_id" | ||||||
|         item-label-path="name" |         item-label-path="name" | ||||||
|         .items=${this._areas} |  | ||||||
|         .value=${this._value} |         .value=${this._value} | ||||||
|         .renderer=${rowRenderer} |         .renderer=${rowRenderer} | ||||||
|         @opened-changed=${this._openedChanged} |         @opened-changed=${this._openedChanged} | ||||||
| @@ -110,6 +346,9 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|           .label=${this.label === undefined && this.hass |           .label=${this.label === undefined && this.hass | ||||||
|             ? this.hass.localize("ui.components.area-picker.area") |             ? this.hass.localize("ui.components.area-picker.area") | ||||||
|             : this.label} |             : this.label} | ||||||
|  |           .placeholder=${this.placeholder | ||||||
|  |             ? this._area(this.placeholder)?.name | ||||||
|  |             : undefined} | ||||||
|           class="input" |           class="input" | ||||||
|           autocapitalize="none" |           autocapitalize="none" | ||||||
|           autocomplete="off" |           autocomplete="off" | ||||||
| @@ -118,39 +357,39 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|         > |         > | ||||||
|           ${this.value |           ${this.value | ||||||
|             ? html` |             ? html` | ||||||
|                 <ha-icon-button |                 <mwc-icon-button | ||||||
|                   aria-label=${this.hass.localize( |                   .label=${this.hass.localize( | ||||||
|                     "ui.components.area-picker.clear" |                     "ui.components.area-picker.clear" | ||||||
|                   )} |                   )} | ||||||
|                   slot="suffix" |                   slot="suffix" | ||||||
|                   class="clear-button" |                   class="clear-button" | ||||||
|                   icon="hass:close" |  | ||||||
|                   @click=${this._clearValue} |                   @click=${this._clearValue} | ||||||
|                   no-ripple |  | ||||||
|                 > |                 > | ||||||
|                   ${this.hass.localize("ui.components.area-picker.clear")} |                   <ha-svg-icon .path=${mdiClose}></ha-svg-icon> | ||||||
|                 </ha-icon-button> |                 </mwc-icon-button> | ||||||
|               ` |  | ||||||
|             : ""} |  | ||||||
|           ${this._areas.length > 0 |  | ||||||
|             ? html` |  | ||||||
|                 <ha-icon-button |  | ||||||
|                   aria-label=${this.hass.localize( |  | ||||||
|                     "ui.components.area-picker.show_areas" |  | ||||||
|                   )} |  | ||||||
|                   slot="suffix" |  | ||||||
|                   class="toggle-button" |  | ||||||
|                   .icon=${this._opened ? "hass:menu-up" : "hass:menu-down"} |  | ||||||
|                 > |  | ||||||
|                   ${this.hass.localize("ui.components.area-picker.toggle")} |  | ||||||
|                 </ha-icon-button> |  | ||||||
|               ` |               ` | ||||||
|             : ""} |             : ""} | ||||||
|  |  | ||||||
|  |           <mwc-icon-button | ||||||
|  |             .label=${this.hass.localize("ui.components.area-picker.toggle")} | ||||||
|  |             slot="suffix" | ||||||
|  |             class="toggle-button" | ||||||
|  |           > | ||||||
|  |             <ha-svg-icon | ||||||
|  |               .path=${this._opened ? mdiMenuUp : mdiMenuDown} | ||||||
|  |             ></ha-svg-icon> | ||||||
|  |           </mwc-icon-button> | ||||||
|         </paper-input> |         </paper-input> | ||||||
|       </vaadin-combo-box-light> |       </vaadin-combo-box-light> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _area = memoizeOne((areaId: string): | ||||||
|  |     | AreaRegistryEntry | ||||||
|  |     | undefined => { | ||||||
|  |     return this._areas?.find((area) => area.area_id === areaId); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   private _clearValue(ev: Event) { |   private _clearValue(ev: Event) { | ||||||
|     ev.stopPropagation(); |     ev.stopPropagation(); | ||||||
|     this._setValue(""); |     this._setValue(""); | ||||||
| @@ -215,7 +454,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   static get styles(): CSSResult { |   static get styles(): CSSResult { | ||||||
|     return css` |     return css` | ||||||
|       paper-input > ha-icon-button { |       paper-input > mwc-icon-button { | ||||||
|         --mdc-icon-button-size: 24px; |         --mdc-icon-button-size: 24px; | ||||||
|         padding: 2px; |         padding: 2px; | ||||||
|         color: var(--secondary-text-color); |         color: var(--secondary-text-color); | ||||||
|   | |||||||
| @@ -52,6 +52,7 @@ class HaBluePrintPicker extends LitElement { | |||||||
|         .label=${this.label || |         .label=${this.label || | ||||||
|         this.hass.localize("ui.components.blueprint-picker.label")} |         this.hass.localize("ui.components.blueprint-picker.label")} | ||||||
|         .disabled=${this.disabled} |         .disabled=${this.disabled} | ||||||
|  |         horizontal-align="left" | ||||||
|       > |       > | ||||||
|         <paper-listbox |         <paper-listbox | ||||||
|           slot="dropdown-content" |           slot="dropdown-content" | ||||||
| @@ -110,6 +111,9 @@ class HaBluePrintPicker extends LitElement { | |||||||
|       paper-listbox { |       paper-listbox { | ||||||
|         min-width: 200px; |         min-width: 200px; | ||||||
|       } |       } | ||||||
|  |       paper-item { | ||||||
|  |         cursor: pointer; | ||||||
|  |       } | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import { | |||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import type { ToggleButton } from "../types"; | import type { ToggleButton } from "../types"; | ||||||
| import "./ha-svg-icon"; | import "./ha-svg-icon"; | ||||||
|  | import "@material/mwc-button/mwc-button"; | ||||||
|  |  | ||||||
| @customElement("ha-button-toggle-group") | @customElement("ha-button-toggle-group") | ||||||
| export class HaButtonToggleGroup extends LitElement { | export class HaButtonToggleGroup extends LitElement { | ||||||
| @@ -21,17 +22,22 @@ export class HaButtonToggleGroup extends LitElement { | |||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <div> |       <div> | ||||||
|         ${this.buttons.map( |         ${this.buttons.map((button) => | ||||||
|           (button) => html` |           button.iconPath | ||||||
|             <mwc-icon-button |             ? html`<mwc-icon-button | ||||||
|               .label=${button.label} |                 .label=${button.label} | ||||||
|               .value=${button.value} |                 .value=${button.value} | ||||||
|               ?active=${this.active === button.value} |                 ?active=${this.active === button.value} | ||||||
|               @click=${this._handleClick} |                 @click=${this._handleClick} | ||||||
|             > |               > | ||||||
|               <ha-svg-icon .path=${button.iconPath}></ha-svg-icon> |                 <ha-svg-icon .path=${button.iconPath}></ha-svg-icon> | ||||||
|             </mwc-icon-button> |               </mwc-icon-button>` | ||||||
|           ` |             : html`<mwc-button | ||||||
|  |                 .value=${button.value} | ||||||
|  |                 ?active=${this.active === button.value} | ||||||
|  |                 @click=${this._handleClick} | ||||||
|  |                 >${button.label}</mwc-button | ||||||
|  |               >` | ||||||
|         )} |         )} | ||||||
|       </div> |       </div> | ||||||
|     `; |     `; | ||||||
| @@ -49,13 +55,15 @@ export class HaButtonToggleGroup extends LitElement { | |||||||
|         --mdc-icon-button-size: var(--button-toggle-size, 36px); |         --mdc-icon-button-size: var(--button-toggle-size, 36px); | ||||||
|         --mdc-icon-size: var(--button-toggle-icon-size, 20px); |         --mdc-icon-size: var(--button-toggle-icon-size, 20px); | ||||||
|       } |       } | ||||||
|       mwc-icon-button { |       mwc-icon-button, | ||||||
|  |       mwc-button { | ||||||
|         border: 1px solid var(--primary-color); |         border: 1px solid var(--primary-color); | ||||||
|         border-right-width: 0px; |         border-right-width: 0px; | ||||||
|         position: relative; |         position: relative; | ||||||
|         cursor: pointer; |         cursor: pointer; | ||||||
|       } |       } | ||||||
|       mwc-icon-button::before { |       mwc-icon-button::before, | ||||||
|  |       mwc-button::before { | ||||||
|         top: 0; |         top: 0; | ||||||
|         left: 0; |         left: 0; | ||||||
|         width: 100%; |         width: 100%; | ||||||
| @@ -67,17 +75,21 @@ export class HaButtonToggleGroup extends LitElement { | |||||||
|         content: ""; |         content: ""; | ||||||
|         transition: opacity 15ms linear, background-color 15ms linear; |         transition: opacity 15ms linear, background-color 15ms linear; | ||||||
|       } |       } | ||||||
|       mwc-icon-button[active]::before { |       mwc-icon-button[active]::before, | ||||||
|  |       mwc-button[active]::before { | ||||||
|         opacity: var(--mdc-icon-button-ripple-opacity, 0.12); |         opacity: var(--mdc-icon-button-ripple-opacity, 0.12); | ||||||
|       } |       } | ||||||
|       mwc-icon-button:first-child { |       mwc-icon-button:first-child, | ||||||
|  |       mwc-button:first-child { | ||||||
|         border-radius: 4px 0 0 4px; |         border-radius: 4px 0 0 4px; | ||||||
|       } |       } | ||||||
|       mwc-icon-button:last-child { |       mwc-icon-button:last-child, | ||||||
|  |       mwc-button:last-child { | ||||||
|         border-radius: 0 4px 4px 0; |         border-radius: 0 4px 4px 0; | ||||||
|         border-right-width: 1px; |         border-right-width: 1px; | ||||||
|       } |       } | ||||||
|       mwc-icon-button:only-child { |       mwc-icon-button:only-child, | ||||||
|  |       mwc-button:only-child { | ||||||
|         border-radius: 4px; |         border-radius: 4px; | ||||||
|         border-right-width: 1px; |         border-right-width: 1px; | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -19,12 +19,14 @@ class HaExpansionPanel extends LitElement { | |||||||
|  |  | ||||||
|   @property({ type: Boolean, reflect: true }) outlined = false; |   @property({ type: Boolean, reflect: true }) outlined = false; | ||||||
|  |  | ||||||
|  |   @property() header?: string; | ||||||
|  |  | ||||||
|   @query(".container") private _container!: HTMLDivElement; |   @query(".container") private _container!: HTMLDivElement; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <div class="summary" @click=${this._toggleContainer}> |       <div class="summary" @click=${this._toggleContainer}> | ||||||
|         <slot name="title"></slot> |         <slot name="header">${this.header}</slot> | ||||||
|         <ha-svg-icon |         <ha-svg-icon | ||||||
|           .path=${mdiChevronDown} |           .path=${mdiChevronDown} | ||||||
|           class="summary-icon ${classMap({ expanded: this.expanded })}" |           class="summary-icon ${classMap({ expanded: this.expanded })}" | ||||||
| @@ -76,7 +78,7 @@ class HaExpansionPanel extends LitElement { | |||||||
|  |  | ||||||
|       .summary { |       .summary { | ||||||
|         display: flex; |         display: flex; | ||||||
|         padding: var(--expansion-panel-summary-padding, 0px 16px); |         padding: var(--expansion-panel-summary-padding, 0); | ||||||
|         min-height: 48px; |         min-height: 48px; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|         cursor: pointer; |         cursor: pointer; | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								src/components/ha-fab.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/components/ha-fab.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | import type { Fab } from "@material/mwc-fab"; | ||||||
|  | import "@material/mwc-fab"; | ||||||
|  | import { customElement } from "lit-element"; | ||||||
|  | import { Constructor } from "../types"; | ||||||
|  |  | ||||||
|  | const MwcFab = customElements.get("mwc-fab") as Constructor<Fab>; | ||||||
|  |  | ||||||
|  | @customElement("ha-fab") | ||||||
|  | export class HaFab extends MwcFab { | ||||||
|  |   protected firstUpdated(changedProperties) { | ||||||
|  |     super.firstUpdated(changedProperties); | ||||||
|  |     this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)"); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-fab": HaFab; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -127,7 +127,7 @@ class HaHLSPlayer extends LitElement { | |||||||
|  |  | ||||||
|     // Parse playlist assuming it is a master playlist. Match group 1 is whether hevc, match group 2 is regular playlist url |     // Parse playlist assuming it is a master playlist. Match group 1 is whether hevc, match group 2 is regular playlist url | ||||||
|     // See https://tools.ietf.org/html/rfc8216 for HLS spec details |     // See https://tools.ietf.org/html/rfc8216 for HLS spec details | ||||||
|     const playlistRegexp = /#EXT-X-STREAM-INF:.*?(?:CODECS=".*?(?<isHevc>hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(?<streamUrl>.+)/g; |     const playlistRegexp = /#EXT-X-STREAM-INF:.*?(?:CODECS=".*?(hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(.+)/g; | ||||||
|     const match = playlistRegexp.exec(masterPlaylist); |     const match = playlistRegexp.exec(masterPlaylist); | ||||||
|     const matchTwice = playlistRegexp.exec(masterPlaylist); |     const matchTwice = playlistRegexp.exec(masterPlaylist); | ||||||
|  |  | ||||||
| @@ -136,17 +136,13 @@ class HaHLSPlayer extends LitElement { | |||||||
|     let playlist_url: string; |     let playlist_url: string; | ||||||
|     if (match !== null && matchTwice === null) { |     if (match !== null && matchTwice === null) { | ||||||
|       // Only send the regular playlist url if we match exactly once |       // Only send the regular playlist url if we match exactly once | ||||||
|       playlist_url = new URL(match.groups!.streamUrl, this.url).href; |       playlist_url = new URL(match[2], this.url).href; | ||||||
|     } else { |     } else { | ||||||
|       playlist_url = this.url; |       playlist_url = this.url; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // If codec is HEVC and ExoPlayer is supported, use ExoPlayer. |     // If codec is HEVC and ExoPlayer is supported, use ExoPlayer. | ||||||
|     if ( |     if (this._useExoPlayer && match !== null && match[1] !== undefined) { | ||||||
|       this._useExoPlayer && |  | ||||||
|       match !== null && |  | ||||||
|       match.groups!.isHevc !== undefined |  | ||||||
|     ) { |  | ||||||
|       this._renderHLSExoPlayer(playlist_url); |       this._renderHLSExoPlayer(playlist_url); | ||||||
|     } else if (hls.isSupported()) { |     } else if (hls.isSupported()) { | ||||||
|       this._renderHLSPolyfill(videoEl, hls, playlist_url); |       this._renderHLSPolyfill(videoEl, hls, playlist_url); | ||||||
|   | |||||||
| @@ -60,8 +60,9 @@ export class HaIconInput extends LitElement { | |||||||
|   static get styles() { |   static get styles() { | ||||||
|     return css` |     return css` | ||||||
|       ha-icon { |       ha-icon { | ||||||
|         position: relative; |         position: absolute; | ||||||
|         bottom: 4px; |         bottom: 2px; | ||||||
|  |         right: 0; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ import type { HomeAssistant } from "../types"; | |||||||
| class HaRelativeTime extends UpdatingElement { | class HaRelativeTime extends UpdatingElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|   @property({ attribute: false }) public datetime?: string; |   @property({ attribute: false }) public datetime?: string | Date; | ||||||
|  |  | ||||||
|   private _interval?: number; |   private _interval?: number; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										45
									
								
								src/components/ha-selector/ha-selector-action.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/components/ha-selector/ha-selector-action.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | import { | ||||||
|  |   css, | ||||||
|  |   CSSResult, | ||||||
|  |   customElement, | ||||||
|  |   html, | ||||||
|  |   LitElement, | ||||||
|  |   property, | ||||||
|  | } from "lit-element"; | ||||||
|  | import { HomeAssistant } from "../../types"; | ||||||
|  | import { ActionSelector } from "../../data/selector"; | ||||||
|  | import { Action } from "../../data/script"; | ||||||
|  | import "../../panels/config/automation/action/ha-automation-action"; | ||||||
|  |  | ||||||
|  | @customElement("ha-selector-action") | ||||||
|  | export class HaActionSelector extends LitElement { | ||||||
|  |   @property() public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @property() public selector!: ActionSelector; | ||||||
|  |  | ||||||
|  |   @property() public value?: Action; | ||||||
|  |  | ||||||
|  |   @property() public label?: string; | ||||||
|  |  | ||||||
|  |   protected render() { | ||||||
|  |     return html`<ha-automation-action | ||||||
|  |       .actions=${this.value || []} | ||||||
|  |       .hass=${this.hass} | ||||||
|  |     ></ha-automation-action>`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static get styles(): CSSResult { | ||||||
|  |     return css` | ||||||
|  |       ha-automation-action { | ||||||
|  |         display: block; | ||||||
|  |         margin-bottom: 16px; | ||||||
|  |       } | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-selector-action": HaActionSelector; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,7 +1,16 @@ | |||||||
| import { customElement, html, LitElement, property } from "lit-element"; | import { | ||||||
|  |   customElement, | ||||||
|  |   html, | ||||||
|  |   internalProperty, | ||||||
|  |   LitElement, | ||||||
|  |   property, | ||||||
|  | } from "lit-element"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
| import { AreaSelector } from "../../data/selector"; | import { AreaSelector } from "../../data/selector"; | ||||||
| import "../ha-area-picker"; | import "../ha-area-picker"; | ||||||
|  | import { ConfigEntry, getConfigEntries } from "../../data/config_entries"; | ||||||
|  | import { DeviceRegistryEntry } from "../../data/device_registry"; | ||||||
|  | import { EntityRegistryEntry } from "../../data/entity_registry"; | ||||||
|  |  | ||||||
| @customElement("ha-selector-area") | @customElement("ha-selector-area") | ||||||
| export class HaAreaSelector extends LitElement { | export class HaAreaSelector extends LitElement { | ||||||
| @@ -13,14 +22,77 @@ export class HaAreaSelector extends LitElement { | |||||||
|  |  | ||||||
|   @property() public label?: string; |   @property() public label?: string; | ||||||
|  |  | ||||||
|  |   @internalProperty() public _configEntries?: ConfigEntry[]; | ||||||
|  |  | ||||||
|  |   protected updated(changedProperties) { | ||||||
|  |     if (changedProperties.has("selector")) { | ||||||
|  |       const oldSelector = changedProperties.get("selector"); | ||||||
|  |       if ( | ||||||
|  |         oldSelector !== this.selector && | ||||||
|  |         this.selector.area.device?.integration | ||||||
|  |       ) { | ||||||
|  |         this._loadConfigEntries(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|     return html`<ha-area-picker |     return html`<ha-area-picker | ||||||
|       .hass=${this.hass} |       .hass=${this.hass} | ||||||
|       .value=${this.value} |       .value=${this.value} | ||||||
|       .label=${this.label} |       .label=${this.label} | ||||||
|       no-add |       no-add | ||||||
|  |       .deviceFilter=${(device) => this._filterDevices(device)} | ||||||
|  |       .entityFilter=${(entity) => this._filterEntities(entity)} | ||||||
|  |       .includeDeviceClasses=${this.selector.area.entity?.device_class | ||||||
|  |         ? [this.selector.area.entity.device_class] | ||||||
|  |         : undefined} | ||||||
|  |       .includeDomains=${this.selector.area.entity?.domain | ||||||
|  |         ? [this.selector.area.entity.domain] | ||||||
|  |         : undefined} | ||||||
|     ></ha-area-picker>`; |     ></ha-area-picker>`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _filterEntities(entity: EntityRegistryEntry): boolean { | ||||||
|  |     if (this.selector.area.entity?.integration) { | ||||||
|  |       if (entity.platform !== this.selector.area.entity.integration) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _filterDevices(device: DeviceRegistryEntry): boolean { | ||||||
|  |     if ( | ||||||
|  |       this.selector.area.device?.manufacturer && | ||||||
|  |       device.manufacturer !== this.selector.area.device.manufacturer | ||||||
|  |     ) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if ( | ||||||
|  |       this.selector.area.device?.model && | ||||||
|  |       device.model !== this.selector.area.device.model | ||||||
|  |     ) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if (this.selector.area.device?.integration) { | ||||||
|  |       if ( | ||||||
|  |         this._configEntries && | ||||||
|  |         !this._configEntries.some((entry) => | ||||||
|  |           device.config_entries.includes(entry.entry_id) | ||||||
|  |         ) | ||||||
|  |       ) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _loadConfigEntries() { | ||||||
|  |     this._configEntries = (await getConfigEntries(this.hass)).filter( | ||||||
|  |       (entry) => entry.domain === this.selector.area.device?.integration | ||||||
|  |     ); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -63,7 +63,8 @@ export class HaDeviceSelector extends LitElement { | |||||||
|     } |     } | ||||||
|     if (this.selector.device.integration) { |     if (this.selector.device.integration) { | ||||||
|       if ( |       if ( | ||||||
|         !this._configEntries?.some((entry) => |         this._configEntries && | ||||||
|  |         !this._configEntries.some((entry) => | ||||||
|           device.config_entries.includes(entry.entry_id) |           device.config_entries.includes(entry.entry_id) | ||||||
|         ) |         ) | ||||||
|       ) { |       ) { | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @property() public selector!: EntitySelector; |   @property() public selector!: EntitySelector; | ||||||
|  |  | ||||||
|   @internalProperty() private _entities?: Record<string, string>; |   @internalProperty() private _entityPlaformLookup?: Record<string, string>; | ||||||
|  |  | ||||||
|   @property() public value?: any; |   @property() public value?: any; | ||||||
|  |  | ||||||
| @@ -45,7 +45,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { | |||||||
|           } |           } | ||||||
|           entityLookup[confEnt.entity_id] = confEnt.platform; |           entityLookup[confEnt.entity_id] = confEnt.platform; | ||||||
|         } |         } | ||||||
|         this._entities = entityLookup; |         this._entityPlaformLookup = entityLookup; | ||||||
|       }), |       }), | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
| @@ -66,8 +66,9 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { | |||||||
|     } |     } | ||||||
|     if (this.selector.entity.integration) { |     if (this.selector.entity.integration) { | ||||||
|       if ( |       if ( | ||||||
|         !this._entities || |         !this._entityPlaformLookup || | ||||||
|         this._entities[entity.entity_id] !== this.selector.entity.integration |         this._entityPlaformLookup[entity.entity_id] !== | ||||||
|  |           this.selector.entity.integration | ||||||
|       ) { |       ) { | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
|   | |||||||
							
								
								
									
										153
									
								
								src/components/ha-selector/ha-selector-target.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								src/components/ha-selector/ha-selector-target.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | |||||||
|  | import { | ||||||
|  |   css, | ||||||
|  |   CSSResult, | ||||||
|  |   customElement, | ||||||
|  |   html, | ||||||
|  |   internalProperty, | ||||||
|  |   LitElement, | ||||||
|  |   property, | ||||||
|  | } from "lit-element"; | ||||||
|  | import { HomeAssistant } from "../../types"; | ||||||
|  | import { TargetSelector } from "../../data/selector"; | ||||||
|  | import { ConfigEntry, getConfigEntries } from "../../data/config_entries"; | ||||||
|  | import { DeviceRegistryEntry } from "../../data/device_registry"; | ||||||
|  | import "../ha-target-picker"; | ||||||
|  | import "@material/mwc-list/mwc-list-item"; | ||||||
|  | import "@polymer/paper-input/paper-input"; | ||||||
|  | import "@material/mwc-list/mwc-list"; | ||||||
|  | import { | ||||||
|  |   EntityRegistryEntry, | ||||||
|  |   subscribeEntityRegistry, | ||||||
|  | } from "../../data/entity_registry"; | ||||||
|  | import { Target } from "../../data/target"; | ||||||
|  | import "@material/mwc-tab-bar/mwc-tab-bar"; | ||||||
|  | import "@material/mwc-tab/mwc-tab"; | ||||||
|  | import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||||
|  | import { SubscribeMixin } from "../../mixins/subscribe-mixin"; | ||||||
|  |  | ||||||
|  | @customElement("ha-selector-target") | ||||||
|  | export class HaTargetSelector extends SubscribeMixin(LitElement) { | ||||||
|  |   @property() public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @property() public selector!: TargetSelector; | ||||||
|  |  | ||||||
|  |   @property() public value?: Target; | ||||||
|  |  | ||||||
|  |   @property() public label?: string; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _entityPlaformLookup?: Record<string, string>; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _configEntries?: ConfigEntry[]; | ||||||
|  |  | ||||||
|  |   public hassSubscribe(): UnsubscribeFunc[] { | ||||||
|  |     return [ | ||||||
|  |       subscribeEntityRegistry(this.hass.connection!, (entities) => { | ||||||
|  |         const entityLookup = {}; | ||||||
|  |         for (const confEnt of entities) { | ||||||
|  |           if (!confEnt.platform) { | ||||||
|  |             continue; | ||||||
|  |           } | ||||||
|  |           entityLookup[confEnt.entity_id] = confEnt.platform; | ||||||
|  |         } | ||||||
|  |         this._entityPlaformLookup = entityLookup; | ||||||
|  |       }), | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected updated(changedProperties) { | ||||||
|  |     if (changedProperties.has("selector")) { | ||||||
|  |       const oldSelector = changedProperties.get("selector"); | ||||||
|  |       if ( | ||||||
|  |         oldSelector !== this.selector && | ||||||
|  |         this.selector.target.device?.integration | ||||||
|  |       ) { | ||||||
|  |         this._loadConfigEntries(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected render() { | ||||||
|  |     return html`<ha-target-picker | ||||||
|  |       .hass=${this.hass} | ||||||
|  |       .value=${this.value} | ||||||
|  |       .deviceFilter=${(device) => this._filterDevices(device)} | ||||||
|  |       .entityRegFilter=${(entity: EntityRegistryEntry) => | ||||||
|  |         this._filterRegEntities(entity)} | ||||||
|  |       .entityFilter=${(entity: HassEntity) => this._filterEntities(entity)} | ||||||
|  |       .includeDeviceClasses=${this.selector.target.entity?.device_class | ||||||
|  |         ? [this.selector.target.entity.device_class] | ||||||
|  |         : undefined} | ||||||
|  |       .includeDomains=${this.selector.target.entity?.domain | ||||||
|  |         ? [this.selector.target.entity.domain] | ||||||
|  |         : undefined} | ||||||
|  |     ></ha-target-picker>`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _filterEntities(entity: HassEntity): boolean { | ||||||
|  |     if (this.selector.target.entity?.integration) { | ||||||
|  |       if ( | ||||||
|  |         !this._entityPlaformLookup || | ||||||
|  |         this._entityPlaformLookup[entity.entity_id] !== | ||||||
|  |           this.selector.target.entity.integration | ||||||
|  |       ) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _filterRegEntities(entity: EntityRegistryEntry): boolean { | ||||||
|  |     if (this.selector.target.entity?.integration) { | ||||||
|  |       if (entity.platform !== this.selector.target.entity.integration) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _filterDevices(device: DeviceRegistryEntry): boolean { | ||||||
|  |     if ( | ||||||
|  |       this.selector.target.device?.manufacturer && | ||||||
|  |       device.manufacturer !== this.selector.target.device.manufacturer | ||||||
|  |     ) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if ( | ||||||
|  |       this.selector.target.device?.model && | ||||||
|  |       device.model !== this.selector.target.device.model | ||||||
|  |     ) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if (this.selector.target.device?.integration) { | ||||||
|  |       if ( | ||||||
|  |         !this._configEntries?.some((entry) => | ||||||
|  |           device.config_entries.includes(entry.entry_id) | ||||||
|  |         ) | ||||||
|  |       ) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _loadConfigEntries() { | ||||||
|  |     this._configEntries = (await getConfigEntries(this.hass)).filter( | ||||||
|  |       (entry) => entry.domain === this.selector.target.device?.integration | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static get styles(): CSSResult { | ||||||
|  |     return css` | ||||||
|  |       ha-target-picker { | ||||||
|  |         margin: 0 -8px; | ||||||
|  |         display: block; | ||||||
|  |       } | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-selector-target": HaTargetSelector; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -5,9 +5,11 @@ import { HomeAssistant } from "../../types"; | |||||||
| import "./ha-selector-entity"; | import "./ha-selector-entity"; | ||||||
| import "./ha-selector-device"; | import "./ha-selector-device"; | ||||||
| import "./ha-selector-area"; | import "./ha-selector-area"; | ||||||
|  | import "./ha-selector-target"; | ||||||
| import "./ha-selector-number"; | import "./ha-selector-number"; | ||||||
| import "./ha-selector-boolean"; | import "./ha-selector-boolean"; | ||||||
| import "./ha-selector-time"; | import "./ha-selector-time"; | ||||||
|  | import "./ha-selector-action"; | ||||||
| import { Selector } from "../../data/selector"; | import { Selector } from "../../data/selector"; | ||||||
|  |  | ||||||
| @customElement("ha-selector") | @customElement("ha-selector") | ||||||
|   | |||||||
| @@ -18,11 +18,6 @@ export class HaSettingsRow extends LitElement { | |||||||
|  |  | ||||||
|   protected render(): SVGTemplateResult { |   protected render(): SVGTemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <style> |  | ||||||
|         paper-item-body { |  | ||||||
|           padding-right: 16px; |  | ||||||
|         } |  | ||||||
|       </style> |  | ||||||
|       <paper-item-body |       <paper-item-body | ||||||
|         ?two-line=${!this.threeLine} |         ?two-line=${!this.threeLine} | ||||||
|         ?three-line=${this.threeLine} |         ?three-line=${this.threeLine} | ||||||
| @@ -43,6 +38,14 @@ export class HaSettingsRow extends LitElement { | |||||||
|         align-self: auto; |         align-self: auto; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|       } |       } | ||||||
|  |       paper-item-body { | ||||||
|  |         padding: 8px 16px 8px 0; | ||||||
|  |       } | ||||||
|  |       paper-item-body[two-line] { | ||||||
|  |         min-height: calc( | ||||||
|  |           var(--paper-item-body-two-line-min-height, 72px) - 16px | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|       :host([narrow]) { |       :host([narrow]) { | ||||||
|         align-items: normal; |         align-items: normal; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
| @@ -52,6 +55,9 @@ export class HaSettingsRow extends LitElement { | |||||||
|       ::slotted(ha-switch) { |       ::slotted(ha-switch) { | ||||||
|         padding: 16px 0; |         padding: 16px 0; | ||||||
|       } |       } | ||||||
|  |       div[secondary] { | ||||||
|  |         white-space: normal; | ||||||
|  |       } | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										605
									
								
								src/components/ha-target-picker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										605
									
								
								src/components/ha-target-picker.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,605 @@ | |||||||
|  | import { | ||||||
|  |   css, | ||||||
|  |   CSSResult, | ||||||
|  |   customElement, | ||||||
|  |   html, | ||||||
|  |   internalProperty, | ||||||
|  |   LitElement, | ||||||
|  |   property, | ||||||
|  |   query, | ||||||
|  |   unsafeCSS, | ||||||
|  | } from "lit-element"; | ||||||
|  | import { HomeAssistant } from "../types"; | ||||||
|  | // @ts-ignore | ||||||
|  | import chipStyles from "@material/chips/dist/mdc.chips.min.css"; | ||||||
|  | import { | ||||||
|  |   mdiSofa, | ||||||
|  |   mdiDevices, | ||||||
|  |   mdiClose, | ||||||
|  |   mdiPlus, | ||||||
|  |   mdiUnfoldMoreVertical, | ||||||
|  | } from "@mdi/js"; | ||||||
|  | import "./ha-svg-icon"; | ||||||
|  | import "./ha-icon"; | ||||||
|  | import "@material/mwc-icon-button/mwc-icon-button"; | ||||||
|  | import { classMap } from "lit-html/directives/class-map"; | ||||||
|  | import "@material/mwc-button/mwc-button"; | ||||||
|  | import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||||
|  | import { | ||||||
|  |   AreaRegistryEntry, | ||||||
|  |   subscribeAreaRegistry, | ||||||
|  | } from "../data/area_registry"; | ||||||
|  | import { | ||||||
|  |   computeDeviceName, | ||||||
|  |   DeviceRegistryEntry, | ||||||
|  |   subscribeDeviceRegistry, | ||||||
|  | } from "../data/device_registry"; | ||||||
|  | import { | ||||||
|  |   EntityRegistryEntry, | ||||||
|  |   subscribeEntityRegistry, | ||||||
|  | } from "../data/entity_registry"; | ||||||
|  | import { SubscribeMixin } from "../mixins/subscribe-mixin"; | ||||||
|  | import { computeStateName } from "../common/entity/compute_state_name"; | ||||||
|  | import { stateIcon } from "../common/entity/state_icon"; | ||||||
|  | import { fireEvent } from "../common/dom/fire_event"; | ||||||
|  | import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||||
|  | import { computeDomain } from "../common/entity/compute_domain"; | ||||||
|  | import { Target } from "../data/target"; | ||||||
|  | import { ensureArray } from "../common/ensure-array"; | ||||||
|  | import "./entity/ha-entity-picker"; | ||||||
|  | import "./device/ha-device-picker"; | ||||||
|  | import "./ha-area-picker"; | ||||||
|  | import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker"; | ||||||
|  | import "@polymer/paper-tooltip/paper-tooltip"; | ||||||
|  |  | ||||||
|  | @customElement("ha-target-picker") | ||||||
|  | export class HaTargetPicker extends SubscribeMixin(LitElement) { | ||||||
|  |   @property() public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @property() public value?: Target; | ||||||
|  |  | ||||||
|  |   @property() public label?: string; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Show only targets with entities from specific domains. | ||||||
|  |    * @type {Array} | ||||||
|  |    * @attr include-domains | ||||||
|  |    */ | ||||||
|  |   @property({ type: Array, attribute: "include-domains" }) | ||||||
|  |   public includeDomains?: string[]; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Show only targets with entities of these device classes. | ||||||
|  |    * @type {Array} | ||||||
|  |    * @attr include-device-classes | ||||||
|  |    */ | ||||||
|  |   @property({ type: Array, attribute: "include-device-classes" }) | ||||||
|  |   public includeDeviceClasses?: string[]; | ||||||
|  |  | ||||||
|  |   @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; | ||||||
|  |  | ||||||
|  |   @property() public entityRegFilter?: (entity: EntityRegistryEntry) => boolean; | ||||||
|  |  | ||||||
|  |   @property() public entityFilter?: HaEntityPickerEntityFilterFunc; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _areas?: { [areaId: string]: AreaRegistryEntry }; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _devices?: { | ||||||
|  |     [deviceId: string]: DeviceRegistryEntry; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _entities?: EntityRegistryEntry[]; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _addMode?: "area_id" | "entity_id" | "device_id"; | ||||||
|  |  | ||||||
|  |   @query("#input") private _inputElement?; | ||||||
|  |  | ||||||
|  |   public hassSubscribe(): UnsubscribeFunc[] { | ||||||
|  |     return [ | ||||||
|  |       subscribeAreaRegistry(this.hass.connection!, (areas) => { | ||||||
|  |         const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; | ||||||
|  |         for (const area of areas) { | ||||||
|  |           areaLookup[area.area_id] = area; | ||||||
|  |         } | ||||||
|  |         this._areas = areaLookup; | ||||||
|  |       }), | ||||||
|  |       subscribeDeviceRegistry(this.hass.connection!, (devices) => { | ||||||
|  |         const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {}; | ||||||
|  |         for (const device of devices) { | ||||||
|  |           deviceLookup[device.id] = device; | ||||||
|  |         } | ||||||
|  |         this._devices = deviceLookup; | ||||||
|  |       }), | ||||||
|  |       subscribeEntityRegistry(this.hass.connection!, (entities) => { | ||||||
|  |         this._entities = entities; | ||||||
|  |       }), | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected render() { | ||||||
|  |     if (!this._areas || !this._devices || !this._entities) { | ||||||
|  |       return html``; | ||||||
|  |     } | ||||||
|  |     return html`<div class="mdc-chip-set items"> | ||||||
|  |         ${ensureArray(this.value?.area_id)?.map((area_id) => { | ||||||
|  |           const area = this._areas![area_id]; | ||||||
|  |           return this._renderChip( | ||||||
|  |             "area_id", | ||||||
|  |             area_id, | ||||||
|  |             area?.name || area_id, | ||||||
|  |             undefined, | ||||||
|  |             mdiSofa | ||||||
|  |           ); | ||||||
|  |         })} | ||||||
|  |         ${ensureArray(this.value?.device_id)?.map((device_id) => { | ||||||
|  |           const device = this._devices![device_id]; | ||||||
|  |           return this._renderChip( | ||||||
|  |             "device_id", | ||||||
|  |             device_id, | ||||||
|  |             device ? computeDeviceName(device, this.hass) : device_id, | ||||||
|  |             undefined, | ||||||
|  |             mdiDevices | ||||||
|  |           ); | ||||||
|  |         })} | ||||||
|  |         ${ensureArray(this.value?.entity_id)?.map((entity_id) => { | ||||||
|  |           const entity = this.hass.states[entity_id]; | ||||||
|  |           return this._renderChip( | ||||||
|  |             "entity_id", | ||||||
|  |             entity_id, | ||||||
|  |             entity ? computeStateName(entity) : entity_id, | ||||||
|  |             entity ? stateIcon(entity) : undefined | ||||||
|  |           ); | ||||||
|  |         })} | ||||||
|  |       </div> | ||||||
|  |       ${this._renderPicker()} | ||||||
|  |       <div class="mdc-chip-set"> | ||||||
|  |         <div | ||||||
|  |           class="mdc-chip area_id add" | ||||||
|  |           .type=${"area_id"} | ||||||
|  |           @click=${this._showPicker} | ||||||
|  |         > | ||||||
|  |           <div class="mdc-chip__ripple"></div> | ||||||
|  |           <ha-svg-icon | ||||||
|  |             class="mdc-chip__icon mdc-chip__icon--leading" | ||||||
|  |             .path=${mdiPlus} | ||||||
|  |           ></ha-svg-icon> | ||||||
|  |           <span role="gridcell"> | ||||||
|  |             <span role="button" tabindex="0" class="mdc-chip__primary-action"> | ||||||
|  |               <span class="mdc-chip__text" | ||||||
|  |                 >${this.hass.localize( | ||||||
|  |                   "ui.components.target-picker.add_area_id" | ||||||
|  |                 )}</span | ||||||
|  |               > | ||||||
|  |             </span> | ||||||
|  |           </span> | ||||||
|  |         </div> | ||||||
|  |         <div | ||||||
|  |           class="mdc-chip device_id add" | ||||||
|  |           .type=${"device_id"} | ||||||
|  |           @click=${this._showPicker} | ||||||
|  |         > | ||||||
|  |           <div class="mdc-chip__ripple"></div> | ||||||
|  |           <ha-svg-icon | ||||||
|  |             class="mdc-chip__icon mdc-chip__icon--leading" | ||||||
|  |             .path=${mdiPlus} | ||||||
|  |           ></ha-svg-icon> | ||||||
|  |           <span role="gridcell"> | ||||||
|  |             <span role="button" tabindex="0" class="mdc-chip__primary-action"> | ||||||
|  |               <span class="mdc-chip__text" | ||||||
|  |                 >${this.hass.localize( | ||||||
|  |                   "ui.components.target-picker.add_device_id" | ||||||
|  |                 )}</span | ||||||
|  |               > | ||||||
|  |             </span> | ||||||
|  |           </span> | ||||||
|  |         </div> | ||||||
|  |         <div | ||||||
|  |           class="mdc-chip entity_id add" | ||||||
|  |           .type=${"entity_id"} | ||||||
|  |           @click=${this._showPicker} | ||||||
|  |         > | ||||||
|  |           <div class="mdc-chip__ripple"></div> | ||||||
|  |           <ha-svg-icon | ||||||
|  |             class="mdc-chip__icon mdc-chip__icon--leading" | ||||||
|  |             .path=${mdiPlus} | ||||||
|  |           ></ha-svg-icon> | ||||||
|  |           <span role="gridcell"> | ||||||
|  |             <span role="button" tabindex="0" class="mdc-chip__primary-action"> | ||||||
|  |               <span class="mdc-chip__text" | ||||||
|  |                 >${this.hass.localize( | ||||||
|  |                   "ui.components.target-picker.add_entity_id" | ||||||
|  |                 )}</span | ||||||
|  |               > | ||||||
|  |             </span> | ||||||
|  |           </span> | ||||||
|  |         </div> | ||||||
|  |       </div>`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _showPicker(ev) { | ||||||
|  |     this._addMode = ev.currentTarget.type; | ||||||
|  |     await this.updateComplete; | ||||||
|  |     setTimeout(() => { | ||||||
|  |       this._inputElement?.open(); | ||||||
|  |       this._inputElement?.focus(); | ||||||
|  |     }, 0); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _renderChip( | ||||||
|  |     type: string, | ||||||
|  |     id: string, | ||||||
|  |     name: string, | ||||||
|  |     icon?: string, | ||||||
|  |     iconPath?: string | ||||||
|  |   ) { | ||||||
|  |     return html` | ||||||
|  |       <div | ||||||
|  |         class="mdc-chip ${classMap({ | ||||||
|  |           [type]: true, | ||||||
|  |         })}" | ||||||
|  |       > | ||||||
|  |         ${iconPath | ||||||
|  |           ? html`<ha-svg-icon | ||||||
|  |               class="mdc-chip__icon mdc-chip__icon--leading" | ||||||
|  |               .path=${iconPath} | ||||||
|  |             ></ha-svg-icon>` | ||||||
|  |           : ""} | ||||||
|  |         ${icon | ||||||
|  |           ? html`<ha-icon | ||||||
|  |               class="mdc-chip__icon mdc-chip__icon--leading" | ||||||
|  |               .icon=${icon} | ||||||
|  |             ></ha-icon>` | ||||||
|  |           : ""} | ||||||
|  |         <span role="gridcell"> | ||||||
|  |           <span role="button" tabindex="0" class="mdc-chip__primary-action"> | ||||||
|  |             <span class="mdc-chip__text">${name}</span> | ||||||
|  |           </span> | ||||||
|  |         </span> | ||||||
|  |         ${type === "entity_id" | ||||||
|  |           ? "" | ||||||
|  |           : html` <span role="gridcell"> | ||||||
|  |               <mwc-icon-button | ||||||
|  |                 class="expand-btn mdc-chip__icon mdc-chip__icon--trailing" | ||||||
|  |                 tabindex="-1" | ||||||
|  |                 role="button" | ||||||
|  |                 .label=${"Expand"} | ||||||
|  |                 .id=${id} | ||||||
|  |                 .type=${type} | ||||||
|  |                 @click=${this._handleExpand} | ||||||
|  |               > | ||||||
|  |                 <ha-svg-icon .path=${mdiUnfoldMoreVertical}></ha-svg-icon> | ||||||
|  |               </mwc-icon-button> | ||||||
|  |               <paper-tooltip class="expand" animation-delay="0" | ||||||
|  |                 >${this.hass.localize( | ||||||
|  |                   `ui.components.target-picker.expand_${type}` | ||||||
|  |                 )}</paper-tooltip | ||||||
|  |               > | ||||||
|  |             </span>`} | ||||||
|  |         <span role="gridcell"> | ||||||
|  |           <mwc-icon-button | ||||||
|  |             class="mdc-chip__icon mdc-chip__icon--trailing" | ||||||
|  |             tabindex="-1" | ||||||
|  |             role="button" | ||||||
|  |             .label=${"Remove"} | ||||||
|  |             .id=${id} | ||||||
|  |             .type=${type} | ||||||
|  |             @click=${this._handleRemove} | ||||||
|  |           > | ||||||
|  |             <ha-svg-icon .path=${mdiClose}></ha-svg-icon> | ||||||
|  |           </mwc-icon-button> | ||||||
|  |           <paper-tooltip animation-delay="0" | ||||||
|  |             >${this.hass.localize( | ||||||
|  |               `ui.components.target-picker.remove_${type}` | ||||||
|  |             )}</paper-tooltip | ||||||
|  |           > | ||||||
|  |         </span> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _renderPicker() { | ||||||
|  |     switch (this._addMode) { | ||||||
|  |       case "area_id": | ||||||
|  |         return html`<ha-area-picker | ||||||
|  |           .hass=${this.hass} | ||||||
|  |           id="input" | ||||||
|  |           .type=${"area_id"} | ||||||
|  |           .label=${this.hass.localize( | ||||||
|  |             "ui.components.target-picker.add_area_id" | ||||||
|  |           )} | ||||||
|  |           no-add | ||||||
|  |           .deviceFilter=${this.deviceFilter} | ||||||
|  |           .entityFilter=${this.entityRegFilter} | ||||||
|  |           .includeDeviceClasses=${this.includeDeviceClasses} | ||||||
|  |           .includeDomains=${this.includeDomains} | ||||||
|  |           @value-changed=${this._targetPicked} | ||||||
|  |         ></ha-area-picker>`; | ||||||
|  |       case "device_id": | ||||||
|  |         return html`<ha-device-picker | ||||||
|  |           .hass=${this.hass} | ||||||
|  |           id="input" | ||||||
|  |           .type=${"device_id"} | ||||||
|  |           .label=${this.hass.localize( | ||||||
|  |             "ui.components.target-picker.add_device_id" | ||||||
|  |           )} | ||||||
|  |           .deviceFilter=${this.deviceFilter} | ||||||
|  |           .entityFilter=${this.entityRegFilter} | ||||||
|  |           .includeDeviceClasses=${this.includeDeviceClasses} | ||||||
|  |           .includeDomains=${this.includeDomains} | ||||||
|  |           @value-changed=${this._targetPicked} | ||||||
|  |         ></ha-device-picker>`; | ||||||
|  |       case "entity_id": | ||||||
|  |         return html`<ha-entity-picker | ||||||
|  |           .hass=${this.hass} | ||||||
|  |           id="input" | ||||||
|  |           .type=${"entity_id"} | ||||||
|  |           .label=${this.hass.localize( | ||||||
|  |             "ui.components.target-picker.add_entity_id" | ||||||
|  |           )} | ||||||
|  |           .entityFilter=${this.entityFilter} | ||||||
|  |           .includeDeviceClasses=${this.includeDeviceClasses} | ||||||
|  |           .includeDomains=${this.includeDomains} | ||||||
|  |           @value-changed=${this._targetPicked} | ||||||
|  |         ></ha-entity-picker>`; | ||||||
|  |     } | ||||||
|  |     return html``; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _targetPicked(ev) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     if (!ev.detail.value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     const value = ev.detail.value; | ||||||
|  |     const target = ev.currentTarget; | ||||||
|  |     target.value = ""; | ||||||
|  |     this._addMode = undefined; | ||||||
|  |     fireEvent(this, "value-changed", { | ||||||
|  |       value: this.value | ||||||
|  |         ? { | ||||||
|  |             ...this.value, | ||||||
|  |             [target.type]: this.value[target.type] | ||||||
|  |               ? [...ensureArray(this.value[target.type]), value] | ||||||
|  |               : value, | ||||||
|  |           } | ||||||
|  |         : { [target.type]: value }, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _handleExpand(ev) { | ||||||
|  |     const target = ev.currentTarget as any; | ||||||
|  |     const newDevices: string[] = []; | ||||||
|  |     const newEntities: string[] = []; | ||||||
|  |     if (target.type === "area_id") { | ||||||
|  |       Object.values(this._devices!).forEach((device) => { | ||||||
|  |         if ( | ||||||
|  |           device.area_id === target.id && | ||||||
|  |           !this.value!.device_id?.includes(device.id) && | ||||||
|  |           this._deviceMeetsFilter(device) | ||||||
|  |         ) { | ||||||
|  |           newDevices.push(device.id); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |       this._entities!.forEach((entity) => { | ||||||
|  |         if ( | ||||||
|  |           entity.area_id === target.id && | ||||||
|  |           !this.value!.entity_id?.includes(entity.entity_id) && | ||||||
|  |           this._entityRegMeetsFilter(entity) | ||||||
|  |         ) { | ||||||
|  |           newEntities.push(entity.entity_id); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } else if (target.type === "device_id") { | ||||||
|  |       this._entities!.forEach((entity) => { | ||||||
|  |         if ( | ||||||
|  |           entity.device_id === target.id && | ||||||
|  |           !this.value!.entity_id?.includes(entity.entity_id) && | ||||||
|  |           this._entityRegMeetsFilter(entity) | ||||||
|  |         ) { | ||||||
|  |           newEntities.push(entity.entity_id); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     let value = this.value; | ||||||
|  |     if (newEntities.length) { | ||||||
|  |       value = this._addItems(value, "entity_id", newEntities); | ||||||
|  |     } | ||||||
|  |     if (newDevices.length) { | ||||||
|  |       value = this._addItems(value, "device_id", newDevices); | ||||||
|  |     } | ||||||
|  |     value = this._removeItem(value, target.type, target.id); | ||||||
|  |     fireEvent(this, "value-changed", { value }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _handleRemove(ev) { | ||||||
|  |     const target = ev.currentTarget as any; | ||||||
|  |     fireEvent(this, "value-changed", { | ||||||
|  |       value: this._removeItem(this.value, target.type, target.id), | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _addItems( | ||||||
|  |     value: this["value"], | ||||||
|  |     type: string, | ||||||
|  |     ids: string[] | ||||||
|  |   ): this["value"] { | ||||||
|  |     return { | ||||||
|  |       ...value, | ||||||
|  |       [type]: value![type] ? ensureArray(value![type])!.concat(ids) : ids, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _removeItem( | ||||||
|  |     value: this["value"], | ||||||
|  |     type: string, | ||||||
|  |     id: string | ||||||
|  |   ): this["value"] { | ||||||
|  |     const newVal = ensureArray(value![type])!.filter((val) => val !== id); | ||||||
|  |     if (newVal.length) { | ||||||
|  |       return { | ||||||
|  |         ...value, | ||||||
|  |         [type]: newVal, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     const val = { ...value }!; | ||||||
|  |     delete val[type]; | ||||||
|  |     if (Object.keys(val).length) { | ||||||
|  |       return val; | ||||||
|  |     } | ||||||
|  |     return undefined; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _deviceMeetsFilter(device: DeviceRegistryEntry): boolean { | ||||||
|  |     const devEntities = this._entities?.filter( | ||||||
|  |       (entity) => entity.device_id === device.id | ||||||
|  |     ); | ||||||
|  |     if (this.includeDomains) { | ||||||
|  |       if (!devEntities || !devEntities.length) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       if ( | ||||||
|  |         !devEntities.some((entity) => | ||||||
|  |           this.includeDomains!.includes(computeDomain(entity.entity_id)) | ||||||
|  |         ) | ||||||
|  |       ) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (this.includeDeviceClasses) { | ||||||
|  |       if (!devEntities || !devEntities.length) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       if ( | ||||||
|  |         !devEntities.some((entity) => { | ||||||
|  |           const stateObj = this.hass.states[entity.entity_id]; | ||||||
|  |           if (!stateObj) { | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|  |           return ( | ||||||
|  |             stateObj.attributes.device_class && | ||||||
|  |             this.includeDeviceClasses!.includes( | ||||||
|  |               stateObj.attributes.device_class | ||||||
|  |             ) | ||||||
|  |           ); | ||||||
|  |         }) | ||||||
|  |       ) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (this.deviceFilter) { | ||||||
|  |       return this.deviceFilter(device); | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _entityRegMeetsFilter(entity: EntityRegistryEntry): boolean { | ||||||
|  |     if ( | ||||||
|  |       this.includeDomains && | ||||||
|  |       !this.includeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |     ) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if (this.includeDeviceClasses) { | ||||||
|  |       const stateObj = this.hass.states[entity.entity_id]; | ||||||
|  |       if (!stateObj) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       if ( | ||||||
|  |         !stateObj.attributes.device_class || | ||||||
|  |         !this.includeDeviceClasses!.includes(stateObj.attributes.device_class) | ||||||
|  |       ) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (this.entityRegFilter) { | ||||||
|  |       return this.entityRegFilter(entity); | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static get styles(): CSSResult { | ||||||
|  |     return css` | ||||||
|  |       ${unsafeCSS(chipStyles)} | ||||||
|  |       .mdc-chip { | ||||||
|  |         color: var(--primary-text-color); | ||||||
|  |       } | ||||||
|  |       .items { | ||||||
|  |         z-index: 2; | ||||||
|  |       } | ||||||
|  |       .mdc-chip.add { | ||||||
|  |         color: rgba(0, 0, 0, 0.87); | ||||||
|  |       } | ||||||
|  |       .mdc-chip:not(.add) { | ||||||
|  |         cursor: default; | ||||||
|  |       } | ||||||
|  |       .mdc-chip mwc-icon-button { | ||||||
|  |         --mdc-icon-button-size: 24px; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         outline: none; | ||||||
|  |       } | ||||||
|  |       .mdc-chip mwc-icon-button ha-svg-icon { | ||||||
|  |         border-radius: 50%; | ||||||
|  |         background: var(--secondary-text-color); | ||||||
|  |       } | ||||||
|  |       .mdc-chip__icon.mdc-chip__icon--trailing { | ||||||
|  |         width: 16px; | ||||||
|  |         height: 16px; | ||||||
|  |         --mdc-icon-size: 14px; | ||||||
|  |         color: var(--card-background-color); | ||||||
|  |       } | ||||||
|  |       .mdc-chip__icon--leading { | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         justify-content: center; | ||||||
|  |         --mdc-icon-size: 20px; | ||||||
|  |         border-radius: 50%; | ||||||
|  |         padding: 6px; | ||||||
|  |         margin-left: -14px !important; | ||||||
|  |       } | ||||||
|  |       .expand-btn { | ||||||
|  |         margin-right: 0; | ||||||
|  |       } | ||||||
|  |       .mdc-chip.area_id:not(.add) { | ||||||
|  |         border: 2px solid #fed6a4; | ||||||
|  |         background: var(--card-background-color); | ||||||
|  |       } | ||||||
|  |       .mdc-chip.area_id:not(.add) .mdc-chip__icon--leading, | ||||||
|  |       .mdc-chip.area_id.add { | ||||||
|  |         background: #fed6a4; | ||||||
|  |       } | ||||||
|  |       .mdc-chip.device_id:not(.add) { | ||||||
|  |         border: 2px solid #a8e1fb; | ||||||
|  |         background: var(--card-background-color); | ||||||
|  |       } | ||||||
|  |       .mdc-chip.device_id:not(.add) .mdc-chip__icon--leading, | ||||||
|  |       .mdc-chip.device_id.add { | ||||||
|  |         background: #a8e1fb; | ||||||
|  |       } | ||||||
|  |       .mdc-chip.entity_id:not(.add) { | ||||||
|  |         border: 2px solid #d2e7b9; | ||||||
|  |         background: var(--card-background-color); | ||||||
|  |       } | ||||||
|  |       .mdc-chip.entity_id:not(.add) .mdc-chip__icon--leading, | ||||||
|  |       .mdc-chip.entity_id.add { | ||||||
|  |         background: #d2e7b9; | ||||||
|  |       } | ||||||
|  |       .mdc-chip:hover { | ||||||
|  |         z-index: 5; | ||||||
|  |       } | ||||||
|  |       paper-tooltip.expand { | ||||||
|  |         min-width: 200px; | ||||||
|  |       } | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-target-picker": HaTargetPicker; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| import "@material/mwc-button/mwc-button"; | import "@material/mwc-button/mwc-button"; | ||||||
| import "@material/mwc-fab/mwc-fab"; | import "../ha-fab"; | ||||||
| import "@material/mwc-list/mwc-list"; | import "@material/mwc-list/mwc-list"; | ||||||
| import "@material/mwc-list/mwc-list-item"; | import "@material/mwc-list/mwc-list-item"; | ||||||
| import { mdiArrowLeft, mdiClose, mdiPlay, mdiPlus } from "@mdi/js"; | import { mdiArrowLeft, mdiClose, mdiPlay, mdiPlus } from "@mdi/js"; | ||||||
| @@ -170,7 +170,7 @@ export class HaMediaPlayerBrowse extends LitElement { | |||||||
|                 > |                 > | ||||||
|                   ${this._narrow && currentItem?.can_play |                   ${this._narrow && currentItem?.can_play | ||||||
|                     ? html` |                     ? html` | ||||||
|                         <mwc-fab |                         <ha-fab | ||||||
|                           mini |                           mini | ||||||
|                           .item=${currentItem} |                           .item=${currentItem} | ||||||
|                           @click=${this._actionClicked} |                           @click=${this._actionClicked} | ||||||
| @@ -185,7 +185,7 @@ export class HaMediaPlayerBrowse extends LitElement { | |||||||
|                           ${this.hass.localize( |                           ${this.hass.localize( | ||||||
|                             `ui.components.media-browser.${this.action}` |                             `ui.components.media-browser.${this.action}` | ||||||
|                           )} |                           )} | ||||||
|                         </mwc-fab> |                         </ha-fab> | ||||||
|                       ` |                       ` | ||||||
|                     : ""} |                     : ""} | ||||||
|                 </div> |                 </div> | ||||||
| @@ -927,7 +927,7 @@ export class HaMediaPlayerBrowse extends LitElement { | |||||||
|           transition: width 0.4s, height 0.4s, padding-bottom 0.4s; |           transition: width 0.4s, height 0.4s, padding-bottom 0.4s; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         mwc-fab { |         ha-fab { | ||||||
|           position: absolute; |           position: absolute; | ||||||
|           --mdc-theme-secondary: var(--primary-color); |           --mdc-theme-secondary: var(--primary-color); | ||||||
|           bottom: -20px; |           bottom: -20px; | ||||||
| @@ -1011,7 +1011,7 @@ export class HaMediaPlayerBrowse extends LitElement { | |||||||
|           margin-bottom: 0; |           margin-bottom: 0; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         :host([scroll]) mwc-fab { |         :host([scroll]) ha-fab { | ||||||
|           bottom: 4px; |           bottom: 4px; | ||||||
|           right: 4px; |           right: 4px; | ||||||
|           --mdc-fab-box-shadow: none; |           --mdc-fab-box-shadow: none; | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import { navigate } from "../common/navigate"; | |||||||
| import { Context, HomeAssistant } from "../types"; | import { Context, HomeAssistant } from "../types"; | ||||||
| import { BlueprintInput } from "./blueprint"; | import { BlueprintInput } from "./blueprint"; | ||||||
| import { DeviceCondition, DeviceTrigger } from "./device_automation"; | import { DeviceCondition, DeviceTrigger } from "./device_automation"; | ||||||
| import { Action } from "./script"; | import { Action, MODES } from "./script"; | ||||||
|  |  | ||||||
| export interface AutomationEntity extends HassEntityBase { | export interface AutomationEntity extends HassEntityBase { | ||||||
|   attributes: HassEntityAttributeBase & { |   attributes: HassEntityAttributeBase & { | ||||||
| @@ -26,7 +26,7 @@ export interface ManualAutomationConfig { | |||||||
|   trigger: Trigger[]; |   trigger: Trigger[]; | ||||||
|   condition?: Condition[]; |   condition?: Condition[]; | ||||||
|   action: Action[]; |   action: Action[]; | ||||||
|   mode?: "single" | "restart" | "queued" | "parallel"; |   mode?: typeof MODES[number]; | ||||||
|   max?: number; |   max?: number; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ export interface DeviceRegistryEntry { | |||||||
|   area_id?: string; |   area_id?: string; | ||||||
|   name_by_user?: string; |   name_by_user?: string; | ||||||
|   entry_type: "service" | null; |   entry_type: "service" | null; | ||||||
|  |   disabled_by: string | null; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface DeviceEntityLookup { | export interface DeviceEntityLookup { | ||||||
| @@ -26,6 +27,7 @@ export interface DeviceEntityLookup { | |||||||
| export interface DeviceRegistryEntryMutableParams { | export interface DeviceRegistryEntryMutableParams { | ||||||
|   area_id?: string | null; |   area_id?: string | null; | ||||||
|   name_by_user?: string | null; |   name_by_user?: string | null; | ||||||
|  |   disabled_by?: string | null; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const fallbackDeviceName = ( | export const fallbackDeviceName = ( | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ export interface EntityRegistryEntry { | |||||||
|   platform: string; |   platform: string; | ||||||
|   config_entry_id?: string; |   config_entry_id?: string; | ||||||
|   device_id?: string; |   device_id?: string; | ||||||
|  |   area_id?: string; | ||||||
|   disabled_by: string | null; |   disabled_by: string | null; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -29,6 +30,7 @@ export interface UpdateEntityRegistryEntryResult { | |||||||
| export interface EntityRegistryEntryUpdateParams { | export interface EntityRegistryEntryUpdateParams { | ||||||
|   name?: string | null; |   name?: string | null; | ||||||
|   icon?: string | null; |   icon?: string | null; | ||||||
|  |   area_id?: string | null; | ||||||
|   disabled_by?: string | null; |   disabled_by?: string | null; | ||||||
|   new_entity_id?: string; |   new_entity_id?: string; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,13 +7,13 @@ import { navigate } from "../common/navigate"; | |||||||
| import { HomeAssistant } from "../types"; | import { HomeAssistant } from "../types"; | ||||||
| import { Condition, Trigger } from "./automation"; | import { Condition, Trigger } from "./automation"; | ||||||
|  |  | ||||||
| export const MODES = ["single", "restart", "queued", "parallel"]; | export const MODES = ["single", "restart", "queued", "parallel"] as const; | ||||||
| export const MODES_MAX = ["queued", "parallel"]; | export const MODES_MAX = ["queued", "parallel"]; | ||||||
|  |  | ||||||
| export interface ScriptEntity extends HassEntityBase { | export interface ScriptEntity extends HassEntityBase { | ||||||
|   attributes: HassEntityAttributeBase & { |   attributes: HassEntityAttributeBase & { | ||||||
|     last_triggered: string; |     last_triggered: string; | ||||||
|     mode: "single" | "restart" | "queued" | "parallel"; |     mode: typeof MODES[number]; | ||||||
|     current?: number; |     current?: number; | ||||||
|     max?: number; |     max?: number; | ||||||
|   }; |   }; | ||||||
| @@ -23,7 +23,7 @@ export interface ScriptConfig { | |||||||
|   alias: string; |   alias: string; | ||||||
|   sequence: Action[]; |   sequence: Action[]; | ||||||
|   icon?: string; |   icon?: string; | ||||||
|   mode?: "single" | "restart" | "queued" | "parallel"; |   mode?: typeof MODES[number]; | ||||||
|   max?: number; |   max?: number; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,9 +2,11 @@ export type Selector = | |||||||
|   | EntitySelector |   | EntitySelector | ||||||
|   | DeviceSelector |   | DeviceSelector | ||||||
|   | AreaSelector |   | AreaSelector | ||||||
|  |   | TargetSelector | ||||||
|   | NumberSelector |   | NumberSelector | ||||||
|   | BooleanSelector |   | BooleanSelector | ||||||
|   | TimeSelector; |   | TimeSelector | ||||||
|  |   | ActionSelector; | ||||||
|  |  | ||||||
| export interface EntitySelector { | export interface EntitySelector { | ||||||
|   entity: { |   entity: { | ||||||
| @@ -19,13 +21,41 @@ export interface DeviceSelector { | |||||||
|     integration?: string; |     integration?: string; | ||||||
|     manufacturer?: string; |     manufacturer?: string; | ||||||
|     model?: string; |     model?: string; | ||||||
|     entity?: EntitySelector["entity"]; |     entity?: { | ||||||
|  |       domain?: EntitySelector["entity"]["domain"]; | ||||||
|  |       device_class?: EntitySelector["entity"]["device_class"]; | ||||||
|  |     }; | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface AreaSelector { | export interface AreaSelector { | ||||||
|   // eslint-disable-next-line @typescript-eslint/ban-types |   area: { | ||||||
|   area: {}; |     entity?: { | ||||||
|  |       integration?: EntitySelector["entity"]["integration"]; | ||||||
|  |       domain?: EntitySelector["entity"]["domain"]; | ||||||
|  |       device_class?: EntitySelector["entity"]["device_class"]; | ||||||
|  |     }; | ||||||
|  |     device?: { | ||||||
|  |       integration?: DeviceSelector["device"]["integration"]; | ||||||
|  |       manufacturer?: DeviceSelector["device"]["manufacturer"]; | ||||||
|  |       model?: DeviceSelector["device"]["model"]; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface TargetSelector { | ||||||
|  |   target: { | ||||||
|  |     entity?: { | ||||||
|  |       integration?: EntitySelector["entity"]["integration"]; | ||||||
|  |       domain?: EntitySelector["entity"]["domain"]; | ||||||
|  |       device_class?: EntitySelector["entity"]["device_class"]; | ||||||
|  |     }; | ||||||
|  |     device?: { | ||||||
|  |       integration?: DeviceSelector["device"]["integration"]; | ||||||
|  |       manufacturer?: DeviceSelector["device"]["manufacturer"]; | ||||||
|  |       model?: DeviceSelector["device"]["model"]; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface NumberSelector { | export interface NumberSelector { | ||||||
| @@ -47,3 +77,8 @@ export interface TimeSelector { | |||||||
|   // eslint-disable-next-line @typescript-eslint/ban-types |   // eslint-disable-next-line @typescript-eslint/ban-types | ||||||
|   time: {}; |   time: {}; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export interface ActionSelector { | ||||||
|  |   // eslint-disable-next-line @typescript-eslint/ban-types | ||||||
|  |   action: {}; | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								src/data/target.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/data/target.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | export interface Target { | ||||||
|  |   entity_id?: string[]; | ||||||
|  |   device_id?: string[]; | ||||||
|  |   area_id?: string[]; | ||||||
|  | } | ||||||
| @@ -9,6 +9,7 @@ export const GROUPS = [SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_ADMIN]; | |||||||
|  |  | ||||||
| export interface User { | export interface User { | ||||||
|   id: string; |   id: string; | ||||||
|  |   username: string | null; | ||||||
|   name: string; |   name: string; | ||||||
|   is_owner: boolean; |   is_owner: boolean; | ||||||
|   is_active: boolean; |   is_active: boolean; | ||||||
| @@ -19,6 +20,7 @@ export interface User { | |||||||
|  |  | ||||||
| export interface UpdateUserParams { | export interface UpdateUserParams { | ||||||
|   name?: User["name"]; |   name?: User["name"]; | ||||||
|  |   is_active?: User["is_active"]; | ||||||
|   group_ids?: User["group_ids"]; |   group_ids?: User["group_ids"]; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ export interface ZHADevice { | |||||||
|   device_type: string; |   device_type: string; | ||||||
|   signature: any; |   signature: any; | ||||||
|   neighbors: Neighbor[]; |   neighbors: Neighbor[]; | ||||||
|  |   pairing_status?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface Neighbor { | export interface Neighbor { | ||||||
| @@ -270,3 +271,23 @@ export const addGroup = ( | |||||||
|     group_name: groupName, |     group_name: groupName, | ||||||
|     members: membersToAdd, |     members: membersToAdd, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  | export const INITIALIZED = "INITIALIZED"; | ||||||
|  | export const INTERVIEW_COMPLETE = "INTERVIEW_COMPLETE"; | ||||||
|  | export const CONFIGURED = "CONFIGURED"; | ||||||
|  | export const PAIRED = "PAIRED"; | ||||||
|  | export const INCOMPLETE_PAIRING_STATUSES = [ | ||||||
|  |   PAIRED, | ||||||
|  |   CONFIGURED, | ||||||
|  |   INTERVIEW_COMPLETE, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export const DEVICE_JOINED = "device_joined"; | ||||||
|  | export const RAW_DEVICE_INITIALIZED = "raw_device_initialized"; | ||||||
|  | export const DEVICE_FULLY_INITIALIZED = "device_fully_initialized"; | ||||||
|  | export const DEVICE_MESSAGE_TYPES = [ | ||||||
|  |   DEVICE_JOINED, | ||||||
|  |   RAW_DEVICE_INITIALIZED, | ||||||
|  |   DEVICE_FULLY_INITIALIZED, | ||||||
|  | ]; | ||||||
|  | export const LOG_OUTPUT = "log_output"; | ||||||
|   | |||||||
| @@ -17,17 +17,17 @@ import "../../components/ha-switch"; | |||||||
| import { PolymerChangedEvent } from "../../polymer-types"; | import { PolymerChangedEvent } from "../../polymer-types"; | ||||||
| import { haStyleDialog } from "../../resources/styles"; | import { haStyleDialog } from "../../resources/styles"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
| import { DialogParams } from "./show-dialog-box"; | import { DialogBoxParams } from "./show-dialog-box"; | ||||||
|  |  | ||||||
| @customElement("dialog-box") | @customElement("dialog-box") | ||||||
| class DialogBox extends LitElement { | class DialogBox extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|   @internalProperty() private _params?: DialogParams; |   @internalProperty() private _params?: DialogBoxParams; | ||||||
|  |  | ||||||
|   @internalProperty() private _value?: string; |   @internalProperty() private _value?: string; | ||||||
|  |  | ||||||
|   public async showDialog(params: DialogParams): Promise<void> { |   public async showDialog(params: DialogBoxParams): Promise<void> { | ||||||
|     this._params = params; |     this._params = params; | ||||||
|     if (params.prompt) { |     if (params.prompt) { | ||||||
|       this._value = params.defaultValue; |       this._value = params.defaultValue; | ||||||
| @@ -55,8 +55,8 @@ class DialogBox extends LitElement { | |||||||
|     return html` |     return html` | ||||||
|       <ha-dialog |       <ha-dialog | ||||||
|         open |         open | ||||||
|         ?scrimClickAction=${this._params.prompt} |         ?scrimClickAction=${confirmPrompt} | ||||||
|         ?escapeKeyAction=${this._params.prompt} |         ?escapeKeyAction=${confirmPrompt} | ||||||
|         @closed=${this._dialogClosed} |         @closed=${this._dialogClosed} | ||||||
|         defaultAction="ignore" |         defaultAction="ignore" | ||||||
|         .heading=${this._params.title |         .heading=${this._params.title | ||||||
| @@ -140,10 +140,10 @@ class DialogBox extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _dialogClosed(ev) { |   private _dialogClosed(ev) { | ||||||
|     if (ev.detail.action === "ignore") { |     if (this._params?.prompt && ev.detail.action === "ignore") { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     this.closeDialog(); |     this._dismiss(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _close(): void { |   private _close(): void { | ||||||
|   | |||||||
| @@ -1,31 +1,31 @@ | |||||||
| import { TemplateResult } from "lit-html"; | import { TemplateResult } from "lit-html"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
|  |  | ||||||
| interface BaseDialogParams { | interface BaseDialogBoxParams { | ||||||
|   confirmText?: string; |   confirmText?: string; | ||||||
|   text?: string | TemplateResult; |   text?: string | TemplateResult; | ||||||
|   title?: string; |   title?: string; | ||||||
|   warning?: boolean; |   warning?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface AlertDialogParams extends BaseDialogParams { | export interface AlertDialogParams extends BaseDialogBoxParams { | ||||||
|   confirm?: () => void; |   confirm?: () => void; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface ConfirmationDialogParams extends BaseDialogParams { | export interface ConfirmationDialogParams extends BaseDialogBoxParams { | ||||||
|   dismissText?: string; |   dismissText?: string; | ||||||
|   confirm?: () => void; |   confirm?: () => void; | ||||||
|   cancel?: () => void; |   cancel?: () => void; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface PromptDialogParams extends BaseDialogParams { | export interface PromptDialogParams extends BaseDialogBoxParams { | ||||||
|   inputLabel?: string; |   inputLabel?: string; | ||||||
|   inputType?: string; |   inputType?: string; | ||||||
|   defaultValue?: string; |   defaultValue?: string; | ||||||
|   confirm?: (out?: string) => void; |   confirm?: (out?: string) => void; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface DialogParams | export interface DialogBoxParams | ||||||
|   extends ConfirmationDialogParams, |   extends ConfirmationDialogParams, | ||||||
|     PromptDialogParams { |     PromptDialogParams { | ||||||
|   confirm?: (out?: string) => void; |   confirm?: (out?: string) => void; | ||||||
| @@ -37,10 +37,10 @@ export const loadGenericDialog = () => import("./dialog-box"); | |||||||
|  |  | ||||||
| const showDialogHelper = ( | const showDialogHelper = ( | ||||||
|   element: HTMLElement, |   element: HTMLElement, | ||||||
|   dialogParams: DialogParams, |   dialogParams: DialogBoxParams, | ||||||
|   extra?: { |   extra?: { | ||||||
|     confirmation?: DialogParams["confirmation"]; |     confirmation?: DialogBoxParams["confirmation"]; | ||||||
|     prompt?: DialogParams["prompt"]; |     prompt?: DialogBoxParams["prompt"]; | ||||||
|   } |   } | ||||||
| ) => | ) => | ||||||
|   new Promise((resolve) => { |   new Promise((resolve) => { | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ class MoreInfoSun extends LitElement { | |||||||
|               > |               > | ||||||
|               <ha-relative-time |               <ha-relative-time | ||||||
|                 .hass=${this.hass} |                 .hass=${this.hass} | ||||||
|                 .datetimeObj=${item === "ris" ? risingDate : settingDate} |                 .datetime=${item === "ris" ? risingDate : settingDate} | ||||||
|               ></ha-relative-time> |               ></ha-relative-time> | ||||||
|             </div> |             </div> | ||||||
|             <div class="value"> |             <div class="value"> | ||||||
|   | |||||||
| @@ -46,7 +46,7 @@ export class HuiNotificationItemTemplate extends LitElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       .actions { |       .actions { | ||||||
|         border-top: 1px solid #e8e8e8; |         border-top: 1px solid var(--divider-color, #e8e8e8); | ||||||
|         padding: 5px 16px; |         padding: 5px 16px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ | |||||||
|       #ha-init-skeleton::before { |       #ha-init-skeleton::before { | ||||||
|         display: block; |         display: block; | ||||||
|         content: ""; |         content: ""; | ||||||
|         height: 112px; |         height: 56px; | ||||||
|         background-color: #THEMEC; |         background-color: #THEMEC; | ||||||
|       } |       } | ||||||
|       html { |       html { | ||||||
|   | |||||||
| @@ -60,6 +60,12 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|    */ |    */ | ||||||
|   @property({ type: Boolean }) public hasFab = false; |   @property({ type: Boolean }) public hasFab = false; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Add an extra rows at the bottom of the datatabel | ||||||
|  |    * @type {TemplateResult} | ||||||
|  |    */ | ||||||
|  |   @property({ attribute: false }) public appendRow?; | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Field with a unique id per entry in data. |    * Field with a unique id per entry in data. | ||||||
|    * @type {String} |    * @type {String} | ||||||
| @@ -171,6 +177,7 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|           .noDataText=${this.noDataText} |           .noDataText=${this.noDataText} | ||||||
|           .dir=${computeRTLDirection(this.hass)} |           .dir=${computeRTLDirection(this.hass)} | ||||||
|           .clickable=${this.clickable} |           .clickable=${this.clickable} | ||||||
|  |           .appendRow=${this.appendRow} | ||||||
|         > |         > | ||||||
|           ${!this.narrow |           ${!this.narrow | ||||||
|             ? html` |             ? html` | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ import { PolymerChangedEvent } from "../../../polymer-types"; | |||||||
| import { haStyleDialog } from "../../../resources/styles"; | import { haStyleDialog } from "../../../resources/styles"; | ||||||
| import { HomeAssistant } from "../../../types"; | import { HomeAssistant } from "../../../types"; | ||||||
| import { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail"; | import { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail"; | ||||||
|  | import { navigate } from "../../../common/navigate"; | ||||||
|  |  | ||||||
| class DialogAreaDetail extends LitElement { | class DialogAreaDetail extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -154,6 +155,8 @@ class DialogAreaDetail extends LitElement { | |||||||
|     } finally { |     } finally { | ||||||
|       this._submitting = false; |       this._submitting = false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     navigate(this, "/config/areas/dashboard"); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResult[] { |   static get styles(): CSSResult[] { | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "@material/mwc-fab"; | import "../../../components/ha-fab"; | ||||||
| import { mdiPlus } from "@mdi/js"; | import { mdiPlus } from "@mdi/js"; | ||||||
| import "@polymer/paper-item/paper-item"; | import "@polymer/paper-item/paper-item"; | ||||||
| import "@polymer/paper-item/paper-item-body"; | import "@polymer/paper-item/paper-item-body"; | ||||||
| @@ -124,7 +124,7 @@ export class HaConfigAreasDashboard extends LitElement { | |||||||
|           icon="hass:help-circle" |           icon="hass:help-circle" | ||||||
|           @click=${this._showHelp} |           @click=${this._showHelp} | ||||||
|         ></ha-icon-button> |         ></ha-icon-button> | ||||||
|         <mwc-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           .label=${this.hass.localize( |           .label=${this.hass.localize( | ||||||
|             "ui.panel.config.areas.picker.create_area" |             "ui.panel.config.areas.picker.create_area" | ||||||
| @@ -133,7 +133,7 @@ export class HaConfigAreasDashboard extends LitElement { | |||||||
|           @click=${this._createArea} |           @click=${this._createArea} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|         </mwc-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage-data-table> |       </hass-tabs-subpage-data-table> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ export class HaWaitForTriggerAction extends LitElement | |||||||
|         )} |         )} | ||||||
|       > |       > | ||||||
|         <ha-switch |         <ha-switch | ||||||
|           .checked=${continue_on_timeout} |           .checked=${continue_on_timeout ?? true} | ||||||
|           @change=${this._continueChanged} |           @change=${this._continueChanged} | ||||||
|         ></ha-switch> |         ></ha-switch> | ||||||
|       </ha-formfield> |       </ha-formfield> | ||||||
|   | |||||||
| @@ -18,13 +18,9 @@ import "@polymer/paper-input/paper-textarea"; | |||||||
| import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; | import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; | ||||||
| import "../../../components/entity/ha-entity-toggle"; | import "../../../components/entity/ha-entity-toggle"; | ||||||
| import "@material/mwc-button/mwc-button"; | import "@material/mwc-button/mwc-button"; | ||||||
| import "./trigger/ha-automation-trigger"; |  | ||||||
| import "./condition/ha-automation-condition"; |  | ||||||
| import "./action/ha-automation-action"; |  | ||||||
| import { fireEvent } from "../../../common/dom/fire_event"; | import { fireEvent } from "../../../common/dom/fire_event"; | ||||||
| import { haStyle } from "../../../resources/styles"; | import { haStyle } from "../../../resources/styles"; | ||||||
| import { HassEntity } from "home-assistant-js-websocket"; | import { HassEntity } from "home-assistant-js-websocket"; | ||||||
| import { navigate } from "../../../common/navigate"; |  | ||||||
| import { | import { | ||||||
|   BlueprintOrError, |   BlueprintOrError, | ||||||
|   Blueprints, |   Blueprints, | ||||||
| @@ -63,7 +59,7 @@ export class HaBlueprintAutomationEditor extends LitElement { | |||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|     const blueprint = this._blueprint; |     const blueprint = this._blueprint; | ||||||
|     return html`<ha-config-section .isWide=${this.isWide}> |     return html`<ha-config-section vertical .isWide=${this.isWide}> | ||||||
|         ${!this.narrow |         ${!this.narrow | ||||||
|           ? html` <span slot="header">${this.config.alias}</span> ` |           ? html` <span slot="header">${this.config.alias}</span> ` | ||||||
|           : ""} |           : ""} | ||||||
| @@ -119,7 +115,7 @@ export class HaBlueprintAutomationEditor extends LitElement { | |||||||
|         </ha-card> |         </ha-card> | ||||||
|       </ha-config-section> |       </ha-config-section> | ||||||
|  |  | ||||||
|       <ha-config-section .isWide=${this.isWide}> |       <ha-config-section vertical .isWide=${this.isWide}> | ||||||
|         <span slot="header" |         <span slot="header" | ||||||
|           >${this.hass.localize( |           >${this.hass.localize( | ||||||
|             "ui.panel.config.automation.editor.blueprint.header" |             "ui.panel.config.automation.editor.blueprint.header" | ||||||
| @@ -144,11 +140,6 @@ export class HaBlueprintAutomationEditor extends LitElement { | |||||||
|                     "ui.panel.config.automation.editor.blueprint.no_blueprints" |                     "ui.panel.config.automation.editor.blueprint.no_blueprints" | ||||||
|                   ) |                   ) | ||||||
|               : html`<ha-circular-progress active></ha-circular-progress>`} |               : html`<ha-circular-progress active></ha-circular-progress>`} | ||||||
|             <mwc-button @click=${this._navigateBlueprints}> |  | ||||||
|               ${this.hass.localize( |  | ||||||
|                 "ui.panel.config.automation.editor.blueprint.manage_blueprints" |  | ||||||
|               )} |  | ||||||
|             </mwc-button> |  | ||||||
|           </div> |           </div> | ||||||
|  |  | ||||||
|           ${this.config.use_blueprint.path |           ${this.config.use_blueprint.path | ||||||
| @@ -157,41 +148,37 @@ export class HaBlueprintAutomationEditor extends LitElement { | |||||||
|                   There is an error in this Blueprint: ${blueprint.error} |                   There is an error in this Blueprint: ${blueprint.error} | ||||||
|                 </p>` |                 </p>` | ||||||
|               : html`${blueprint?.metadata.description |               : html`${blueprint?.metadata.description | ||||||
|                   ? html`<p>${blueprint.metadata.description}</p>` |                   ? html`<p class="card-content pre-line"> | ||||||
|  |                       ${blueprint.metadata.description} | ||||||
|  |                     </p>` | ||||||
|                   : ""} |                   : ""} | ||||||
|                 ${blueprint?.metadata?.input && |                 ${blueprint?.metadata?.input && | ||||||
|                 Object.keys(blueprint.metadata.input).length |                 Object.keys(blueprint.metadata.input).length | ||||||
|                   ? html`<h3> |                   ? Object.entries(blueprint.metadata.input).map( | ||||||
|                         ${this.hass.localize( |                       ([key, value]) => | ||||||
|                           "ui.panel.config.automation.editor.blueprint.inputs" |                         html`<ha-settings-row .narrow=${this.narrow}> | ||||||
|                         )} |                           <span slot="heading">${value?.name || key}</span> | ||||||
|                       </h3> |                           <span slot="description">${value?.description}</span> | ||||||
|                       ${Object.entries(blueprint.metadata.input).map( |                           ${value?.selector | ||||||
|                         ([key, value]) => |                             ? html`<ha-selector | ||||||
|                           html`<ha-settings-row .narrow=${this.narrow}> |                                 .hass=${this.hass} | ||||||
|                             <span slot="heading">${value?.name || key}</span> |                                 .selector=${value.selector} | ||||||
|                             <span slot="description" |                                 .key=${key} | ||||||
|                               >${value?.description}</span |                                 .value=${(this.config.use_blueprint.input && | ||||||
|                             > |                                   this.config.use_blueprint.input[key]) || | ||||||
|                             ${value?.selector |                                 value?.default} | ||||||
|                               ? html`<ha-selector |                                 @value-changed=${this._inputChanged} | ||||||
|                                   .hass=${this.hass} |                               ></ha-selector>` | ||||||
|                                   .selector=${value.selector} |                             : html`<paper-input | ||||||
|                                   .key=${key} |                                 .key=${key} | ||||||
|                                   .value=${(this.config.use_blueprint.input && |                                 required | ||||||
|                                     this.config.use_blueprint.input[key]) || |                                 .value=${this.config.use_blueprint.input && | ||||||
|                                   value?.default} |                                 this.config.use_blueprint.input[key]} | ||||||
|                                   @value-changed=${this._inputChanged} |                                 @value-changed=${this._inputChanged} | ||||||
|                                 ></ha-selector>` |                                 no-label-float | ||||||
|                               : html`<paper-input |                               ></paper-input>`} | ||||||
|                                   .key=${key} |                         </ha-settings-row>` | ||||||
|                                   .value=${this.config.use_blueprint.input && |                     ) | ||||||
|                                   this.config.use_blueprint.input[key]} |  | ||||||
|                                   @value-changed=${this._inputChanged} |  | ||||||
|                                   no-label-float |  | ||||||
|                                 ></paper-input>`} |  | ||||||
|                           </ha-settings-row>` |  | ||||||
|                       )}` |  | ||||||
|                   : html`<p class="padding"> |                   : html`<p class="padding"> | ||||||
|                       ${this.hass.localize( |                       ${this.hass.localize( | ||||||
|                         "ui.panel.config.automation.editor.blueprint.no_inputs" |                         "ui.panel.config.automation.editor.blueprint.no_inputs" | ||||||
| @@ -237,12 +224,18 @@ export class HaBlueprintAutomationEditor extends LitElement { | |||||||
|     ) { |     ) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |     const input = { ...this.config.use_blueprint.input, [key]: value }; | ||||||
|  |  | ||||||
|  |     if (value === "" || value === undefined) { | ||||||
|  |       delete input[key]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     fireEvent(this, "value-changed", { |     fireEvent(this, "value-changed", { | ||||||
|       value: { |       value: { | ||||||
|         ...this.config!, |         ...this.config!, | ||||||
|         use_blueprint: { |         use_blueprint: { | ||||||
|           ...this.config.use_blueprint, |           ...this.config.use_blueprint, | ||||||
|           input: { ...this.config.use_blueprint.input, [key]: value }, |           input, | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
| @@ -267,33 +260,18 @@ export class HaBlueprintAutomationEditor extends LitElement { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _navigateBlueprints() { |  | ||||||
|     navigate(this, "/config/blueprint"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult[] { |   static get styles(): CSSResult[] { | ||||||
|     return [ |     return [ | ||||||
|       haStyle, |       haStyle, | ||||||
|       css` |       css` | ||||||
|         ha-card { |  | ||||||
|           overflow: hidden; |  | ||||||
|         } |  | ||||||
|         .errors { |  | ||||||
|           padding: 20px; |  | ||||||
|           font-weight: bold; |  | ||||||
|           color: var(--error-color); |  | ||||||
|         } |  | ||||||
|         .padding { |         .padding { | ||||||
|           padding: 16px; |           padding: 16px; | ||||||
|         } |         } | ||||||
|         .content { |         .pre-line { | ||||||
|           padding-bottom: 20px; |           white-space: pre-line; | ||||||
|         } |         } | ||||||
|         .blueprint-picker-container { |         .blueprint-picker-container { | ||||||
|           padding: 16px; |           padding: 16px; | ||||||
|           display: flex; |  | ||||||
|           align-items: center; |  | ||||||
|           justify-content: space-between; |  | ||||||
|         } |         } | ||||||
|         h3 { |         h3 { | ||||||
|           margin: 16px; |           margin: 16px; | ||||||
| @@ -312,24 +290,10 @@ export class HaBlueprintAutomationEditor extends LitElement { | |||||||
|           border-top: 1px solid var(--divider-color); |           border-top: 1px solid var(--divider-color); | ||||||
|         } |         } | ||||||
|         :host(:not([narrow])) ha-settings-row paper-input { |         :host(:not([narrow])) ha-settings-row paper-input { | ||||||
|           width: 50%; |           width: 60%; | ||||||
|         } |         } | ||||||
|         :host(:not([narrow])) ha-settings-row ha-selector { |         :host(:not([narrow])) ha-settings-row ha-selector { | ||||||
|           width: 50%; |           width: 60%; | ||||||
|         } |  | ||||||
|         mwc-fab { |  | ||||||
|           position: relative; |  | ||||||
|           bottom: calc(-80px - env(safe-area-inset-bottom)); |  | ||||||
|           transition: bottom 0.3s; |  | ||||||
|         } |  | ||||||
|         mwc-fab.dirty { |  | ||||||
|           bottom: 0; |  | ||||||
|         } |  | ||||||
|         .selected_menu_item { |  | ||||||
|           color: var(--primary-color); |  | ||||||
|         } |  | ||||||
|         li[role="separator"] { |  | ||||||
|           border-bottom-color: var(--divider-color); |  | ||||||
|         } |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "@material/mwc-fab"; | import "../../../components/ha-fab"; | ||||||
| import { | import { | ||||||
|   mdiCheck, |   mdiCheck, | ||||||
|   mdiContentDuplicate, |   mdiContentDuplicate, | ||||||
| @@ -32,6 +32,7 @@ import "../../../components/ha-svg-icon"; | |||||||
| import "../../../components/ha-yaml-editor"; | import "../../../components/ha-yaml-editor"; | ||||||
| import { showToast } from "../../../util/toast"; | import { showToast } from "../../../util/toast"; | ||||||
| import type { HaYamlEditor } from "../../../components/ha-yaml-editor"; | import type { HaYamlEditor } from "../../../components/ha-yaml-editor"; | ||||||
|  | import { copyToClipboard } from "../../../common/util/copy-clipboard"; | ||||||
| import { | import { | ||||||
|   AutomationConfig, |   AutomationConfig, | ||||||
|   AutomationEntity, |   AutomationEntity, | ||||||
| @@ -206,6 +207,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { | |||||||
|                         ? html`<blueprint-automation-editor |                         ? html`<blueprint-automation-editor | ||||||
|                             .hass=${this.hass} |                             .hass=${this.hass} | ||||||
|                             .narrow=${this.narrow} |                             .narrow=${this.narrow} | ||||||
|  |                             .isWide=${this.isWide} | ||||||
|                             .stateObj=${stateObj} |                             .stateObj=${stateObj} | ||||||
|                             .config=${this._config} |                             .config=${this._config} | ||||||
|                             @value-changed=${this._valueChanged} |                             @value-changed=${this._valueChanged} | ||||||
| @@ -213,6 +215,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { | |||||||
|                         : html`<manual-automation-editor |                         : html`<manual-automation-editor | ||||||
|                             .hass=${this.hass} |                             .hass=${this.hass} | ||||||
|                             .narrow=${this.narrow} |                             .narrow=${this.narrow} | ||||||
|  |                             .isWide=${this.isWide} | ||||||
|                             .stateObj=${stateObj} |                             .stateObj=${stateObj} | ||||||
|                             .config=${this._config} |                             .config=${this._config} | ||||||
|                             @value-changed=${this._valueChanged} |                             @value-changed=${this._valueChanged} | ||||||
| @@ -271,7 +274,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { | |||||||
|               </div> |               </div> | ||||||
|             ` |             ` | ||||||
|           : ""} |           : ""} | ||||||
|         <mwc-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           class=${classMap({ dirty: this._dirty })} |           class=${classMap({ dirty: this._dirty })} | ||||||
|           .label=${this.hass.localize("ui.panel.config.automation.editor.save")} |           .label=${this.hass.localize("ui.panel.config.automation.editor.save")} | ||||||
| @@ -279,7 +282,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { | |||||||
|           @click=${this._saveAutomation} |           @click=${this._saveAutomation} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon> | ||||||
|         </mwc-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage> |       </hass-tabs-subpage> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| @@ -394,7 +397,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { | |||||||
|  |  | ||||||
|   private async _copyYaml() { |   private async _copyYaml() { | ||||||
|     if (this._editor?.yaml) { |     if (this._editor?.yaml) { | ||||||
|       navigator.clipboard.writeText(this._editor.yaml); |       copyToClipboard(this._editor.yaml); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -524,21 +527,18 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { | |||||||
|         .content { |         .content { | ||||||
|           padding-bottom: 20px; |           padding-bottom: 20px; | ||||||
|         } |         } | ||||||
|         span[slot="introduction"] a { |  | ||||||
|           color: var(--primary-color); |  | ||||||
|         } |  | ||||||
|         p { |         p { | ||||||
|           margin-bottom: 0; |           margin-bottom: 0; | ||||||
|         } |         } | ||||||
|         ha-entity-toggle { |         ha-entity-toggle { | ||||||
|           margin-right: 8px; |           margin-right: 8px; | ||||||
|         } |         } | ||||||
|         mwc-fab { |         ha-fab { | ||||||
|           position: relative; |           position: relative; | ||||||
|           bottom: calc(-80px - env(safe-area-inset-bottom)); |           bottom: calc(-80px - env(safe-area-inset-bottom)); | ||||||
|           transition: bottom 0.3s; |           transition: bottom 0.3s; | ||||||
|         } |         } | ||||||
|         mwc-fab.dirty { |         ha-fab.dirty { | ||||||
|           bottom: 0; |           bottom: 0; | ||||||
|         } |         } | ||||||
|         .selected_menu_item { |         .selected_menu_item { | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "@material/mwc-fab"; | import "../../../components/ha-fab"; | ||||||
| import "@material/mwc-icon-button"; | import "@material/mwc-icon-button"; | ||||||
| import { mdiPlus, mdiHelpCircle } from "@mdi/js"; | import { mdiPlus, mdiHelpCircle } from "@mdi/js"; | ||||||
| import "@polymer/paper-tooltip/paper-tooltip"; | import "@polymer/paper-tooltip/paper-tooltip"; | ||||||
| @@ -170,7 +170,7 @@ class HaAutomationPicker extends LitElement { | |||||||
|         <mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}> |         <mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}> | ||||||
|           <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> |           <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> | ||||||
|         </mwc-icon-button> |         </mwc-icon-button> | ||||||
|         <mwc-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           .label=${this.hass.localize( |           .label=${this.hass.localize( | ||||||
|             "ui.panel.config.automation.picker.add_automation" |             "ui.panel.config.automation.picker.add_automation" | ||||||
| @@ -179,7 +179,7 @@ class HaAutomationPicker extends LitElement { | |||||||
|           @click=${this._createNew} |           @click=${this._createNew} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|         </mwc-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage-data-table> |       </hass-tabs-subpage-data-table> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -309,14 +309,6 @@ export class HaManualAutomationEditor extends LitElement { | |||||||
|         ha-card { |         ha-card { | ||||||
|           overflow: hidden; |           overflow: hidden; | ||||||
|         } |         } | ||||||
|         .errors { |  | ||||||
|           padding: 20px; |  | ||||||
|           font-weight: bold; |  | ||||||
|           color: var(--error-color); |  | ||||||
|         } |  | ||||||
|         .content { |  | ||||||
|           padding-bottom: 20px; |  | ||||||
|         } |  | ||||||
|         span[slot="introduction"] a { |         span[slot="introduction"] a { | ||||||
|           color: var(--primary-color); |           color: var(--primary-color); | ||||||
|         } |         } | ||||||
| @@ -326,20 +318,6 @@ export class HaManualAutomationEditor extends LitElement { | |||||||
|         ha-entity-toggle { |         ha-entity-toggle { | ||||||
|           margin-right: 8px; |           margin-right: 8px; | ||||||
|         } |         } | ||||||
|         mwc-fab { |  | ||||||
|           position: relative; |  | ||||||
|           bottom: calc(-80px - env(safe-area-inset-bottom)); |  | ||||||
|           transition: bottom 0.3s; |  | ||||||
|         } |  | ||||||
|         mwc-fab.dirty { |  | ||||||
|           bottom: 0; |  | ||||||
|         } |  | ||||||
|         .selected_menu_item { |  | ||||||
|           color: var(--primary-color); |  | ||||||
|         } |  | ||||||
|         li[role="separator"] { |  | ||||||
|           border-bottom-color: var(--divider-color); |  | ||||||
|         } |  | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ import "@polymer/paper-input/paper-input"; | |||||||
| import type { PaperInputElement } from "@polymer/paper-input/paper-input"; | import type { PaperInputElement } from "@polymer/paper-input/paper-input"; | ||||||
| import "../../../components/ha-circular-progress"; | import "../../../components/ha-circular-progress"; | ||||||
| import { | import { | ||||||
|   css, |  | ||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
| @@ -13,6 +12,7 @@ import { | |||||||
|   internalProperty, |   internalProperty, | ||||||
|   query, |   query, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
|  |   css, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import "../../../components/ha-dialog"; | import "../../../components/ha-dialog"; | ||||||
| import { haStyleDialog } from "../../../resources/styles"; | import { haStyleDialog } from "../../../resources/styles"; | ||||||
| @@ -74,7 +74,9 @@ class DialogImportBlueprint extends LitElement { | |||||||
|                   this._result.blueprint.metadata.domain |                   this._result.blueprint.metadata.domain | ||||||
|                 )} |                 )} | ||||||
|                 <br /> |                 <br /> | ||||||
|                 ${this._result.blueprint.metadata.description} |                 <p class="pre-line"> | ||||||
|  |                   ${this._result.blueprint.metadata.description} | ||||||
|  |                 </p> | ||||||
|                 ${this._result.validation_errors |                 ${this._result.validation_errors | ||||||
|                   ? html` |                   ? html` | ||||||
|                       <p class="error"> |                       <p class="error"> | ||||||
| @@ -97,16 +99,24 @@ class DialogImportBlueprint extends LitElement { | |||||||
|                         )} |                         )} | ||||||
|                       ></paper-input> |                       ></paper-input> | ||||||
|                     `} |                     `} | ||||||
|                 <ha-expansion-panel> |                 <ha-expansion-panel | ||||||
|                   <span slot="title" |                   .header=${this.hass.localize( | ||||||
|                     >${this.hass.localize( |                     "ui.panel.config.blueprint.add.raw_blueprint" | ||||||
|                       "ui.panel.config.blueprint.add.raw_blueprint" |                   )} | ||||||
|                     )}</span |                 > | ||||||
|                   > |  | ||||||
|                   <pre>${this._result.raw_data}</pre> |                   <pre>${this._result.raw_data}</pre> | ||||||
|                 </ha-expansion-panel>` |                 </ha-expansion-panel>` | ||||||
|             : html`${this.hass.localize( |             : html`${this.hass.localize( | ||||||
|                   "ui.panel.config.blueprint.add.import_introduction" |                   "ui.panel.config.blueprint.add.import_introduction_link", | ||||||
|  |                   "community_link", | ||||||
|  |                   html`<a | ||||||
|  |                     href="https://www.home-assistant.io/get-blueprints" | ||||||
|  |                     target="_blank" | ||||||
|  |                     rel="noreferrer noopener" | ||||||
|  |                     >${this.hass.localize( | ||||||
|  |                       "ui.panel.config.blueprint.add.community_forums" | ||||||
|  |                     )}</a | ||||||
|  |                   >` | ||||||
|                 )}<paper-input |                 )}<paper-input | ||||||
|                   id="input" |                   id="input" | ||||||
|                   .label=${this.hass.localize( |                   .label=${this.hass.localize( | ||||||
| @@ -205,8 +215,8 @@ class DialogImportBlueprint extends LitElement { | |||||||
|     return [ |     return [ | ||||||
|       haStyleDialog, |       haStyleDialog, | ||||||
|       css` |       css` | ||||||
|         ha-expansion-panel { |         .pre-line { | ||||||
|           --expansion-panel-summary-padding: 0; |           white-space: pre-line; | ||||||
|         } |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import "@material/mwc-fab"; | import "../../../components/ha-fab"; | ||||||
| import "@material/mwc-icon-button"; | import "@material/mwc-icon-button"; | ||||||
| import { mdiPlus, mdiHelpCircle, mdiDelete, mdiRobot } from "@mdi/js"; | import { mdiHelpCircle, mdiDelete, mdiRobot, mdiDownload } from "@mdi/js"; | ||||||
| import "@polymer/paper-tooltip/paper-tooltip"; | import "@polymer/paper-tooltip/paper-tooltip"; | ||||||
| import { | import { | ||||||
|   CSSResult, |   CSSResult, | ||||||
| @@ -112,7 +112,6 @@ class HaBlueprintOverview extends LitElement { | |||||||
|       create: { |       create: { | ||||||
|         title: "", |         title: "", | ||||||
|         type: narrow ? "icon-button" : undefined, |         type: narrow ? "icon-button" : undefined, | ||||||
|         width: narrow ? undefined : "180px", |  | ||||||
|         template: (_, blueprint: any) => |         template: (_, blueprint: any) => | ||||||
|           blueprint.error |           blueprint.error | ||||||
|             ? "" |             ? "" | ||||||
| @@ -126,8 +125,9 @@ class HaBlueprintOverview extends LitElement { | |||||||
|                   "ui.panel.config.blueprint.overview.use_blueprint" |                   "ui.panel.config.blueprint.overview.use_blueprint" | ||||||
|                 )} |                 )} | ||||||
|                 @click=${(ev) => this._createNew(ev)} |                 @click=${(ev) => this._createNew(ev)} | ||||||
|                 ><ha-svg-icon .path=${mdiRobot}></ha-svg-icon |               > | ||||||
|               ></mwc-icon-button>` |                 <ha-svg-icon .path=${mdiRobot}></ha-svg-icon> | ||||||
|  |               </mwc-icon-button>` | ||||||
|             : html`<mwc-button |             : html`<mwc-button | ||||||
|                 .blueprint=${blueprint} |                 .blueprint=${blueprint} | ||||||
|                 @click=${(ev) => this._createNew(ev)} |                 @click=${(ev) => this._createNew(ev)} | ||||||
| @@ -170,11 +170,28 @@ class HaBlueprintOverview extends LitElement { | |||||||
|           "ui.panel.config.blueprint.overview.no_blueprints" |           "ui.panel.config.blueprint.overview.no_blueprints" | ||||||
|         )} |         )} | ||||||
|         hasFab |         hasFab | ||||||
|  |         .appendRow=${html` <div | ||||||
|  |           class="mdc-data-table__cell" | ||||||
|  |           style="width: 100%; text-align: center;" | ||||||
|  |           role="cell" | ||||||
|  |         > | ||||||
|  |           <a | ||||||
|  |             href="https://www.home-assistant.io/get-blueprints" | ||||||
|  |             target="_blank" | ||||||
|  |             rel="noreferrer noopener" | ||||||
|  |           > | ||||||
|  |             <mwc-button | ||||||
|  |               >${this.hass.localize( | ||||||
|  |                 "ui.panel.config.blueprint.overview.discover_more" | ||||||
|  |               )}</mwc-button | ||||||
|  |             > | ||||||
|  |           </a> | ||||||
|  |         </div>`} | ||||||
|       > |       > | ||||||
|         <mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}> |         <mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}> | ||||||
|           <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> |           <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> | ||||||
|         </mwc-icon-button> |         </mwc-icon-button> | ||||||
|         <mwc-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           .label=${this.hass.localize( |           .label=${this.hass.localize( | ||||||
|             "ui.panel.config.blueprint.overview.add_blueprint" |             "ui.panel.config.blueprint.overview.add_blueprint" | ||||||
| @@ -182,8 +199,8 @@ class HaBlueprintOverview extends LitElement { | |||||||
|           extended |           extended | ||||||
|           @click=${this._addBlueprint} |           @click=${this._addBlueprint} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon> | ||||||
|         </mwc-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage-data-table> |       </hass-tabs-subpage-data-table> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| @@ -195,7 +212,10 @@ class HaBlueprintOverview extends LitElement { | |||||||
|         ${this.hass.localize("ui.panel.config.blueprint.overview.introduction")} |         ${this.hass.localize("ui.panel.config.blueprint.overview.introduction")} | ||||||
|         <p> |         <p> | ||||||
|           <a |           <a | ||||||
|             href="${documentationUrl(this.hass, "/docs/blueprint/editor/")}" |             href="${documentationUrl( | ||||||
|  |               this.hass, | ||||||
|  |               "/docs/automation/using_blueprints/" | ||||||
|  |             )}" | ||||||
|             target="_blank" |             target="_blank" | ||||||
|             rel="noreferrer" |             rel="noreferrer" | ||||||
|           > |           > | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ import { | |||||||
|   html, |   html, | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|   internalProperty, |  | ||||||
|   PropertyValues, |   PropertyValues, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| @@ -31,7 +30,7 @@ export class HaDeviceEntitiesCard extends LitElement { | |||||||
|  |  | ||||||
|   @property() public entities!: EntityRegistryStateEntry[]; |   @property() public entities!: EntityRegistryStateEntry[]; | ||||||
|  |  | ||||||
|   @internalProperty() private _showDisabled = false; |   @property() public showDisabled = false; | ||||||
|  |  | ||||||
|   private _entityRows: Array<LovelaceRow | HuiErrorCard> = []; |   private _entityRows: Array<LovelaceRow | HuiErrorCard> = []; | ||||||
|  |  | ||||||
| @@ -68,7 +67,7 @@ export class HaDeviceEntitiesCard extends LitElement { | |||||||
|                 })} |                 })} | ||||||
|               </div> |               </div> | ||||||
|               ${disabledEntities.length |               ${disabledEntities.length | ||||||
|                 ? !this._showDisabled |                 ? !this.showDisabled | ||||||
|                   ? html` |                   ? html` | ||||||
|                       <button |                       <button | ||||||
|                         class="show-more" |                         class="show-more" | ||||||
| @@ -119,7 +118,7 @@ export class HaDeviceEntitiesCard extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _toggleShowDisabled() { |   private _toggleShowDisabled() { | ||||||
|     this._showDisabled = !this._showDisabled; |     this.showDisabled = !this.showDisabled; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult { |   private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult { | ||||||
| @@ -227,3 +226,9 @@ export class HaDeviceEntitiesCard extends LitElement { | |||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-device-entities-card": HaDeviceEntitiesCard; | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ import { | |||||||
|   computeDeviceName, |   computeDeviceName, | ||||||
|   DeviceRegistryEntry, |   DeviceRegistryEntry, | ||||||
| } from "../../../../data/device_registry"; | } from "../../../../data/device_registry"; | ||||||
| import { loadDeviceRegistryDetailDialog } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail"; | import { loadDeviceRegistryDetailDialog } from "../device-registry-detail/show-dialog-device-registry-detail"; | ||||||
| import { HomeAssistant } from "../../../../types"; | import { HomeAssistant } from "../../../../types"; | ||||||
|  |  | ||||||
| @customElement("ha-device-info-card") | @customElement("ha-device-info-card") | ||||||
|   | |||||||
| @@ -3,8 +3,8 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; | |||||||
| import "@polymer/paper-input/paper-input"; | import "@polymer/paper-input/paper-input"; | ||||||
| import "@polymer/paper-item/paper-item"; | import "@polymer/paper-item/paper-item"; | ||||||
| import "@polymer/paper-listbox/paper-listbox"; | import "@polymer/paper-listbox/paper-listbox"; | ||||||
| import "../../components/ha-dialog"; | import "../../../../components/ha-dialog"; | ||||||
| import "../../components/ha-area-picker"; | import "../../../../components/ha-area-picker"; | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
|   CSSResult, |   CSSResult, | ||||||
| @@ -18,11 +18,12 @@ import { | |||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| 
 | 
 | ||||||
| import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail"; | import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../../../types"; | ||||||
| import { PolymerChangedEvent } from "../../polymer-types"; | import type { HaSwitch } from "../../../../components/ha-switch"; | ||||||
| import { computeDeviceName } from "../../data/device_registry"; | import { PolymerChangedEvent } from "../../../../polymer-types"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { computeDeviceName } from "../../../../data/device_registry"; | ||||||
| import { haStyleDialog } from "../../resources/styles"; | import { fireEvent } from "../../../../common/dom/fire_event"; | ||||||
|  | import { haStyle, haStyleDialog } from "../../../../resources/styles"; | ||||||
| 
 | 
 | ||||||
| @customElement("dialog-device-registry-detail") | @customElement("dialog-device-registry-detail") | ||||||
| class DialogDeviceRegistryDetail extends LitElement { | class DialogDeviceRegistryDetail extends LitElement { | ||||||
| @@ -36,6 +37,8 @@ class DialogDeviceRegistryDetail extends LitElement { | |||||||
| 
 | 
 | ||||||
|   @internalProperty() private _areaId?: string; |   @internalProperty() private _areaId?: string; | ||||||
| 
 | 
 | ||||||
|  |   @internalProperty() private _disabledBy!: string | null; | ||||||
|  | 
 | ||||||
|   @internalProperty() private _submitting?: boolean; |   @internalProperty() private _submitting?: boolean; | ||||||
| 
 | 
 | ||||||
|   public async showDialog( |   public async showDialog( | ||||||
| @@ -45,6 +48,7 @@ class DialogDeviceRegistryDetail extends LitElement { | |||||||
|     this._error = undefined; |     this._error = undefined; | ||||||
|     this._nameByUser = this._params.device.name_by_user || ""; |     this._nameByUser = this._params.device.name_by_user || ""; | ||||||
|     this._areaId = this._params.device.area_id; |     this._areaId = this._params.device.area_id; | ||||||
|  |     this._disabledBy = this._params.device.disabled_by; | ||||||
|     await this.updateComplete; |     await this.updateComplete; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @@ -80,6 +84,32 @@ class DialogDeviceRegistryDetail extends LitElement { | |||||||
|               .value=${this._areaId} |               .value=${this._areaId} | ||||||
|               @value-changed=${this._areaPicked} |               @value-changed=${this._areaPicked} | ||||||
|             ></ha-area-picker> |             ></ha-area-picker> | ||||||
|  |             <div class="row"> | ||||||
|  |               <ha-switch | ||||||
|  |                 .checked=${!this._disabledBy} | ||||||
|  |                 @change=${this._disabledByChanged} | ||||||
|  |               > | ||||||
|  |               </ha-switch> | ||||||
|  |               <div> | ||||||
|  |                 <div> | ||||||
|  |                   ${this.hass.localize("ui.panel.config.devices.enabled_label")} | ||||||
|  |                 </div> | ||||||
|  |                 <div class="secondary"> | ||||||
|  |                   ${this._disabledBy && this._disabledBy !== "user" | ||||||
|  |                     ? this.hass.localize( | ||||||
|  |                         "ui.panel.config.devices.enabled_cause", | ||||||
|  |                         "cause", | ||||||
|  |                         this.hass.localize( | ||||||
|  |                           `config_entry.disabled_by.${this._disabledBy}` | ||||||
|  |                         ) | ||||||
|  |                       ) | ||||||
|  |                     : ""} | ||||||
|  |                   ${this.hass.localize( | ||||||
|  |                     "ui.panel.config.devices.enabled_description" | ||||||
|  |                   )} | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|         <mwc-button |         <mwc-button | ||||||
| @@ -109,12 +139,17 @@ class DialogDeviceRegistryDetail extends LitElement { | |||||||
|     this._areaId = event.detail.value; |     this._areaId = event.detail.value; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private _disabledByChanged(ev: Event): void { | ||||||
|  |     this._disabledBy = (ev.target as HaSwitch).checked ? null : "user"; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private async _updateEntry(): Promise<void> { |   private async _updateEntry(): Promise<void> { | ||||||
|     this._submitting = true; |     this._submitting = true; | ||||||
|     try { |     try { | ||||||
|       await this._params!.updateEntry({ |       await this._params!.updateEntry({ | ||||||
|         name_by_user: this._nameByUser.trim() || null, |         name_by_user: this._nameByUser.trim() || null, | ||||||
|         area_id: this._areaId || null, |         area_id: this._areaId || null, | ||||||
|  |         disabled_by: this._disabledBy || null, | ||||||
|       }); |       }); | ||||||
|       this._params = undefined; |       this._params = undefined; | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
| @@ -128,6 +163,7 @@ class DialogDeviceRegistryDetail extends LitElement { | |||||||
| 
 | 
 | ||||||
|   static get styles(): CSSResult[] { |   static get styles(): CSSResult[] { | ||||||
|     return [ |     return [ | ||||||
|  |       haStyle, | ||||||
|       haStyleDialog, |       haStyleDialog, | ||||||
|       css` |       css` | ||||||
|         .form { |         .form { | ||||||
| @@ -139,6 +175,15 @@ class DialogDeviceRegistryDetail extends LitElement { | |||||||
|         .error { |         .error { | ||||||
|           color: var(--error-color); |           color: var(--error-color); | ||||||
|         } |         } | ||||||
|  |         ha-switch { | ||||||
|  |           margin-right: 16px; | ||||||
|  |         } | ||||||
|  |         .row { | ||||||
|  |           margin-top: 8px; | ||||||
|  |           color: var(--primary-text-color); | ||||||
|  |           display: flex; | ||||||
|  |           align-items: center; | ||||||
|  |         } | ||||||
|       `,
 |       `,
 | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
| @@ -1,8 +1,8 @@ | |||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../../../common/dom/fire_event"; | ||||||
| import { | import { | ||||||
|   DeviceRegistryEntry, |   DeviceRegistryEntry, | ||||||
|   DeviceRegistryEntryMutableParams, |   DeviceRegistryEntryMutableParams, | ||||||
| } from "../../data/device_registry"; | } from "../../../../data/device_registry"; | ||||||
| 
 | 
 | ||||||
| export interface DeviceRegistryDetailDialogParams { | export interface DeviceRegistryDetailDialogParams { | ||||||
|   device: DeviceRegistryEntry; |   device: DeviceRegistryEntry; | ||||||
| @@ -35,7 +35,7 @@ import { findRelated, RelatedResult } from "../../../data/search"; | |||||||
| import { | import { | ||||||
|   loadDeviceRegistryDetailDialog, |   loadDeviceRegistryDetailDialog, | ||||||
|   showDeviceRegistryDetailDialog, |   showDeviceRegistryDetailDialog, | ||||||
| } from "../../../dialogs/device-registry-detail/show-dialog-device-registry-detail"; | } from "./device-registry-detail/show-dialog-device-registry-detail"; | ||||||
| import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; | import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; | ||||||
| import "../../../layouts/hass-error-screen"; | import "../../../layouts/hass-error-screen"; | ||||||
| import "../../../layouts/hass-tabs-subpage"; | import "../../../layouts/hass-tabs-subpage"; | ||||||
| @@ -46,6 +46,7 @@ import "./device-detail/ha-device-entities-card"; | |||||||
| import "./device-detail/ha-device-info-card"; | import "./device-detail/ha-device-info-card"; | ||||||
| import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation"; | import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation"; | ||||||
| import { brandsUrl } from "../../../util/brands-url"; | import { brandsUrl } from "../../../util/brands-url"; | ||||||
|  | import { haStyle } from "../../../resources/styles"; | ||||||
|  |  | ||||||
| export interface EntityRegistryStateEntry extends EntityRegistryEntry { | export interface EntityRegistryStateEntry extends EntityRegistryEntry { | ||||||
|   stateName?: string | null; |   stateName?: string | null; | ||||||
| @@ -246,6 +247,28 @@ export class HaConfigDevicePage extends LitElement { | |||||||
|                 .devices=${this.devices} |                 .devices=${this.devices} | ||||||
|                 .device=${device} |                 .device=${device} | ||||||
|               > |               > | ||||||
|  |               ${ | ||||||
|  |                 device.disabled_by | ||||||
|  |                   ? html` | ||||||
|  |                       <div> | ||||||
|  |                         <p class="warning"> | ||||||
|  |                           ${this.hass.localize( | ||||||
|  |                             "ui.panel.config.devices.enabled_cause", | ||||||
|  |                             "cause", | ||||||
|  |                             this.hass.localize( | ||||||
|  |                               `ui.panel.config.devices.disabled_by.${device.disabled_by}` | ||||||
|  |                             ) | ||||||
|  |                           )} | ||||||
|  |                         </p> | ||||||
|  |                       </div> | ||||||
|  |                       <div class="card-actions" slot="actions"> | ||||||
|  |                         <mwc-button unelevated @click=${this._enableDevice}> | ||||||
|  |                           ${this.hass.localize("ui.common.enable")} | ||||||
|  |                         </mwc-button> | ||||||
|  |                       </div> | ||||||
|  |                     ` | ||||||
|  |                   : html`` | ||||||
|  |               } | ||||||
|               ${this._renderIntegrationInfo(device, integrations)} |               ${this._renderIntegrationInfo(device, integrations)} | ||||||
|               </ha-device-info-card> |               </ha-device-info-card> | ||||||
|  |  | ||||||
| @@ -255,6 +278,7 @@ export class HaConfigDevicePage extends LitElement { | |||||||
|                     <ha-device-entities-card |                     <ha-device-entities-card | ||||||
|                       .hass=${this.hass} |                       .hass=${this.hass} | ||||||
|                       .entities=${entities} |                       .entities=${entities} | ||||||
|  |                       .showDisabled=${device.disabled_by !== null} | ||||||
|                     > |                     > | ||||||
|                     </ha-device-entities-card> |                     </ha-device-entities-card> | ||||||
|                   ` |                   ` | ||||||
| @@ -272,9 +296,14 @@ export class HaConfigDevicePage extends LitElement { | |||||||
|                         )} |                         )} | ||||||
|                         <ha-icon-button |                         <ha-icon-button | ||||||
|                           @click=${this._showAutomationDialog} |                           @click=${this._showAutomationDialog} | ||||||
|                           title=${this.hass.localize( |                           .disabled=${device.disabled_by} | ||||||
|                             "ui.panel.config.devices.automation.create" |                           title=${device.disabled_by | ||||||
|                           )} |                             ? this.hass.localize( | ||||||
|  |                                 "ui.panel.config.devices.automation.create_disabled" | ||||||
|  |                               ) | ||||||
|  |                             : this.hass.localize( | ||||||
|  |                                 "ui.panel.config.devices.automation.create" | ||||||
|  |                               )} | ||||||
|                           icon="hass:plus-circle" |                           icon="hass:plus-circle" | ||||||
|                         ></ha-icon-button> |                         ></ha-icon-button> | ||||||
|                       </h1> |                       </h1> | ||||||
| @@ -342,9 +371,16 @@ export class HaConfigDevicePage extends LitElement { | |||||||
|  |  | ||||||
|                                   <ha-icon-button |                                   <ha-icon-button | ||||||
|                                     @click=${this._createScene} |                                     @click=${this._createScene} | ||||||
|                                     title=${this.hass.localize( |                                     .disabled=${device.disabled_by} | ||||||
|                                       "ui.panel.config.devices.scene.create" |                                     title=${ | ||||||
|                                     )} |                                       device.disabled_by | ||||||
|  |                                         ? this.hass.localize( | ||||||
|  |                                             "ui.panel.config.devices.scene.create_disabled" | ||||||
|  |                                           ) | ||||||
|  |                                         : this.hass.localize( | ||||||
|  |                                             "ui.panel.config.devices.scene.create" | ||||||
|  |                                           ) | ||||||
|  |                                     } | ||||||
|                                     icon="hass:plus-circle" |                                     icon="hass:plus-circle" | ||||||
|                                   ></ha-icon-button> |                                   ></ha-icon-button> | ||||||
|                         </h1> |                         </h1> | ||||||
| @@ -415,9 +451,14 @@ export class HaConfigDevicePage extends LitElement { | |||||||
|                           )} |                           )} | ||||||
|                           <ha-icon-button |                           <ha-icon-button | ||||||
|                             @click=${this._showScriptDialog} |                             @click=${this._showScriptDialog} | ||||||
|                             title=${this.hass.localize( |                             .disabled=${device.disabled_by} | ||||||
|                               "ui.panel.config.devices.script.create" |                             title=${device.disabled_by | ||||||
|                             )} |                               ? this.hass.localize( | ||||||
|  |                                   "ui.panel.config.devices.script.create_disabled" | ||||||
|  |                                 ) | ||||||
|  |                               : this.hass.localize( | ||||||
|  |                                   "ui.panel.config.devices.script.create" | ||||||
|  |                                 )} | ||||||
|                             icon="hass:plus-circle" |                             icon="hass:plus-circle" | ||||||
|                           ></ha-icon-button> |                           ></ha-icon-button> | ||||||
|                         </h1> |                         </h1> | ||||||
| @@ -632,128 +673,137 @@ export class HaConfigDevicePage extends LitElement { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResult { |   private async _enableDevice(): Promise<void> { | ||||||
|     return css` |     await updateDeviceRegistryEntry(this.hass, this.deviceId, { | ||||||
|       .container { |       disabled_by: null, | ||||||
|         display: flex; |     }); | ||||||
|         flex-wrap: wrap; |   } | ||||||
|         margin: auto; |  | ||||||
|         max-width: 1000px; |  | ||||||
|         margin-top: 32px; |  | ||||||
|         margin-bottom: 32px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .card-header { |   static get styles(): CSSResult[] { | ||||||
|         display: flex; |     return [ | ||||||
|         align-items: center; |       haStyle, | ||||||
|         justify-content: space-between; |       css` | ||||||
|       } |         .container { | ||||||
|  |           display: flex; | ||||||
|  |           flex-wrap: wrap; | ||||||
|  |           margin: auto; | ||||||
|  |           max-width: 1000px; | ||||||
|  |           margin-top: 32px; | ||||||
|  |           margin-bottom: 32px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|       .card-header ha-icon-button { |         .card-header { | ||||||
|         margin-right: -8px; |           display: flex; | ||||||
|         color: var(--primary-color); |           align-items: center; | ||||||
|         height: auto; |           justify-content: space-between; | ||||||
|       } |         } | ||||||
|  |  | ||||||
|       .device-info { |         .card-header ha-icon-button { | ||||||
|         padding: 16px; |           margin-right: -8px; | ||||||
|       } |           color: var(--primary-color); | ||||||
|  |           height: auto; | ||||||
|  |         } | ||||||
|  |  | ||||||
|       .show-more { |         .device-info { | ||||||
|       } |           padding: 16px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|       h1 { |         .show-more { | ||||||
|         margin: 0; |         } | ||||||
|         font-family: var(--paper-font-headline_-_font-family); |  | ||||||
|         -webkit-font-smoothing: var( |  | ||||||
|           --paper-font-headline_-_-webkit-font-smoothing |  | ||||||
|         ); |  | ||||||
|         font-size: var(--paper-font-headline_-_font-size); |  | ||||||
|         font-weight: var(--paper-font-headline_-_font-weight); |  | ||||||
|         letter-spacing: var(--paper-font-headline_-_letter-spacing); |  | ||||||
|         line-height: var(--paper-font-headline_-_line-height); |  | ||||||
|         opacity: var(--dark-primary-opacity); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .header { |         h1 { | ||||||
|         display: flex; |           margin: 0; | ||||||
|         justify-content: space-between; |           font-family: var(--paper-font-headline_-_font-family); | ||||||
|       } |           -webkit-font-smoothing: var( | ||||||
|  |             --paper-font-headline_-_-webkit-font-smoothing | ||||||
|  |           ); | ||||||
|  |           font-size: var(--paper-font-headline_-_font-size); | ||||||
|  |           font-weight: var(--paper-font-headline_-_font-weight); | ||||||
|  |           letter-spacing: var(--paper-font-headline_-_letter-spacing); | ||||||
|  |           line-height: var(--paper-font-headline_-_line-height); | ||||||
|  |           opacity: var(--dark-primary-opacity); | ||||||
|  |         } | ||||||
|  |  | ||||||
|       .column, |         .header { | ||||||
|       .fullwidth { |           display: flex; | ||||||
|         padding: 8px; |           justify-content: space-between; | ||||||
|         box-sizing: border-box; |         } | ||||||
|       } |  | ||||||
|       .column { |  | ||||||
|         width: 33%; |  | ||||||
|         flex-grow: 1; |  | ||||||
|       } |  | ||||||
|       .fullwidth { |  | ||||||
|         width: 100%; |  | ||||||
|         flex-grow: 1; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .header-right { |         .column, | ||||||
|         align-self: center; |         .fullwidth { | ||||||
|       } |           padding: 8px; | ||||||
|  |           box-sizing: border-box; | ||||||
|  |         } | ||||||
|  |         .column { | ||||||
|  |           width: 33%; | ||||||
|  |           flex-grow: 1; | ||||||
|  |         } | ||||||
|  |         .fullwidth { | ||||||
|  |           width: 100%; | ||||||
|  |           flex-grow: 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|       .header-right img { |         .header-right { | ||||||
|         height: 30px; |           align-self: center; | ||||||
|       } |         } | ||||||
|  |  | ||||||
|       .header-right { |         .header-right img { | ||||||
|         display: flex; |           height: 30px; | ||||||
|       } |         } | ||||||
|  |  | ||||||
|       .header-right:first-child { |         .header-right { | ||||||
|         width: 100%; |           display: flex; | ||||||
|         justify-content: flex-end; |         } | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .header-right > *:not(:first-child) { |         .header-right:first-child { | ||||||
|         margin-left: 16px; |           width: 100%; | ||||||
|       } |           justify-content: flex-end; | ||||||
|  |         } | ||||||
|  |  | ||||||
|       .battery { |         .header-right > *:not(:first-child) { | ||||||
|         align-self: center; |           margin-left: 16px; | ||||||
|         align-items: center; |         } | ||||||
|         display: flex; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .column > *:not(:first-child) { |         .battery { | ||||||
|         margin-top: 16px; |           align-self: center; | ||||||
|       } |           align-items: center; | ||||||
|  |           display: flex; | ||||||
|  |         } | ||||||
|  |  | ||||||
|       :host([narrow]) .column { |         .column > *:not(:first-child) { | ||||||
|         width: 100%; |           margin-top: 16px; | ||||||
|       } |         } | ||||||
|  |  | ||||||
|       :host([narrow]) .container { |         :host([narrow]) .column { | ||||||
|         margin-top: 0; |           width: 100%; | ||||||
|       } |         } | ||||||
|  |  | ||||||
|       paper-item { |         :host([narrow]) .container { | ||||||
|         cursor: pointer; |           margin-top: 0; | ||||||
|         font-size: var(--paper-font-body1_-_font-size); |         } | ||||||
|       } |  | ||||||
|  |  | ||||||
|       paper-item.no-link { |         paper-item { | ||||||
|         cursor: default; |           cursor: pointer; | ||||||
|       } |           font-size: var(--paper-font-body1_-_font-size); | ||||||
|  |         } | ||||||
|  |  | ||||||
|       a { |         paper-item.no-link { | ||||||
|         text-decoration: none; |           cursor: default; | ||||||
|         color: var(--primary-color); |         } | ||||||
|       } |  | ||||||
|  |  | ||||||
|       ha-card { |         a { | ||||||
|         padding-bottom: 8px; |           text-decoration: none; | ||||||
|       } |           color: var(--primary-color); | ||||||
|  |         } | ||||||
|  |  | ||||||
|       ha-card a { |         ha-card { | ||||||
|         color: var(--primary-text-color); |           padding-bottom: 8px; | ||||||
|       } |         } | ||||||
|     `; |  | ||||||
|  |         ha-card a { | ||||||
|  |           color: var(--primary-text-color); | ||||||
|  |         } | ||||||
|  |       `, | ||||||
|  |     ]; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,4 +1,9 @@ | |||||||
|  | import { mdiPlus, mdiFilterVariant, mdiCancel } from "@mdi/js"; | ||||||
|  | import "@material/mwc-list/mwc-list-item"; | ||||||
|  | import "@polymer/paper-tooltip/paper-tooltip"; | ||||||
| import { | import { | ||||||
|  |   css, | ||||||
|  |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |   internalProperty, | ||||||
| @@ -6,16 +11,20 @@ import { | |||||||
|   property, |   property, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
|  | import { classMap } from "lit-html/directives/class-map"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
|  | import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; | ||||||
| import { HASSDomEvent } from "../../../common/dom/fire_event"; | import { HASSDomEvent } from "../../../common/dom/fire_event"; | ||||||
| import { navigate } from "../../../common/navigate"; | import { navigate } from "../../../common/navigate"; | ||||||
| import { LocalizeFunc } from "../../../common/translations/localize"; | import { LocalizeFunc } from "../../../common/translations/localize"; | ||||||
|  | import { computeRTL } from "../../../common/util/compute_rtl"; | ||||||
| import { | import { | ||||||
|   DataTableColumnContainer, |   DataTableColumnContainer, | ||||||
|   DataTableRowData, |   DataTableRowData, | ||||||
|   RowClickedEvent, |   RowClickedEvent, | ||||||
| } from "../../../components/data-table/ha-data-table"; | } from "../../../components/data-table/ha-data-table"; | ||||||
| import "../../../components/entity/ha-battery-icon"; | import "../../../components/entity/ha-battery-icon"; | ||||||
|  | import "../../../components/ha-button-menu"; | ||||||
| import { AreaRegistryEntry } from "../../../data/area_registry"; | import { AreaRegistryEntry } from "../../../data/area_registry"; | ||||||
| import { ConfigEntry } from "../../../data/config_entries"; | import { ConfigEntry } from "../../../data/config_entries"; | ||||||
| import { | import { | ||||||
| @@ -32,6 +41,7 @@ import { domainToName } from "../../../data/integration"; | |||||||
| import "../../../layouts/hass-tabs-subpage-data-table"; | import "../../../layouts/hass-tabs-subpage-data-table"; | ||||||
| import { HomeAssistant, Route } from "../../../types"; | import { HomeAssistant, Route } from "../../../types"; | ||||||
| import { configSections } from "../ha-panel-config"; | import { configSections } from "../ha-panel-config"; | ||||||
|  | import { haStyle } from "../../../resources/styles"; | ||||||
|  |  | ||||||
| interface DeviceRowData extends DeviceRegistryEntry { | interface DeviceRowData extends DeviceRegistryEntry { | ||||||
|   device?: DeviceRowData; |   device?: DeviceRowData; | ||||||
| @@ -62,6 +72,12 @@ export class HaConfigDeviceDashboard extends LitElement { | |||||||
|     window.location.search |     window.location.search | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  |   @internalProperty() private _showDisabled = false; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _filter = ""; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _numHiddenDevices = 0; | ||||||
|  |  | ||||||
|   private _activeFilters = memoizeOne( |   private _activeFilters = memoizeOne( | ||||||
|     ( |     ( | ||||||
|       entries: ConfigEntry[], |       entries: ConfigEntry[], | ||||||
| @@ -72,6 +88,10 @@ export class HaConfigDeviceDashboard extends LitElement { | |||||||
|       filters.forEach((value, key) => { |       filters.forEach((value, key) => { | ||||||
|         switch (key) { |         switch (key) { | ||||||
|           case "config_entry": { |           case "config_entry": { | ||||||
|  |             // If we are requested to show the devices for a given config entry, | ||||||
|  |             // also show the disabled ones by default. | ||||||
|  |             this._showDisabled = true; | ||||||
|  |  | ||||||
|             const configEntry = entries.find( |             const configEntry = entries.find( | ||||||
|               (entry) => entry.entry_id === value |               (entry) => entry.entry_id === value | ||||||
|             ); |             ); | ||||||
| @@ -96,13 +116,14 @@ export class HaConfigDeviceDashboard extends LitElement { | |||||||
|     } |     } | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   private _devices = memoizeOne( |   private _devicesAndFilterDomains = memoizeOne( | ||||||
|     ( |     ( | ||||||
|       devices: DeviceRegistryEntry[], |       devices: DeviceRegistryEntry[], | ||||||
|       entries: ConfigEntry[], |       entries: ConfigEntry[], | ||||||
|       entities: EntityRegistryEntry[], |       entities: EntityRegistryEntry[], | ||||||
|       areas: AreaRegistryEntry[], |       areas: AreaRegistryEntry[], | ||||||
|       filters: URLSearchParams, |       filters: URLSearchParams, | ||||||
|  |       showDisabled: boolean, | ||||||
|       localize: LocalizeFunc |       localize: LocalizeFunc | ||||||
|     ) => { |     ) => { | ||||||
|       // Some older installations might have devices pointing at invalid entryIDs |       // Some older installations might have devices pointing at invalid entryIDs | ||||||
| @@ -115,6 +136,9 @@ export class HaConfigDeviceDashboard extends LitElement { | |||||||
|         deviceLookup[device.id] = device; |         deviceLookup[device.id] = device; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       // If nothing gets filtered, this is our correct count of devices | ||||||
|  |       let startLength = outputDevices.length; | ||||||
|  |  | ||||||
|       const deviceEntityLookup: DeviceEntityLookup = {}; |       const deviceEntityLookup: DeviceEntityLookup = {}; | ||||||
|       for (const entity of entities) { |       for (const entity of entities) { | ||||||
|         if (!entity.device_id) { |         if (!entity.device_id) { | ||||||
| @@ -136,16 +160,25 @@ export class HaConfigDeviceDashboard extends LitElement { | |||||||
|         areaLookup[area.area_id] = area; |         areaLookup[area.area_id] = area; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       const filterDomains: string[] = []; | ||||||
|  |  | ||||||
|       filters.forEach((value, key) => { |       filters.forEach((value, key) => { | ||||||
|         switch (key) { |         if (key === "config_entry") { | ||||||
|           case "config_entry": |           outputDevices = outputDevices.filter((device) => | ||||||
|             outputDevices = outputDevices.filter((device) => |             device.config_entries.includes(value) | ||||||
|               device.config_entries.includes(value) |           ); | ||||||
|             ); |           startLength = outputDevices.length; | ||||||
|             break; |           const configEntry = entries.find((entry) => entry.entry_id === value); | ||||||
|  |           if (configEntry) { | ||||||
|  |             filterDomains.push(configEntry.domain); | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|  |       if (!showDisabled) { | ||||||
|  |         outputDevices = outputDevices.filter((device) => !device.disabled_by); | ||||||
|  |       } | ||||||
|  |  | ||||||
|       outputDevices = outputDevices.map((device) => { |       outputDevices = outputDevices.map((device) => { | ||||||
|         return { |         return { | ||||||
|           ...device, |           ...device, | ||||||
| @@ -176,16 +209,19 @@ export class HaConfigDeviceDashboard extends LitElement { | |||||||
|         }; |         }; | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       return outputDevices; |       this._numHiddenDevices = startLength - outputDevices.length; | ||||||
|  |       return { devicesOutput: outputDevices, filteredDomains: filterDomains }; | ||||||
|     } |     } | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   private _columns = memoizeOne( |   private _columns = memoizeOne( | ||||||
|     (narrow: boolean): DataTableColumnContainer => { |     (narrow: boolean, showDisabled: boolean): DataTableColumnContainer => { | ||||||
|       const columns: DataTableColumnContainer = narrow |       const columns: DataTableColumnContainer = narrow | ||||||
|         ? { |         ? { | ||||||
|             name: { |             name: { | ||||||
|               title: "Device", |               title: this.hass.localize( | ||||||
|  |                 "ui.panel.config.devices.data_table.device" | ||||||
|  |               ), | ||||||
|               sortable: true, |               sortable: true, | ||||||
|               filterable: true, |               filterable: true, | ||||||
|               direction: "asc", |               direction: "asc", | ||||||
| @@ -271,6 +307,24 @@ export class HaConfigDeviceDashboard extends LitElement { | |||||||
|             : html` - `; |             : html` - `; | ||||||
|         }, |         }, | ||||||
|       }; |       }; | ||||||
|  |       if (showDisabled) { | ||||||
|  |         columns.disabled_by = { | ||||||
|  |           title: "", | ||||||
|  |           type: "icon", | ||||||
|  |           template: (disabled_by) => | ||||||
|  |             disabled_by | ||||||
|  |               ? html`<div | ||||||
|  |                   tabindex="0" | ||||||
|  |                   style="display:inline-block; position: relative;" | ||||||
|  |                 > | ||||||
|  |                   <ha-svg-icon .path=${mdiCancel}></ha-svg-icon> | ||||||
|  |                   <paper-tooltip animation-delay="0" position="left"> | ||||||
|  |                     ${this.hass.localize("ui.panel.config.devices.disabled")} | ||||||
|  |                   </paper-tooltip> | ||||||
|  |                 </div>` | ||||||
|  |               : "", | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|       return columns; |       return columns; | ||||||
|     } |     } | ||||||
|   ); |   ); | ||||||
| @@ -286,6 +340,126 @@ export class HaConfigDeviceDashboard extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|  |     const { devicesOutput, filteredDomains } = this._devicesAndFilterDomains( | ||||||
|  |       this.devices, | ||||||
|  |       this.entries, | ||||||
|  |       this.entities, | ||||||
|  |       this.areas, | ||||||
|  |       this._searchParms, | ||||||
|  |       this._showDisabled, | ||||||
|  |       this.hass.localize | ||||||
|  |     ); | ||||||
|  |     const includeZHAFab = filteredDomains.includes("zha"); | ||||||
|  |     const activeFilters = this._activeFilters( | ||||||
|  |       this.entries, | ||||||
|  |       this._searchParms, | ||||||
|  |       this.hass.localize | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const headerToolbar = html` | ||||||
|  |       <search-input | ||||||
|  |         no-label-float | ||||||
|  |         no-underline | ||||||
|  |         @value-changed=${this._handleSearchChange} | ||||||
|  |         .filter=${this._filter} | ||||||
|  |         .label=${this.hass.localize("ui.panel.config.devices.picker.search")} | ||||||
|  |       ></search-input | ||||||
|  |       >${activeFilters | ||||||
|  |         ? html`<div class="active-filters"> | ||||||
|  |             ${this.narrow | ||||||
|  |               ? html` <div> | ||||||
|  |                   <ha-icon icon="hass:filter-variant"></ha-icon> | ||||||
|  |                   <paper-tooltip animation-delay="0" position="left"> | ||||||
|  |                     ${this.hass.localize( | ||||||
|  |                       "ui.panel.config.filtering.filtering_by" | ||||||
|  |                     )} | ||||||
|  |                     ${activeFilters.join(", ")} | ||||||
|  |                     ${this._numHiddenDevices | ||||||
|  |                       ? "(" + | ||||||
|  |                         this.hass.localize( | ||||||
|  |                           "ui.panel.config.devices.picker.filter.hidden_devices", | ||||||
|  |                           "number", | ||||||
|  |                           this._numHiddenDevices | ||||||
|  |                         ) + | ||||||
|  |                         ")" | ||||||
|  |                       : ""} | ||||||
|  |                   </paper-tooltip> | ||||||
|  |                 </div>` | ||||||
|  |               : `${this.hass.localize( | ||||||
|  |                   "ui.panel.config.filtering.filtering_by" | ||||||
|  |                 )} ${activeFilters.join(", ")} | ||||||
|  |                     ${ | ||||||
|  |                       this._numHiddenDevices | ||||||
|  |                         ? "(" + | ||||||
|  |                           this.hass.localize( | ||||||
|  |                             "ui.panel.config.devices.picker.filter.hidden_devices", | ||||||
|  |                             "number", | ||||||
|  |                             this._numHiddenDevices | ||||||
|  |                           ) + | ||||||
|  |                           ")" | ||||||
|  |                         : "" | ||||||
|  |                     } | ||||||
|  |                     `} | ||||||
|  |             <mwc-button @click=${this._clearFilter} | ||||||
|  |               >${this.hass.localize( | ||||||
|  |                 "ui.panel.config.filtering.clear" | ||||||
|  |               )}</mwc-button | ||||||
|  |             > | ||||||
|  |           </div>` | ||||||
|  |         : ""} | ||||||
|  |       ${this._numHiddenDevices && !activeFilters | ||||||
|  |         ? html`<div class="active-filters"> | ||||||
|  |             ${this.narrow | ||||||
|  |               ? html` <div> | ||||||
|  |                   <ha-icon icon="hass:filter-variant"></ha-icon> | ||||||
|  |                   <paper-tooltip animation-delay="0" position="left"> | ||||||
|  |                     ${this.hass.localize( | ||||||
|  |                       "ui.panel.config.devices.picker.filter.hidden_devices", | ||||||
|  |                       "number", | ||||||
|  |                       this._numHiddenDevices | ||||||
|  |                     )} | ||||||
|  |                   </paper-tooltip> | ||||||
|  |                 </div>` | ||||||
|  |               : `${this.hass.localize( | ||||||
|  |                   "ui.panel.config.devices.picker.filter.hidden_devices", | ||||||
|  |                   "number", | ||||||
|  |                   this._numHiddenDevices | ||||||
|  |                 )}`} | ||||||
|  |             <mwc-button @click=${this._showAll} | ||||||
|  |               >${this.hass.localize( | ||||||
|  |                 "ui.panel.config.devices.picker.filter.show_all" | ||||||
|  |               )}</mwc-button | ||||||
|  |             > | ||||||
|  |           </div>` | ||||||
|  |         : ""} | ||||||
|  |       <ha-button-menu corner="BOTTOM_START" multi> | ||||||
|  |         <mwc-icon-button | ||||||
|  |           slot="trigger" | ||||||
|  |           .label=${this.hass!.localize( | ||||||
|  |             "ui.panel.config.devices.picker.filter.filter" | ||||||
|  |           )} | ||||||
|  |           .title=${this.hass!.localize( | ||||||
|  |             "ui.panel.config.devices.picker.filter.filter" | ||||||
|  |           )} | ||||||
|  |         > | ||||||
|  |           <ha-svg-icon .path=${mdiFilterVariant}></ha-svg-icon> | ||||||
|  |         </mwc-icon-button> | ||||||
|  |         <mwc-list-item | ||||||
|  |           @request-selected="${this._showDisabledChanged}" | ||||||
|  |           graphic="control" | ||||||
|  |           .selected=${this._showDisabled} | ||||||
|  |         > | ||||||
|  |           <ha-checkbox | ||||||
|  |             slot="graphic" | ||||||
|  |             .checked=${this._showDisabled} | ||||||
|  |           ></ha-checkbox> | ||||||
|  |           ${this.hass!.localize( | ||||||
|  |             "ui.panel.config.devices.picker.filter.show_disabled" | ||||||
|  |           )} | ||||||
|  |         </mwc-list-item> | ||||||
|  |       </ha-button-menu> | ||||||
|  |     `; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <hass-tabs-subpage-data-table |       <hass-tabs-subpage-data-table | ||||||
|         .hass=${this.hass} |         .hass=${this.hass} | ||||||
| @@ -295,23 +469,33 @@ export class HaConfigDeviceDashboard extends LitElement { | |||||||
|           : "/config"} |           : "/config"} | ||||||
|         .tabs=${configSections.integrations} |         .tabs=${configSections.integrations} | ||||||
|         .route=${this.route} |         .route=${this.route} | ||||||
|         .columns=${this._columns(this.narrow)} |         .columns=${this._columns(this.narrow, this._showDisabled)} | ||||||
|         .data=${this._devices( |         .data=${devicesOutput} | ||||||
|           this.devices, |         .filter=${this._filter} | ||||||
|           this.entries, |  | ||||||
|           this.entities, |  | ||||||
|           this.areas, |  | ||||||
|           this._searchParms, |  | ||||||
|           this.hass.localize |  | ||||||
|         )} |  | ||||||
|         .activeFilters=${this._activeFilters( |  | ||||||
|           this.entries, |  | ||||||
|           this._searchParms, |  | ||||||
|           this.hass.localize |  | ||||||
|         )} |  | ||||||
|         @row-click=${this._handleRowClicked} |         @row-click=${this._handleRowClicked} | ||||||
|         clickable |         clickable | ||||||
|  |         .hasFab=${includeZHAFab} | ||||||
|       > |       > | ||||||
|  |         ${includeZHAFab | ||||||
|  |           ? html`<a href="/config/zha/add" slot="fab"> | ||||||
|  |               <ha-fab | ||||||
|  |                 .label=${this.hass.localize("ui.panel.config.zha.add_device")} | ||||||
|  |                 extended | ||||||
|  |                 ?rtl=${computeRTL(this.hass)} | ||||||
|  |               > | ||||||
|  |                 <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|  |               </ha-fab> | ||||||
|  |             </a>` | ||||||
|  |           : html``} | ||||||
|  |         <div | ||||||
|  |           class=${classMap({ | ||||||
|  |             "search-toolbar": this.narrow, | ||||||
|  |             "table-header": !this.narrow, | ||||||
|  |           })} | ||||||
|  |           slot="header" | ||||||
|  |         > | ||||||
|  |           ${headerToolbar} | ||||||
|  |         </div> | ||||||
|       </hass-tabs-subpage-data-table> |       </hass-tabs-subpage-data-table> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| @@ -342,6 +526,136 @@ export class HaConfigDeviceDashboard extends LitElement { | |||||||
|     const deviceId = ev.detail.id; |     const deviceId = ev.detail.id; | ||||||
|     navigate(this, `/config/devices/device/${deviceId}`); |     navigate(this, `/config/devices/device/${deviceId}`); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _showDisabledChanged(ev: CustomEvent<RequestSelectedDetail>) { | ||||||
|  |     if (ev.detail.source !== "property") { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this._showDisabled = ev.detail.selected; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _handleSearchChange(ev: CustomEvent) { | ||||||
|  |     this._filter = ev.detail.value; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _clearFilter() { | ||||||
|  |     navigate(this, window.location.pathname, true); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _showAll() { | ||||||
|  |     this._showDisabled = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static get styles(): CSSResult[] { | ||||||
|  |     return [ | ||||||
|  |       haStyle, | ||||||
|  |       css` | ||||||
|  |         hass-loading-screen { | ||||||
|  |           --app-header-background-color: var(--sidebar-background-color); | ||||||
|  |           --app-header-text-color: var(--sidebar-text-color); | ||||||
|  |         } | ||||||
|  |         a { | ||||||
|  |           color: var(--primary-color); | ||||||
|  |         } | ||||||
|  |         h2 { | ||||||
|  |           margin-top: 0; | ||||||
|  |           font-family: var(--paper-font-headline_-_font-family); | ||||||
|  |           -webkit-font-smoothing: var( | ||||||
|  |             --paper-font-headline_-_-webkit-font-smoothing | ||||||
|  |           ); | ||||||
|  |           font-size: var(--paper-font-headline_-_font-size); | ||||||
|  |           font-weight: var(--paper-font-headline_-_font-weight); | ||||||
|  |           letter-spacing: var(--paper-font-headline_-_letter-spacing); | ||||||
|  |           line-height: var(--paper-font-headline_-_line-height); | ||||||
|  |           opacity: var(--dark-primary-opacity); | ||||||
|  |         } | ||||||
|  |         p { | ||||||
|  |           font-family: var(--paper-font-subhead_-_font-family); | ||||||
|  |           -webkit-font-smoothing: var( | ||||||
|  |             --paper-font-subhead_-_-webkit-font-smoothing | ||||||
|  |           ); | ||||||
|  |           font-weight: var(--paper-font-subhead_-_font-weight); | ||||||
|  |           line-height: var(--paper-font-subhead_-_line-height); | ||||||
|  |         } | ||||||
|  |         ha-data-table { | ||||||
|  |           width: 100%; | ||||||
|  |           --data-table-border-width: 0; | ||||||
|  |         } | ||||||
|  |         :host(:not([narrow])) ha-data-table { | ||||||
|  |           height: calc(100vh - 1px - var(--header-height)); | ||||||
|  |           display: block; | ||||||
|  |         } | ||||||
|  |         ha-button-menu { | ||||||
|  |           margin-right: 8px; | ||||||
|  |         } | ||||||
|  |         .table-header { | ||||||
|  |           display: flex; | ||||||
|  |           justify-content: space-between; | ||||||
|  |           align-items: center; | ||||||
|  |           border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12); | ||||||
|  |         } | ||||||
|  |         search-input { | ||||||
|  |           margin-left: 16px; | ||||||
|  |           flex-grow: 1; | ||||||
|  |           position: relative; | ||||||
|  |           top: 2px; | ||||||
|  |         } | ||||||
|  |         .search-toolbar search-input { | ||||||
|  |           margin-left: 8px; | ||||||
|  |           top: 1px; | ||||||
|  |         } | ||||||
|  |         .search-toolbar { | ||||||
|  |           display: flex; | ||||||
|  |           justify-content: space-between; | ||||||
|  |           align-items: center; | ||||||
|  |           color: var(--secondary-text-color); | ||||||
|  |         } | ||||||
|  |         .search-toolbar ha-button-menu { | ||||||
|  |           position: static; | ||||||
|  |         } | ||||||
|  |         .selected-txt { | ||||||
|  |           font-weight: bold; | ||||||
|  |           padding-left: 16px; | ||||||
|  |         } | ||||||
|  |         .table-header .selected-txt { | ||||||
|  |           margin-top: 20px; | ||||||
|  |         } | ||||||
|  |         .search-toolbar .selected-txt { | ||||||
|  |           font-size: 16px; | ||||||
|  |         } | ||||||
|  |         .header-btns > mwc-button, | ||||||
|  |         .header-btns > ha-icon-button { | ||||||
|  |           margin: 8px; | ||||||
|  |         } | ||||||
|  |         .active-filters { | ||||||
|  |           color: var(--primary-text-color); | ||||||
|  |           position: relative; | ||||||
|  |           display: flex; | ||||||
|  |           align-items: center; | ||||||
|  |           padding: 2px 2px 2px 8px; | ||||||
|  |           margin-left: 4px; | ||||||
|  |           font-size: 14px; | ||||||
|  |         } | ||||||
|  |         .active-filters ha-icon { | ||||||
|  |           color: var(--primary-color); | ||||||
|  |         } | ||||||
|  |         .active-filters mwc-button { | ||||||
|  |           margin-left: 8px; | ||||||
|  |         } | ||||||
|  |         .active-filters::before { | ||||||
|  |           background-color: var(--primary-color); | ||||||
|  |           opacity: 0.12; | ||||||
|  |           border-radius: 4px; | ||||||
|  |           position: absolute; | ||||||
|  |           top: 0; | ||||||
|  |           right: 0; | ||||||
|  |           bottom: 0; | ||||||
|  |           left: 0; | ||||||
|  |           content: ""; | ||||||
|  |         } | ||||||
|  |       `, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -20,9 +20,16 @@ import { | |||||||
| import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; | import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; | ||||||
| import type { PolymerChangedEvent } from "../../../polymer-types"; | import type { PolymerChangedEvent } from "../../../polymer-types"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
|  | import "../../../components/ha-area-picker"; | ||||||
|  | import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||||
|  | import { | ||||||
|  |   DeviceRegistryEntry, | ||||||
|  |   subscribeDeviceRegistry, | ||||||
|  | } from "../../../data/device_registry"; | ||||||
|  | import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; | ||||||
|  |  | ||||||
| @customElement("ha-registry-basic-editor") | @customElement("ha-registry-basic-editor") | ||||||
| export class HaEntityRegistryBasicEditor extends LitElement { | export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|   @property() public entry!: ExtEntityRegistryEntry; |   @property() public entry!: ExtEntityRegistryEntry; | ||||||
| @@ -31,16 +38,26 @@ export class HaEntityRegistryBasicEditor extends LitElement { | |||||||
|  |  | ||||||
|   @internalProperty() private _entityId!: string; |   @internalProperty() private _entityId!: string; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _areaId?: string; | ||||||
|  |  | ||||||
|   @internalProperty() private _disabledBy!: string | null; |   @internalProperty() private _disabledBy!: string | null; | ||||||
|  |  | ||||||
|  |   private _deviceLookup?: Record<string, DeviceRegistryEntry>; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _device?: DeviceRegistryEntry; | ||||||
|  |  | ||||||
|   @internalProperty() private _submitting?: boolean; |   @internalProperty() private _submitting?: boolean; | ||||||
|  |  | ||||||
|   public async updateEntry(): Promise<void> { |   public async updateEntry(): Promise<void> { | ||||||
|     this._submitting = true; |     this._submitting = true; | ||||||
|     const params: Partial<EntityRegistryEntryUpdateParams> = { |     const params: Partial<EntityRegistryEntryUpdateParams> = { | ||||||
|       new_entity_id: this._entityId.trim(), |       new_entity_id: this._entityId.trim(), | ||||||
|  |       area_id: this._areaId || null, | ||||||
|     }; |     }; | ||||||
|     if (this._disabledBy === null || this._disabledBy === "user") { |     if ( | ||||||
|  |       this.entry.disabled_by !== this._disabledBy && | ||||||
|  |       (this._disabledBy === null || this._disabledBy === "user") | ||||||
|  |     ) { | ||||||
|       params.disabled_by = this._disabledBy; |       params.disabled_by = this._disabledBy; | ||||||
|     } |     } | ||||||
|     try { |     try { | ||||||
| @@ -70,6 +87,20 @@ export class HaEntityRegistryBasicEditor extends LitElement { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public hassSubscribe(): UnsubscribeFunc[] { | ||||||
|  |     return [ | ||||||
|  |       subscribeDeviceRegistry(this.hass.connection!, (devices) => { | ||||||
|  |         this._deviceLookup = {}; | ||||||
|  |         for (const device of devices) { | ||||||
|  |           this._deviceLookup[device.id] = device; | ||||||
|  |         } | ||||||
|  |         if (!this._device && this.entry.device_id) { | ||||||
|  |           this._device = this._deviceLookup[this.entry.device_id]; | ||||||
|  |         } | ||||||
|  |       }), | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   protected updated(changedProperties: PropertyValues) { |   protected updated(changedProperties: PropertyValues) { | ||||||
|     super.updated(changedProperties); |     super.updated(changedProperties); | ||||||
|     if (!changedProperties.has("entry")) { |     if (!changedProperties.has("entry")) { | ||||||
| @@ -79,6 +110,11 @@ export class HaEntityRegistryBasicEditor extends LitElement { | |||||||
|       this._origEntityId = this.entry.entity_id; |       this._origEntityId = this.entry.entity_id; | ||||||
|       this._entityId = this.entry.entity_id; |       this._entityId = this.entry.entity_id; | ||||||
|       this._disabledBy = this.entry.disabled_by; |       this._disabledBy = this.entry.disabled_by; | ||||||
|  |       this._areaId = this.entry.area_id; | ||||||
|  |       this._device = | ||||||
|  |         this.entry.device_id && this._deviceLookup | ||||||
|  |           ? this._deviceLookup[this.entry.device_id] | ||||||
|  |           : undefined; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -105,6 +141,12 @@ export class HaEntityRegistryBasicEditor extends LitElement { | |||||||
|         .invalid=${invalidDomainUpdate} |         .invalid=${invalidDomainUpdate} | ||||||
|         .disabled=${this._submitting} |         .disabled=${this._submitting} | ||||||
|       ></paper-input> |       ></paper-input> | ||||||
|  |       <ha-area-picker | ||||||
|  |         .hass=${this.hass} | ||||||
|  |         .value=${this._areaId} | ||||||
|  |         .placeholder=${this._device?.area_id} | ||||||
|  |         @value-changed=${this._areaPicked} | ||||||
|  |       ></ha-area-picker> | ||||||
|       <div class="row"> |       <div class="row"> | ||||||
|         <ha-switch |         <ha-switch | ||||||
|           .checked=${!this._disabledBy} |           .checked=${!this._disabledBy} | ||||||
| @@ -139,6 +181,10 @@ export class HaEntityRegistryBasicEditor extends LitElement { | |||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _areaPicked(ev: CustomEvent) { | ||||||
|  |     this._areaId = ev.detail.value; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _entityIdChanged(ev: PolymerChangedEvent<string>): void { |   private _entityIdChanged(ev: PolymerChangedEvent<string>): void { | ||||||
|     this._entityId = ev.detail.value; |     this._entityId = ev.detail.value; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import "@material/mwc-button/mwc-button"; | import "@material/mwc-button/mwc-button"; | ||||||
| import "@polymer/paper-input/paper-input"; | import "@polymer/paper-input/paper-input"; | ||||||
| import { HassEntity } from "home-assistant-js-websocket"; | import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResult, |   CSSResult, | ||||||
| @@ -31,9 +31,18 @@ import type { PolymerChangedEvent } from "../../../polymer-types"; | |||||||
| import { haStyle } from "../../../resources/styles"; | import { haStyle } from "../../../resources/styles"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
| import { domainIcon } from "../../../common/entity/domain_icon"; | import { domainIcon } from "../../../common/entity/domain_icon"; | ||||||
|  | import "../../../components/ha-area-picker"; | ||||||
|  | import { | ||||||
|  |   DeviceRegistryEntry, | ||||||
|  |   subscribeDeviceRegistry, | ||||||
|  |   updateDeviceRegistryEntry, | ||||||
|  | } from "../../../data/device_registry"; | ||||||
|  | import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; | ||||||
|  | import "../../../components/ha-expansion-panel"; | ||||||
|  | import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; | ||||||
|  |  | ||||||
| @customElement("entity-registry-settings") | @customElement("entity-registry-settings") | ||||||
| export class EntityRegistrySettings extends LitElement { | export class EntityRegistrySettings extends SubscribeMixin(LitElement) { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|   @property() public entry!: ExtEntityRegistryEntry; |   @property() public entry!: ExtEntityRegistryEntry; | ||||||
| @@ -44,14 +53,34 @@ export class EntityRegistrySettings extends LitElement { | |||||||
|  |  | ||||||
|   @internalProperty() private _entityId!: string; |   @internalProperty() private _entityId!: string; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _areaId?: string | null; | ||||||
|  |  | ||||||
|   @internalProperty() private _disabledBy!: string | null; |   @internalProperty() private _disabledBy!: string | null; | ||||||
|  |  | ||||||
|  |   private _deviceLookup?: Record<string, DeviceRegistryEntry>; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _device?: DeviceRegistryEntry; | ||||||
|  |  | ||||||
|   @internalProperty() private _error?: string; |   @internalProperty() private _error?: string; | ||||||
|  |  | ||||||
|   @internalProperty() private _submitting?: boolean; |   @internalProperty() private _submitting?: boolean; | ||||||
|  |  | ||||||
|   private _origEntityId!: string; |   private _origEntityId!: string; | ||||||
|  |  | ||||||
|  |   public hassSubscribe(): UnsubscribeFunc[] { | ||||||
|  |     return [ | ||||||
|  |       subscribeDeviceRegistry(this.hass.connection!, (devices) => { | ||||||
|  |         this._deviceLookup = {}; | ||||||
|  |         for (const device of devices) { | ||||||
|  |           this._deviceLookup[device.id] = device; | ||||||
|  |         } | ||||||
|  |         if (this.entry.device_id) { | ||||||
|  |           this._device = this._deviceLookup[this.entry.device_id]; | ||||||
|  |         } | ||||||
|  |       }), | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   protected updated(changedProperties: PropertyValues) { |   protected updated(changedProperties: PropertyValues) { | ||||||
|     super.updated(changedProperties); |     super.updated(changedProperties); | ||||||
|     if (changedProperties.has("entry")) { |     if (changedProperties.has("entry")) { | ||||||
| @@ -59,8 +88,13 @@ export class EntityRegistrySettings extends LitElement { | |||||||
|       this._name = this.entry.name || ""; |       this._name = this.entry.name || ""; | ||||||
|       this._icon = this.entry.icon || ""; |       this._icon = this.entry.icon || ""; | ||||||
|       this._origEntityId = this.entry.entity_id; |       this._origEntityId = this.entry.entity_id; | ||||||
|  |       this._areaId = this.entry.area_id; | ||||||
|       this._entityId = this.entry.entity_id; |       this._entityId = this.entry.entity_id; | ||||||
|       this._disabledBy = this.entry.disabled_by; |       this._disabledBy = this.entry.disabled_by; | ||||||
|  |       this._device = | ||||||
|  |         this.entry.device_id && this._deviceLookup | ||||||
|  |           ? this._deviceLookup[this.entry.device_id] | ||||||
|  |           : undefined; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -77,10 +111,19 @@ export class EntityRegistrySettings extends LitElement { | |||||||
|     return html` |     return html` | ||||||
|       ${!stateObj |       ${!stateObj | ||||||
|         ? html` |         ? html` | ||||||
|             <div class="container"> |             <div class="container warning"> | ||||||
|               ${this.hass!.localize( |               ${this.hass!.localize( | ||||||
|                 "ui.dialogs.entity_registry.editor.unavailable" |                 "ui.dialogs.entity_registry.editor.unavailable" | ||||||
|               )} |               )} | ||||||
|  |               ${this._device?.disabled_by | ||||||
|  |                 ? html`<br />${this.hass!.localize( | ||||||
|  |                       "ui.dialogs.entity_registry.editor.device_disabled" | ||||||
|  |                     )}<br /><mwc-button @click=${this._openDeviceSettings}> | ||||||
|  |                       ${this.hass!.localize( | ||||||
|  |                         "ui.dialogs.entity_registry.editor.open_device_settings" | ||||||
|  |                       )} | ||||||
|  |                     </mwc-button>` | ||||||
|  |                 : ""} | ||||||
|             </div> |             </div> | ||||||
|           ` |           ` | ||||||
|         : ""} |         : ""} | ||||||
| @@ -117,9 +160,17 @@ export class EntityRegistrySettings extends LitElement { | |||||||
|           .invalid=${invalidDomainUpdate} |           .invalid=${invalidDomainUpdate} | ||||||
|           .disabled=${this._submitting} |           .disabled=${this._submitting} | ||||||
|         ></paper-input> |         ></paper-input> | ||||||
|  |         ${!this.entry.device_id | ||||||
|  |           ? html`<ha-area-picker | ||||||
|  |               .hass=${this.hass} | ||||||
|  |               .value=${this._areaId} | ||||||
|  |               @value-changed=${this._areaPicked} | ||||||
|  |             ></ha-area-picker>` | ||||||
|  |           : ""} | ||||||
|         <div class="row"> |         <div class="row"> | ||||||
|           <ha-switch |           <ha-switch | ||||||
|             .checked=${!this._disabledBy} |             .checked=${!this._disabledBy} | ||||||
|  |             .disabled=${this._device?.disabled_by} | ||||||
|             @change=${this._disabledByChanged} |             @change=${this._disabledByChanged} | ||||||
|           > |           > | ||||||
|           </ha-switch> |           </ha-switch> | ||||||
| @@ -148,6 +199,31 @@ export class EntityRegistrySettings extends LitElement { | |||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|  |         ${this.entry.device_id | ||||||
|  |           ? html`<ha-expansion-panel .header=${"Advanced"}> | ||||||
|  |               <p> | ||||||
|  |                 By default the entities of a device are in the same area as the | ||||||
|  |                 device. If you change the area of this entity, it will no longer | ||||||
|  |                 follow the area of the device. | ||||||
|  |               </p> | ||||||
|  |               ${this._areaId | ||||||
|  |                 ? html`<mwc-button @click=${this._clearArea} | ||||||
|  |                     >Follow device area</mwc-button | ||||||
|  |                   >` | ||||||
|  |                 : this._device | ||||||
|  |                 ? html`<mwc-button @click=${this._openDeviceSettings} | ||||||
|  |                     >Change device area</mwc-button | ||||||
|  |                   >` | ||||||
|  |                 : ""} | ||||||
|  |               <ha-area-picker | ||||||
|  |                 .hass=${this.hass} | ||||||
|  |                 .value=${this._areaId} | ||||||
|  |                 .placeholder=${this._device?.area_id} | ||||||
|  |                 @value-changed=${this._areaPicked} | ||||||
|  |               ></ha-area-picker | ||||||
|  |             ></ha-expansion-panel>` | ||||||
|  |           : ""} | ||||||
|       </div> |       </div> | ||||||
|       <div class="buttons"> |       <div class="buttons"> | ||||||
|         <mwc-button |         <mwc-button | ||||||
| @@ -183,14 +259,37 @@ export class EntityRegistrySettings extends LitElement { | |||||||
|     this._entityId = ev.detail.value; |     this._entityId = ev.detail.value; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _areaPicked(ev: CustomEvent) { | ||||||
|  |     this._error = undefined; | ||||||
|  |     this._areaId = ev.detail.value; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _clearArea() { | ||||||
|  |     this._error = undefined; | ||||||
|  |     this._areaId = null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _openDeviceSettings() { | ||||||
|  |     showDeviceRegistryDetailDialog(this, { | ||||||
|  |       device: this._device!, | ||||||
|  |       updateEntry: async (updates) => { | ||||||
|  |         await updateDeviceRegistryEntry(this.hass, this._device!.id, updates); | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private async _updateEntry(): Promise<void> { |   private async _updateEntry(): Promise<void> { | ||||||
|     this._submitting = true; |     this._submitting = true; | ||||||
|     const params: Partial<EntityRegistryEntryUpdateParams> = { |     const params: Partial<EntityRegistryEntryUpdateParams> = { | ||||||
|       name: this._name.trim() || null, |       name: this._name.trim() || null, | ||||||
|       icon: this._icon.trim() || null, |       icon: this._icon.trim() || null, | ||||||
|  |       area_id: this._areaId || null, | ||||||
|       new_entity_id: this._entityId.trim(), |       new_entity_id: this._entityId.trim(), | ||||||
|     }; |     }; | ||||||
|     if (this._disabledBy === null || this._disabledBy === "user") { |     if ( | ||||||
|  |       this.entry.disabled_by !== this._disabledBy && | ||||||
|  |       (this._disabledBy === null || this._disabledBy === "user") | ||||||
|  |     ) { | ||||||
|       params.disabled_by = this._disabledBy; |       params.disabled_by = this._disabledBy; | ||||||
|     } |     } | ||||||
|     try { |     try { | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import "@material/mwc-list/mwc-list-item"; | import "@material/mwc-list/mwc-list-item"; | ||||||
| import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; | import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; | ||||||
| import { mdiFilterVariant } from "@mdi/js"; | import { mdiFilterVariant, mdiPlus } from "@mdi/js"; | ||||||
| import "@polymer/paper-checkbox/paper-checkbox"; | import "@polymer/paper-checkbox/paper-checkbox"; | ||||||
| import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; | import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; | ||||||
| import "@polymer/paper-item/paper-icon-item"; | import "@polymer/paper-item/paper-icon-item"; | ||||||
| @@ -62,6 +62,15 @@ import { | |||||||
| } from "./show-dialog-entity-editor"; | } from "./show-dialog-entity-editor"; | ||||||
| import { haStyle } from "../../../resources/styles"; | import { haStyle } from "../../../resources/styles"; | ||||||
| import { UNAVAILABLE } from "../../../data/entity"; | import { UNAVAILABLE } from "../../../data/entity"; | ||||||
|  | import { | ||||||
|  |   DeviceRegistryEntry, | ||||||
|  |   subscribeDeviceRegistry, | ||||||
|  | } from "../../../data/device_registry"; | ||||||
|  | import { | ||||||
|  |   AreaRegistryEntry, | ||||||
|  |   subscribeAreaRegistry, | ||||||
|  | } from "../../../data/area_registry"; | ||||||
|  | import { computeRTL } from "../../../common/util/compute_rtl"; | ||||||
|  |  | ||||||
| export interface StateEntity extends EntityRegistryEntry { | export interface StateEntity extends EntityRegistryEntry { | ||||||
|   readonly?: boolean; |   readonly?: boolean; | ||||||
| @@ -73,6 +82,7 @@ export interface EntityRow extends StateEntity { | |||||||
|   unavailable: boolean; |   unavailable: boolean; | ||||||
|   restored: boolean; |   restored: boolean; | ||||||
|   status: string; |   status: string; | ||||||
|  |   area?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| @customElement("ha-config-entities") | @customElement("ha-config-entities") | ||||||
| @@ -87,6 +97,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @internalProperty() private _entities?: EntityRegistryEntry[]; |   @internalProperty() private _entities?: EntityRegistryEntry[]; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _devices?: DeviceRegistryEntry[]; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _areas: AreaRegistryEntry[] = []; | ||||||
|  |  | ||||||
|   @internalProperty() private _stateEntities: StateEntity[] = []; |   @internalProperty() private _stateEntities: StateEntity[] = []; | ||||||
|  |  | ||||||
|   @property() public _entries?: ConfigEntry[]; |   @property() public _entries?: ConfigEntry[]; | ||||||
| @@ -175,9 +189,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | |||||||
|           ? (name, entity: any) => |           ? (name, entity: any) => | ||||||
|               html` |               html` | ||||||
|                 ${name}<br /> |                 ${name}<br /> | ||||||
|                 ${entity.entity_id} | |                 <div class="secondary"> | ||||||
|                 ${this.hass.localize(`component.${entity.platform}.title`) || |                   ${entity.entity_id} | | ||||||
|                 entity.platform} |                   ${this.hass.localize(`component.${entity.platform}.title`) || | ||||||
|  |                   entity.platform} | ||||||
|  |                 </div> | ||||||
|               ` |               ` | ||||||
|           : undefined, |           : undefined, | ||||||
|       }, |       }, | ||||||
| @@ -201,6 +217,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | |||||||
|         template: (platform) => |         template: (platform) => | ||||||
|           this.hass.localize(`component.${platform}.title`) || platform, |           this.hass.localize(`component.${platform}.title`) || platform, | ||||||
|       }, |       }, | ||||||
|  |       area: { | ||||||
|  |         title: this.hass.localize( | ||||||
|  |           "ui.panel.config.entities.picker.headers.area" | ||||||
|  |         ), | ||||||
|  |         sortable: true, | ||||||
|  |         hidden: narrow, | ||||||
|  |         filterable: true, | ||||||
|  |         width: "15%", | ||||||
|  |       }, | ||||||
|       status: { |       status: { | ||||||
|         title: this.hass.localize( |         title: this.hass.localize( | ||||||
|           "ui.panel.config.entities.picker.headers.status" |           "ui.panel.config.entities.picker.headers.status" | ||||||
| @@ -252,48 +277,87 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | |||||||
|     }) |     }) | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   private _filteredEntities = memoize( |   private _filteredEntitiesAndDomains = memoize( | ||||||
|     ( |     ( | ||||||
|       entities: EntityRegistryEntry[], |       entities: EntityRegistryEntry[], | ||||||
|  |       devices: DeviceRegistryEntry[] | undefined, | ||||||
|  |       areas: AreaRegistryEntry[] | undefined, | ||||||
|       stateEntities: StateEntity[], |       stateEntities: StateEntity[], | ||||||
|       filters: URLSearchParams, |       filters: URLSearchParams, | ||||||
|       showDisabled: boolean, |       showDisabled: boolean, | ||||||
|       showUnavailable: boolean, |       showUnavailable: boolean, | ||||||
|       showReadOnly: boolean |       showReadOnly: boolean, | ||||||
|     ): EntityRow[] => { |       entries?: ConfigEntry[] | ||||||
|  |     ) => { | ||||||
|       const result: EntityRow[] = []; |       const result: EntityRow[] = []; | ||||||
|  |  | ||||||
|       // If nothing gets filtered, this is our correct count of entities |       // If nothing gets filtered, this is our correct count of entities | ||||||
|       let startLength = entities.length + stateEntities.length; |       let startLength = entities.length + stateEntities.length; | ||||||
|  |  | ||||||
|       entities = showReadOnly ? entities.concat(stateEntities) : entities; |       const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; | ||||||
|  |       const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {}; | ||||||
|  |  | ||||||
|  |       if (areas) { | ||||||
|  |         for (const area of areas) { | ||||||
|  |           areaLookup[area.area_id] = area; | ||||||
|  |         } | ||||||
|  |         if (devices) { | ||||||
|  |           for (const device of devices) { | ||||||
|  |             deviceLookup[device.id] = device; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       entities.forEach((entity) => { | ||||||
|  |         return entity; | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       let filteredEntities = showReadOnly | ||||||
|  |         ? entities.concat(stateEntities) | ||||||
|  |         : entities; | ||||||
|  |  | ||||||
|  |       const filteredDomains: string[] = []; | ||||||
|  |  | ||||||
|       filters.forEach((value, key) => { |       filters.forEach((value, key) => { | ||||||
|         switch (key) { |         if (key === "config_entry") { | ||||||
|           case "config_entry": |           filteredEntities = filteredEntities.filter( | ||||||
|             entities = entities.filter( |             (entity) => entity.config_entry_id === value | ||||||
|  |           ); | ||||||
|  |           // If we have an active filter and `showReadOnly` is true, the length of `entities` is correct. | ||||||
|  |           // If however, the read-only entities were not added before, we need to check how many would | ||||||
|  |           // have matched the active filter and add that number to the count. | ||||||
|  |           startLength = filteredEntities.length; | ||||||
|  |           if (!showReadOnly) { | ||||||
|  |             startLength += stateEntities.filter( | ||||||
|               (entity) => entity.config_entry_id === value |               (entity) => entity.config_entry_id === value | ||||||
|             ); |             ).length; | ||||||
|             // If we have an active filter and `showReadOnly` is true, the length of `entities` is correct. |           } | ||||||
|             // If however, the read-only entities were not added before, we need to check how many would |  | ||||||
|             // have matched the active filter and add that number to the count. |           if (!entries) { | ||||||
|             startLength = entities.length; |             this._loadConfigEntries(); | ||||||
|             if (!showReadOnly) { |             return; | ||||||
|               startLength += stateEntities.filter( |           } | ||||||
|                 (entity) => entity.config_entry_id === value |  | ||||||
|               ).length; |           const configEntry = entries.find((entry) => entry.entry_id === value); | ||||||
|             } |  | ||||||
|             break; |           if (configEntry) { | ||||||
|  |             filteredDomains.push(configEntry.domain); | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       if (!showDisabled) { |       if (!showDisabled) { | ||||||
|         entities = entities.filter((entity) => !entity.disabled_by); |         filteredEntities = filteredEntities.filter( | ||||||
|  |           (entity) => !entity.disabled_by | ||||||
|  |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       for (const entry of entities) { |       for (const entry of filteredEntities) { | ||||||
|         const entity = this.hass.states[entry.entity_id]; |         const entity = this.hass.states[entry.entity_id]; | ||||||
|         const unavailable = entity?.state === UNAVAILABLE; |         const unavailable = entity?.state === UNAVAILABLE; | ||||||
|         const restored = entity?.attributes.restored; |         const restored = entity?.attributes.restored; | ||||||
|  |         const areaId = entry.area_id ?? deviceLookup[entry.device_id!]?.area_id; | ||||||
|  |         const area = areaId ? areaLookup[areaId] : undefined; | ||||||
|  |  | ||||||
|         if (!showUnavailable && unavailable) { |         if (!showUnavailable && unavailable) { | ||||||
|           continue; |           continue; | ||||||
| @@ -309,6 +373,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | |||||||
|             this.hass.localize("state.default.unavailable"), |             this.hass.localize("state.default.unavailable"), | ||||||
|           unavailable, |           unavailable, | ||||||
|           restored, |           restored, | ||||||
|  |           area: area ? area.name : undefined, | ||||||
|           status: restored |           status: restored | ||||||
|             ? this.hass.localize( |             ? this.hass.localize( | ||||||
|                 "ui.panel.config.entities.picker.status.restored" |                 "ui.panel.config.entities.picker.status.restored" | ||||||
| @@ -326,7 +391,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       this._numHiddenEntities = startLength - result.length; |       this._numHiddenEntities = startLength - result.length; | ||||||
|       return result; |       return { filteredEntities: result, filteredDomains: filteredDomains }; | ||||||
|     } |     } | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
| @@ -345,6 +410,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | |||||||
|       subscribeEntityRegistry(this.hass.connection!, (entities) => { |       subscribeEntityRegistry(this.hass.connection!, (entities) => { | ||||||
|         this._entities = entities; |         this._entities = entities; | ||||||
|       }), |       }), | ||||||
|  |       subscribeDeviceRegistry(this.hass.connection!, (devices) => { | ||||||
|  |         this._devices = devices; | ||||||
|  |       }), | ||||||
|  |       subscribeAreaRegistry(this.hass.connection, (areas) => { | ||||||
|  |         this._areas = areas; | ||||||
|  |       }), | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -370,15 +441,22 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | |||||||
|       this._entries |       this._entries | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const entityData = this._filteredEntities( |     const { | ||||||
|  |       filteredEntities, | ||||||
|  |       filteredDomains, | ||||||
|  |     } = this._filteredEntitiesAndDomains( | ||||||
|       this._entities, |       this._entities, | ||||||
|  |       this._devices, | ||||||
|  |       this._areas, | ||||||
|       this._stateEntities, |       this._stateEntities, | ||||||
|       this._searchParms, |       this._searchParms, | ||||||
|       this._showDisabled, |       this._showDisabled, | ||||||
|       this._showUnavailable, |       this._showUnavailable, | ||||||
|       this._showReadOnly |       this._showReadOnly, | ||||||
|  |       this._entries | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     const includeZHAFab = filteredDomains.includes("zha"); | ||||||
|     const headerToolbar = this._selectedEntities.length |     const headerToolbar = this._selectedEntities.length | ||||||
|       ? html` |       ? html` | ||||||
|           <p class="selected-txt"> |           <p class="selected-txt"> | ||||||
| @@ -584,13 +662,14 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | |||||||
|         .route=${this.route} |         .route=${this.route} | ||||||
|         .tabs=${configSections.integrations} |         .tabs=${configSections.integrations} | ||||||
|         .columns=${this._columns(this.narrow, this.hass.language)} |         .columns=${this._columns(this.narrow, this.hass.language)} | ||||||
|         .data=${entityData} |         .data=${filteredEntities} | ||||||
|         .filter=${this._filter} |         .filter=${this._filter} | ||||||
|         selectable |         selectable | ||||||
|         clickable |         clickable | ||||||
|         @selection-changed=${this._handleSelectionChanged} |         @selection-changed=${this._handleSelectionChanged} | ||||||
|         @row-click=${this._openEditEntry} |         @row-click=${this._openEditEntry} | ||||||
|         id="entity_id" |         id="entity_id" | ||||||
|  |         .hasFab=${includeZHAFab} | ||||||
|       > |       > | ||||||
|         <div |         <div | ||||||
|           class=${classMap({ |           class=${classMap({ | ||||||
| @@ -601,6 +680,17 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | |||||||
|         > |         > | ||||||
|           ${headerToolbar} |           ${headerToolbar} | ||||||
|         </div> |         </div> | ||||||
|  |         ${includeZHAFab | ||||||
|  |           ? html`<a href="/config/zha/add" slot="fab"> | ||||||
|  |               <ha-fab | ||||||
|  |                 .label=${this.hass.localize("ui.panel.config.zha.add_device")} | ||||||
|  |                 extended | ||||||
|  |                 ?rtl=${computeRTL(this.hass)} | ||||||
|  |               > | ||||||
|  |                 <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|  |               </ha-fab> | ||||||
|  |             </a>` | ||||||
|  |           : html``} | ||||||
|       </hass-tabs-subpage-data-table> |       </hass-tabs-subpage-data-table> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -5,6 +5,8 @@ import { classMap } from "lit-html/directives/class-map"; | |||||||
| export class HaConfigSection extends LitElement { | export class HaConfigSection extends LitElement { | ||||||
|   @property() public isWide = false; |   @property() public isWide = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public vertical = false; | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|     return html` |     return html` | ||||||
|       <div |       <div | ||||||
| @@ -16,8 +18,8 @@ export class HaConfigSection extends LitElement { | |||||||
|         <div |         <div | ||||||
|           class="together layout ${classMap({ |           class="together layout ${classMap({ | ||||||
|             narrow: !this.isWide, |             narrow: !this.isWide, | ||||||
|             vertical: !this.isWide, |             vertical: this.vertical || !this.isWide, | ||||||
|             horizontal: this.isWide, |             horizontal: !this.vertical && this.isWide, | ||||||
|           })}" |           })}" | ||||||
|         > |         > | ||||||
|           <div class="intro"><slot name="introduction"></slot></div> |           <div class="intro"><slot name="introduction"></slot></div> | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "@material/mwc-fab"; | import "../../../components/ha-fab"; | ||||||
| import { mdiPlus } from "@mdi/js"; | import { mdiPlus } from "@mdi/js"; | ||||||
| import "@polymer/paper-checkbox/paper-checkbox"; | import "@polymer/paper-checkbox/paper-checkbox"; | ||||||
| import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; | import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; | ||||||
| @@ -148,7 +148,7 @@ export class HaConfigHelpers extends LitElement { | |||||||
|         .narrow=${this.narrow} |         .narrow=${this.narrow} | ||||||
|         back-path="/config" |         back-path="/config" | ||||||
|         .route=${this.route} |         .route=${this.route} | ||||||
|         .tabs=${configSections.automation} |         .tabs=${configSections.helpers} | ||||||
|         .columns=${this._columns(this.narrow, this.hass.language)} |         .columns=${this._columns(this.narrow, this.hass.language)} | ||||||
|         .data=${this._getItems(this._stateItems)} |         .data=${this._getItems(this._stateItems)} | ||||||
|         @row-click=${this._openEditDialog} |         @row-click=${this._openEditDialog} | ||||||
| @@ -158,7 +158,7 @@ export class HaConfigHelpers extends LitElement { | |||||||
|           "ui.panel.config.helpers.picker.no_helpers" |           "ui.panel.config.helpers.picker.no_helpers" | ||||||
|         )} |         )} | ||||||
|       > |       > | ||||||
|         <mwc-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           .label=${this.hass.localize( |           .label=${this.hass.localize( | ||||||
|             "ui.panel.config.helpers.picker.add_helper" |             "ui.panel.config.helpers.picker.add_helper" | ||||||
| @@ -167,7 +167,7 @@ export class HaConfigHelpers extends LitElement { | |||||||
|           @click=${this._createHelpler} |           @click=${this._createHelpler} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|         </mwc-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage-data-table> |       </hass-tabs-subpage-data-table> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "@material/mwc-fab"; | import "../../../components/ha-fab"; | ||||||
| import "@material/mwc-icon-button"; | import "@material/mwc-icon-button"; | ||||||
| import "@material/mwc-list/mwc-list-item"; | import "@material/mwc-list/mwc-list-item"; | ||||||
| import { mdiDotsVertical, mdiPlus } from "@mdi/js"; | import { mdiDotsVertical, mdiPlus } from "@mdi/js"; | ||||||
| @@ -474,7 +474,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { | |||||||
|               ` |               ` | ||||||
|             : ""} |             : ""} | ||||||
|         </div> |         </div> | ||||||
|         <mwc-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           .label=${this.hass.localize( |           .label=${this.hass.localize( | ||||||
|             "ui.panel.config.integrations.add_integration" |             "ui.panel.config.integrations.add_integration" | ||||||
| @@ -483,7 +483,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { | |||||||
|           @click=${this._createFlow} |           @click=${this._createFlow} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|         </mwc-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage> |       </hass-tabs-subpage> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import "@material/mwc-button/mwc-button"; | import "@material/mwc-button/mwc-button"; | ||||||
| import "@material/mwc-fab"; |  | ||||||
| import { mdiCheckCircle, mdiCircle, mdiCloseCircle, mdiZWave } from "@mdi/js"; | import { mdiCheckCircle, mdiCircle, mdiCloseCircle, mdiZWave } from "@mdi/js"; | ||||||
| import "@polymer/paper-item/paper-icon-item"; | import "@polymer/paper-item/paper-icon-item"; | ||||||
| import "@polymer/paper-item/paper-item-body"; | import "@polymer/paper-item/paper-item-body"; | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import "@material/mwc-button/mwc-button"; | import "@material/mwc-button/mwc-button"; | ||||||
| import "@material/mwc-fab"; |  | ||||||
| import { mdiCheckCircle, mdiCircle, mdiCloseCircle } from "@mdi/js"; | import { mdiCheckCircle, mdiCircle, mdiCloseCircle } from "@mdi/js"; | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import "@material/mwc-button/mwc-button"; | import "@material/mwc-button/mwc-button"; | ||||||
| import "@material/mwc-fab"; |  | ||||||
| import { mdiAlert, mdiCheck } from "@mdi/js"; | import { mdiAlert, mdiCheck } from "@mdi/js"; | ||||||
| import { | import { | ||||||
|   CSSResult, |   CSSResult, | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import "@material/mwc-button/mwc-button"; | import "@material/mwc-button/mwc-button"; | ||||||
| import "@material/mwc-fab"; |  | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResultArray, |   CSSResultArray, | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import "@material/mwc-button/mwc-button"; | import "@material/mwc-button/mwc-button"; | ||||||
| import "@material/mwc-fab"; |  | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResultArray, |   CSSResultArray, | ||||||
|   | |||||||
| @@ -14,13 +14,17 @@ import { | |||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import "../../../../../components/ha-service-description"; | import "../../../../../components/ha-service-description"; | ||||||
| import "@polymer/paper-input/paper-textarea"; | import "@polymer/paper-input/paper-textarea"; | ||||||
| import { ZHADevice } from "../../../../../data/zha"; |  | ||||||
| import "../../../../../layouts/hass-tabs-subpage"; | import "../../../../../layouts/hass-tabs-subpage"; | ||||||
| import { haStyle } from "../../../../../resources/styles"; | import { haStyle } from "../../../../../resources/styles"; | ||||||
| import { HomeAssistant, Route } from "../../../../../types"; | import { HomeAssistant, Route } from "../../../../../types"; | ||||||
| import "./zha-device-card"; | import "./zha-device-pairing-status-card"; | ||||||
| import { zhaTabs } from "./zha-config-dashboard"; | import { zhaTabs } from "./zha-config-dashboard"; | ||||||
| import { IronAutogrowTextareaElement } from "@polymer/iron-autogrow-textarea"; | import { IronAutogrowTextareaElement } from "@polymer/iron-autogrow-textarea"; | ||||||
|  | import { | ||||||
|  |   DEVICE_MESSAGE_TYPES, | ||||||
|  |   LOG_OUTPUT, | ||||||
|  |   ZHADevice, | ||||||
|  | } from "../../../../../data/zha"; | ||||||
|  |  | ||||||
| @customElement("zha-add-devices-page") | @customElement("zha-add-devices-page") | ||||||
| class ZHAAddDevicesPage extends LitElement { | class ZHAAddDevicesPage extends LitElement { | ||||||
| @@ -34,7 +38,10 @@ class ZHAAddDevicesPage extends LitElement { | |||||||
|  |  | ||||||
|   @internalProperty() private _error?: string; |   @internalProperty() private _error?: string; | ||||||
|  |  | ||||||
|   @internalProperty() private _discoveredDevices: ZHADevice[] = []; |   @internalProperty() private _discoveredDevices: Record< | ||||||
|  |     string, | ||||||
|  |     ZHADevice | ||||||
|  |   > = {}; | ||||||
|  |  | ||||||
|   @internalProperty() private _formattedEvents = ""; |   @internalProperty() private _formattedEvents = ""; | ||||||
|  |  | ||||||
| @@ -64,7 +71,7 @@ class ZHAAddDevicesPage extends LitElement { | |||||||
|     super.disconnectedCallback(); |     super.disconnectedCallback(); | ||||||
|     this._unsubscribe(); |     this._unsubscribe(); | ||||||
|     this._error = undefined; |     this._error = undefined; | ||||||
|     this._discoveredDevices = []; |     this._discoveredDevices = {}; | ||||||
|     this._formattedEvents = ""; |     this._formattedEvents = ""; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -115,7 +122,7 @@ class ZHAAddDevicesPage extends LitElement { | |||||||
|         </div> |         </div> | ||||||
|         ${this._error ? html` <div class="error">${this._error}</div> ` : ""} |         ${this._error ? html` <div class="error">${this._error}</div> ` : ""} | ||||||
|         <div class="content"> |         <div class="content"> | ||||||
|           ${this._discoveredDevices.length < 1 |           ${Object.keys(this._discoveredDevices).length < 1 | ||||||
|             ? html` |             ? html` | ||||||
|                 <div class="discovery-text"> |                 <div class="discovery-text"> | ||||||
|                   <h4> |                   <h4> | ||||||
| @@ -133,15 +140,15 @@ class ZHAAddDevicesPage extends LitElement { | |||||||
|                 </div> |                 </div> | ||||||
|               ` |               ` | ||||||
|             : html` |             : html` | ||||||
|                 ${this._discoveredDevices.map( |                 ${Object.values(this._discoveredDevices).map( | ||||||
|                   (device) => html` |                   (device) => html` | ||||||
|                     <zha-device-card |                     <zha-device-pairing-status-card | ||||||
|                       class="card" |                       class="card" | ||||||
|                       .hass=${this.hass} |                       .hass=${this.hass} | ||||||
|                       .device=${device} |                       .device=${device} | ||||||
|                       .narrow=${this.narrow} |                       .narrow=${this.narrow} | ||||||
|                       .showHelp=${this._showHelp} |                       .showHelp=${this._showHelp} | ||||||
|                     ></zha-device-card> |                     ></zha-device-pairing-status-card> | ||||||
|                   ` |                   ` | ||||||
|                 )} |                 )} | ||||||
|               `} |               `} | ||||||
| @@ -164,7 +171,7 @@ class ZHAAddDevicesPage extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _handleMessage(message: any): void { |   private _handleMessage(message: any): void { | ||||||
|     if (message.type === "log_output") { |     if (message.type === LOG_OUTPUT) { | ||||||
|       this._formattedEvents += message.log_entry.message + "\n"; |       this._formattedEvents += message.log_entry.message + "\n"; | ||||||
|       if (this.shadowRoot) { |       if (this.shadowRoot) { | ||||||
|         const paperTextArea = this.shadowRoot.querySelector("paper-textarea"); |         const paperTextArea = this.shadowRoot.querySelector("paper-textarea"); | ||||||
| @@ -175,8 +182,8 @@ class ZHAAddDevicesPage extends LitElement { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     if (message.type && message.type === "device_fully_initialized") { |     if (message.type && DEVICE_MESSAGE_TYPES.includes(message.type)) { | ||||||
|       this._discoveredDevices.push(message.device_info); |       this._discoveredDevices[message.device_info.ieee] = message.device_info; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import "@polymer/paper-item/paper-item"; | import "@polymer/paper-item/paper-item"; | ||||||
| import "@polymer/paper-item/paper-item-body"; | import "@polymer/paper-item/paper-item-body"; | ||||||
| import "@material/mwc-fab"; | import "../../../../../components/ha-fab"; | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResultArray, |   CSSResultArray, | ||||||
| @@ -88,13 +88,13 @@ class ZHAConfigDashboard extends LitElement { | |||||||
|             : ""} |             : ""} | ||||||
|         </ha-card> |         </ha-card> | ||||||
|         <a href="/config/zha/add" slot="fab"> |         <a href="/config/zha/add" slot="fab"> | ||||||
|           <mwc-fab |           <ha-fab | ||||||
|             .label=${this.hass.localize("ui.panel.config.zha.add_device")} |             .label=${this.hass.localize("ui.panel.config.zha.add_device")} | ||||||
|             extended |             extended | ||||||
|             ?rtl=${computeRTL(this.hass)} |             ?rtl=${computeRTL(this.hass)} | ||||||
|           > |           > | ||||||
|             <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |             <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|           </mwc-fab> |           </ha-fab> | ||||||
|         </a> |         </a> | ||||||
|       </hass-tabs-subpage> |       </hass-tabs-subpage> | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -0,0 +1,147 @@ | |||||||
|  | import "@polymer/paper-input/paper-input"; | ||||||
|  | import "@polymer/paper-listbox/paper-listbox"; | ||||||
|  | import { | ||||||
|  |   css, | ||||||
|  |   CSSResult, | ||||||
|  |   customElement, | ||||||
|  |   html, | ||||||
|  |   LitElement, | ||||||
|  |   property, | ||||||
|  |   internalProperty, | ||||||
|  |   TemplateResult, | ||||||
|  | } from "lit-element"; | ||||||
|  | import "../../../../../components/buttons/ha-call-service-button"; | ||||||
|  | import "../../../../../components/entity/state-badge"; | ||||||
|  | import "../../../../../components/ha-card"; | ||||||
|  | import "../../../../../components/ha-service-description"; | ||||||
|  | import { | ||||||
|  |   CONFIGURED, | ||||||
|  |   INCOMPLETE_PAIRING_STATUSES, | ||||||
|  |   INITIALIZED, | ||||||
|  |   INTERVIEW_COMPLETE, | ||||||
|  |   ZHADevice, | ||||||
|  | } from "../../../../../data/zha"; | ||||||
|  | import { haStyle } from "../../../../../resources/styles"; | ||||||
|  | import { HomeAssistant } from "../../../../../types"; | ||||||
|  | import "../../../../../components/ha-area-picker"; | ||||||
|  | import { formatAsPaddedHex } from "./functions"; | ||||||
|  | import "./zha-device-card"; | ||||||
|  | import { classMap } from "lit-html/directives/class-map"; | ||||||
|  |  | ||||||
|  | @customElement("zha-device-pairing-status-card") | ||||||
|  | class ZHADevicePairingStatusCard extends LitElement { | ||||||
|  |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @property() public device?: ZHADevice; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public narrow?: boolean; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _showHelp = false; | ||||||
|  |  | ||||||
|  |   protected render(): TemplateResult { | ||||||
|  |     if (!this.hass || !this.device) { | ||||||
|  |       return html``; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <ha-card | ||||||
|  |         outlined | ||||||
|  |         class="discovered ${classMap({ | ||||||
|  |           initialized: this.device.pairing_status === INITIALIZED, | ||||||
|  |         })}" | ||||||
|  |         ><div | ||||||
|  |           class="header" | ||||||
|  |         > | ||||||
|  |           <h1> | ||||||
|  |             ${this.hass!.localize( | ||||||
|  |               `ui.panel.config.zha.device_pairing_card.${this.device.pairing_status}` | ||||||
|  |             )} | ||||||
|  |           </h1> | ||||||
|  |           <h4> | ||||||
|  |             ${this.hass!.localize( | ||||||
|  |               `ui.panel.config.zha.device_pairing_card.${this.device.pairing_status}_status_text` | ||||||
|  |             )} | ||||||
|  |           </h4> | ||||||
|  |         </div> | ||||||
|  |         <div class="card-content"> | ||||||
|  |           ${[INTERVIEW_COMPLETE, CONFIGURED].includes( | ||||||
|  |             this.device.pairing_status! | ||||||
|  |           ) | ||||||
|  |             ? html` | ||||||
|  |                 <div class="model">${this.device.model}</div> | ||||||
|  |                 <div class="manuf"> | ||||||
|  |                   ${this.hass.localize( | ||||||
|  |                     "ui.dialogs.zha_device_info.manuf", | ||||||
|  |                     "manufacturer", | ||||||
|  |                     this.device.manufacturer | ||||||
|  |                   )} | ||||||
|  |                 </div> | ||||||
|  |               ` | ||||||
|  |             : html``} | ||||||
|  |           <div class="info"> | ||||||
|  |             ${INCOMPLETE_PAIRING_STATUSES.includes(this.device.pairing_status!) | ||||||
|  |               ? html` | ||||||
|  |                   <div class="text">IEEE: ${this.device.ieee}</div> | ||||||
|  |                   <div class="text"> | ||||||
|  |                     NWK: ${formatAsPaddedHex(this.device.nwk)} | ||||||
|  |                   </div> | ||||||
|  |                 ` | ||||||
|  |               : html``} | ||||||
|  |           </div> | ||||||
|  |           ${this.device.pairing_status === INITIALIZED | ||||||
|  |             ? html` | ||||||
|  |                 <zha-device-card | ||||||
|  |                   class="card" | ||||||
|  |                   .hass=${this.hass} | ||||||
|  |                   .device=${this.device} | ||||||
|  |                   .narrow=${this.narrow} | ||||||
|  |                   .showHelp=${this._showHelp} | ||||||
|  |                 ></zha-device-card> | ||||||
|  |               ` | ||||||
|  |             : html``} | ||||||
|  |         </div> | ||||||
|  |       </ha-card> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static get styles(): CSSResult[] { | ||||||
|  |     return [ | ||||||
|  |       haStyle, | ||||||
|  |       css` | ||||||
|  |         .discovered { | ||||||
|  |           --ha-card-border-color: var(--primary-color); | ||||||
|  |         } | ||||||
|  |         .discovered.initialized { | ||||||
|  |           --ha-card-border-color: var(--success-color); | ||||||
|  |         } | ||||||
|  |         .discovered .header { | ||||||
|  |           background: var(--primary-color); | ||||||
|  |           color: var(--text-primary-color); | ||||||
|  |           padding: 8px; | ||||||
|  |           text-align: center; | ||||||
|  |           margin-bottom: 20px; | ||||||
|  |         } | ||||||
|  |         .discovered.initialized .header { | ||||||
|  |           background: var(--success-color); | ||||||
|  |         } | ||||||
|  |         h1 { | ||||||
|  |           margin: 0; | ||||||
|  |         } | ||||||
|  |         h4 { | ||||||
|  |           margin: 0; | ||||||
|  |         } | ||||||
|  |         .text, | ||||||
|  |         .manuf, | ||||||
|  |         .model { | ||||||
|  |           color: var(--secondary-text-color); | ||||||
|  |         } | ||||||
|  |       `, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "zha-device-pairing-status-card": ZHADevicePairingStatusCard; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| import "@material/mwc-button"; | import "@material/mwc-button"; | ||||||
| import "@material/mwc-fab"; | import "../../../../../components/ha-fab"; | ||||||
| import "../../../../../components/ha-icon-button"; | import "../../../../../components/ha-icon-button"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { | import { | ||||||
| @@ -127,14 +127,14 @@ export class ZHAGroupsDashboard extends LitElement { | |||||||
|         clickable |         clickable | ||||||
|       > |       > | ||||||
|         <a href="/config/zha/group-add" slot="fab"> |         <a href="/config/zha/group-add" slot="fab"> | ||||||
|           <mwc-fab |           <ha-fab | ||||||
|             .label=${this.hass!.localize( |             .label=${this.hass!.localize( | ||||||
|               "ui.panel.config.zha.groups.add_group" |               "ui.panel.config.zha.groups.add_group" | ||||||
|             )} |             )} | ||||||
|             extended |             extended | ||||||
|           > |           > | ||||||
|             <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |             <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|           </mwc-fab> |           </ha-fab> | ||||||
|         </a> |         </a> | ||||||
|       </hass-tabs-subpage-data-table> |       </hass-tabs-subpage-data-table> | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "@material/mwc-fab"; | import "../../../../components/ha-fab"; | ||||||
| import { mdiPlus } from "@mdi/js"; | import { mdiPlus } from "@mdi/js"; | ||||||
| import "@polymer/paper-tooltip/paper-tooltip"; | import "@polymer/paper-tooltip/paper-tooltip"; | ||||||
| import { | import { | ||||||
| @@ -223,7 +223,7 @@ export class HaConfigLovelaceDashboards extends LitElement { | |||||||
|         hasFab |         hasFab | ||||||
|         clickable |         clickable | ||||||
|       > |       > | ||||||
|         <mwc-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           .label=${this.hass.localize( |           .label=${this.hass.localize( | ||||||
|             "ui.panel.config.lovelace.dashboards.picker.add_dashboard" |             "ui.panel.config.lovelace.dashboards.picker.add_dashboard" | ||||||
| @@ -232,7 +232,7 @@ export class HaConfigLovelaceDashboards extends LitElement { | |||||||
|           @click=${this._addDashboard} |           @click=${this._addDashboard} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|         </mwc-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage-data-table> |       </hass-tabs-subpage-data-table> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "@material/mwc-fab"; | import "../../../../components/ha-fab"; | ||||||
| import { mdiPlus } from "@mdi/js"; | import { mdiPlus } from "@mdi/js"; | ||||||
| import "@polymer/paper-checkbox/paper-checkbox"; | import "@polymer/paper-checkbox/paper-checkbox"; | ||||||
| import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; | import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; | ||||||
| @@ -103,7 +103,7 @@ export class HaConfigLovelaceRescources extends LitElement { | |||||||
|         hasFab |         hasFab | ||||||
|         clickable |         clickable | ||||||
|       > |       > | ||||||
|         <mwc-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           .label=${this.hass.localize( |           .label=${this.hass.localize( | ||||||
|             "ui.panel.config.lovelace.resources.picker.add_resource" |             "ui.panel.config.lovelace.resources.picker.add_resource" | ||||||
| @@ -112,7 +112,7 @@ export class HaConfigLovelaceRescources extends LitElement { | |||||||
|           @click=${this._addResource} |           @click=${this._addResource} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|         </mwc-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage-data-table> |       </hass-tabs-subpage-data-table> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -68,6 +68,8 @@ class DialogPersonDetail extends LitElement { | |||||||
|  |  | ||||||
|   @internalProperty() private _submitting = false; |   @internalProperty() private _submitting = false; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _personExists = false; | ||||||
|  |  | ||||||
|   private _deviceTrackersAvailable = memoizeOne((hass) => { |   private _deviceTrackersAvailable = memoizeOne((hass) => { | ||||||
|     return Object.keys(hass.states).some( |     return Object.keys(hass.states).some( | ||||||
|       (entityId) => |       (entityId) => | ||||||
| @@ -79,6 +81,7 @@ class DialogPersonDetail extends LitElement { | |||||||
|     this._params = params; |     this._params = params; | ||||||
|     this._error = undefined; |     this._error = undefined; | ||||||
|     if (this._params.entry) { |     if (this._params.entry) { | ||||||
|  |       this._personExists = true; | ||||||
|       this._name = this._params.entry.name || ""; |       this._name = this._params.entry.name || ""; | ||||||
|       this._userId = this._params.entry.user_id || undefined; |       this._userId = this._params.entry.user_id || undefined; | ||||||
|       this._deviceTrackers = this._params.entry.device_trackers || []; |       this._deviceTrackers = this._params.entry.device_trackers || []; | ||||||
| @@ -88,6 +91,7 @@ class DialogPersonDetail extends LitElement { | |||||||
|         : undefined; |         : undefined; | ||||||
|       this._isAdmin = this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); |       this._isAdmin = this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); | ||||||
|     } else { |     } else { | ||||||
|  |       this._personExists = false; | ||||||
|       this._name = ""; |       this._name = ""; | ||||||
|       this._userId = undefined; |       this._userId = undefined; | ||||||
|       this._user = undefined; |       this._user = undefined; | ||||||
| @@ -398,6 +402,7 @@ class DialogPersonDetail extends LitElement { | |||||||
|         await this._params!.updateEntry(values); |         await this._params!.updateEntry(values); | ||||||
|       } else { |       } else { | ||||||
|         await this._params!.createEntry(values); |         await this._params!.createEntry(values); | ||||||
|  |         this._personExists = true; | ||||||
|       } |       } | ||||||
|       this._params = undefined; |       this._params = undefined; | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
| @@ -422,6 +427,14 @@ class DialogPersonDetail extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _close(): void { |   private _close(): void { | ||||||
|  |     // If we do not have a person ID yet (= person creation dialog was just cancelled), but | ||||||
|  |     // we already created a user ID for it, delete it now to not have it "free floating". | ||||||
|  |     if (!this._personExists && this._userId) { | ||||||
|  |       deleteUser(this.hass, this._userId); | ||||||
|  |       this._params?.refreshUsers(); | ||||||
|  |       this._userId = undefined; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     this._params = undefined; |     this._params = undefined; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "@material/mwc-fab"; | import "../../../components/ha-fab"; | ||||||
| import { mdiPlus } from "@mdi/js"; | import { mdiPlus } from "@mdi/js"; | ||||||
| import "@polymer/paper-item/paper-icon-item"; | import "@polymer/paper-item/paper-icon-item"; | ||||||
| import "@polymer/paper-item/paper-item-body"; | import "@polymer/paper-item/paper-item-body"; | ||||||
| @@ -146,14 +146,14 @@ class HaConfigPerson extends LitElement { | |||||||
|               ` |               ` | ||||||
|             : ""} |             : ""} | ||||||
|         </ha-config-section> |         </ha-config-section> | ||||||
|         <mwc-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           .label=${hass.localize("ui.panel.config.person.add_person")} |           .label=${hass.localize("ui.panel.config.person.add_person")} | ||||||
|           extended |           extended | ||||||
|           @click=${this._createPerson} |           @click=${this._createPerson} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|         </mwc-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage> |       </hass-tabs-subpage> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "@material/mwc-fab"; | import "../../../components/ha-fab"; | ||||||
| import "@material/mwc-icon-button"; | import "@material/mwc-icon-button"; | ||||||
| import { mdiPlus, mdiHelpCircle } from "@mdi/js"; | import { mdiPlus, mdiHelpCircle } from "@mdi/js"; | ||||||
| import "@polymer/paper-tooltip/paper-tooltip"; | import "@polymer/paper-tooltip/paper-tooltip"; | ||||||
| @@ -152,14 +152,14 @@ class HaSceneDashboard extends LitElement { | |||||||
|           <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> |           <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> | ||||||
|         </mwc-icon-button> |         </mwc-icon-button> | ||||||
|         <a href="/config/scene/edit/new" slot="fab"> |         <a href="/config/scene/edit/new" slot="fab"> | ||||||
|           <mwc-fab |           <ha-fab | ||||||
|             .label=${this.hass.localize( |             .label=${this.hass.localize( | ||||||
|               "ui.panel.config.scene.picker.add_scene" |               "ui.panel.config.scene.picker.add_scene" | ||||||
|             )} |             )} | ||||||
|             extended |             extended | ||||||
|           > |           > | ||||||
|             <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |             <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|           </mwc-fab> |           </ha-fab> | ||||||
|         </a> |         </a> | ||||||
|       </hass-tabs-subpage-data-table> |       </hass-tabs-subpage-data-table> | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -25,7 +25,7 @@ import "../../../components/device/ha-device-picker"; | |||||||
| import "../../../components/entity/ha-entities-picker"; | import "../../../components/entity/ha-entities-picker"; | ||||||
| import "../../../components/ha-card"; | import "../../../components/ha-card"; | ||||||
| import "../../../components/ha-icon-input"; | import "../../../components/ha-icon-input"; | ||||||
| import "@material/mwc-fab"; | import "../../../components/ha-fab"; | ||||||
| import { | import { | ||||||
|   computeDeviceName, |   computeDeviceName, | ||||||
|   DeviceRegistryEntry, |   DeviceRegistryEntry, | ||||||
| @@ -403,7 +403,7 @@ export class HaSceneEditor extends SubscribeMixin( | |||||||
|               ` |               ` | ||||||
|             : ""} |             : ""} | ||||||
|         </div> |         </div> | ||||||
|         <mwc-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           .label=${this.hass.localize("ui.panel.config.scene.editor.save")} |           .label=${this.hass.localize("ui.panel.config.scene.editor.save")} | ||||||
|           extended |           extended | ||||||
| @@ -411,7 +411,7 @@ export class HaSceneEditor extends SubscribeMixin( | |||||||
|           class=${classMap({ dirty: this._dirty })} |           class=${classMap({ dirty: this._dirty })} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon> | ||||||
|         </mwc-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage> |       </hass-tabs-subpage> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| @@ -786,12 +786,12 @@ export class HaSceneEditor extends SubscribeMixin( | |||||||
|         span[slot="introduction"] a { |         span[slot="introduction"] a { | ||||||
|           color: var(--primary-color); |           color: var(--primary-color); | ||||||
|         } |         } | ||||||
|         mwc-fab { |         ha-fab { | ||||||
|           position: relative; |           position: relative; | ||||||
|           bottom: calc(-80px - env(safe-area-inset-bottom)); |           bottom: calc(-80px - env(safe-area-inset-bottom)); | ||||||
|           transition: bottom 0.3s; |           transition: bottom 0.3s; | ||||||
|         } |         } | ||||||
|         mwc-fab.dirty { |         ha-fab.dirty { | ||||||
|           bottom: 0; |           bottom: 0; | ||||||
|         } |         } | ||||||
|       `, |       `, | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "@material/mwc-fab"; | import "../../../components/ha-fab"; | ||||||
| import { | import { | ||||||
|   mdiCheck, |   mdiCheck, | ||||||
|   mdiContentSave, |   mdiContentSave, | ||||||
| @@ -35,6 +35,7 @@ import "../../../components/ha-icon-input"; | |||||||
| import "../../../components/ha-svg-icon"; | import "../../../components/ha-svg-icon"; | ||||||
| import "../../../components/ha-yaml-editor"; | import "../../../components/ha-yaml-editor"; | ||||||
| import type { HaYamlEditor } from "../../../components/ha-yaml-editor"; | import type { HaYamlEditor } from "../../../components/ha-yaml-editor"; | ||||||
|  | import { copyToClipboard } from "../../../common/util/copy-clipboard"; | ||||||
| import { | import { | ||||||
|   Action, |   Action, | ||||||
|   deleteScript, |   deleteScript, | ||||||
| @@ -388,7 +389,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { | |||||||
|               ` |               ` | ||||||
|             : ``} |             : ``} | ||||||
|         </div> |         </div> | ||||||
|         <mwc-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           .label=${this.hass.localize( |           .label=${this.hass.localize( | ||||||
|             "ui.panel.config.script.editor.save_script" |             "ui.panel.config.script.editor.save_script" | ||||||
| @@ -400,7 +401,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { | |||||||
|           })} |           })} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon> | ||||||
|         </mwc-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage> |       </hass-tabs-subpage> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| @@ -545,7 +546,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { | |||||||
|  |  | ||||||
|   private async _copyYaml() { |   private async _copyYaml() { | ||||||
|     if (this._editor?.yaml) { |     if (this._editor?.yaml) { | ||||||
|       navigator.clipboard.writeText(this._editor.yaml); |       copyToClipboard(this._editor.yaml); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -690,12 +691,12 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { | |||||||
|         span[slot="introduction"] a { |         span[slot="introduction"] a { | ||||||
|           color: var(--primary-color); |           color: var(--primary-color); | ||||||
|         } |         } | ||||||
|         mwc-fab { |         ha-fab { | ||||||
|           position: relative; |           position: relative; | ||||||
|           bottom: calc(-80px - env(safe-area-inset-bottom)); |           bottom: calc(-80px - env(safe-area-inset-bottom)); | ||||||
|           transition: bottom 0.3s; |           transition: bottom 0.3s; | ||||||
|         } |         } | ||||||
|         mwc-fab.dirty { |         ha-fab.dirty { | ||||||
|           bottom: 0; |           bottom: 0; | ||||||
|         } |         } | ||||||
|         .selected_menu_item { |         .selected_menu_item { | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ import { fireEvent } from "../../../common/dom/fire_event"; | |||||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||||
| import { computeRTL } from "../../../common/util/compute_rtl"; | import { computeRTL } from "../../../common/util/compute_rtl"; | ||||||
| import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; | import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; | ||||||
| import "@material/mwc-fab"; | import "../../../components/ha-fab"; | ||||||
| import { triggerScript } from "../../../data/script"; | import { triggerScript } from "../../../data/script"; | ||||||
| import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; | import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; | ||||||
| import "../../../layouts/hass-tabs-subpage-data-table"; | import "../../../layouts/hass-tabs-subpage-data-table"; | ||||||
| @@ -147,7 +147,7 @@ class HaScriptPicker extends LitElement { | |||||||
|           <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> |           <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> | ||||||
|         </mwc-icon-button> |         </mwc-icon-button> | ||||||
|         <a href="/config/script/edit/new" slot="fab"> |         <a href="/config/script/edit/new" slot="fab"> | ||||||
|           <mwc-fab |           <ha-fab | ||||||
|             ?is-wide=${this.isWide} |             ?is-wide=${this.isWide} | ||||||
|             ?narrow=${this.narrow} |             ?narrow=${this.narrow} | ||||||
|             .label=${this.hass.localize( |             .label=${this.hass.localize( | ||||||
| @@ -157,7 +157,7 @@ class HaScriptPicker extends LitElement { | |||||||
|             ?rtl=${computeRTL(this.hass)} |             ?rtl=${computeRTL(this.hass)} | ||||||
|           > |           > | ||||||
|             <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |             <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|           </mwc-fab> |           </ha-fab> | ||||||
|         </a> |         </a> | ||||||
|       </hass-tabs-subpage-data-table> |       </hass-tabs-subpage-data-table> | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "@material/mwc-fab"; | import "../../../components/ha-fab"; | ||||||
| import "@material/mwc-icon-button"; | import "@material/mwc-icon-button"; | ||||||
| import { | import { | ||||||
|   mdiCog, |   mdiCog, | ||||||
| @@ -84,7 +84,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) { | |||||||
|                 ${tag.last_scanned_datetime |                 ${tag.last_scanned_datetime | ||||||
|                   ? html`<ha-relative-time |                   ? html`<ha-relative-time | ||||||
|                       .hass=${this.hass} |                       .hass=${this.hass} | ||||||
|                       .datetimeObj=${tag.last_scanned_datetime} |                       .datetime=${tag.last_scanned_datetime} | ||||||
|                     ></ha-relative-time>` |                     ></ha-relative-time>` | ||||||
|                   : this.hass.localize("ui.panel.config.tags.never_scanned")} |                   : this.hass.localize("ui.panel.config.tags.never_scanned")} | ||||||
|               </div>` |               </div>` | ||||||
| @@ -103,7 +103,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) { | |||||||
|             ${last_scanned_datetime |             ${last_scanned_datetime | ||||||
|               ? html`<ha-relative-time |               ? html`<ha-relative-time | ||||||
|                   .hass=${this.hass} |                   .hass=${this.hass} | ||||||
|                   .datetimeObj=${last_scanned_datetime} |                   .datetime=${last_scanned_datetime} | ||||||
|                 ></ha-relative-time>` |                 ></ha-relative-time>` | ||||||
|               : this.hass.localize("ui.panel.config.tags.never_scanned")} |               : this.hass.localize("ui.panel.config.tags.never_scanned")} | ||||||
|           `, |           `, | ||||||
| @@ -207,14 +207,14 @@ export class HaConfigTags extends SubscribeMixin(LitElement) { | |||||||
|         <mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}> |         <mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}> | ||||||
|           <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> |           <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> | ||||||
|         </mwc-icon-button> |         </mwc-icon-button> | ||||||
|         <mwc-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           .label=${this.hass.localize("ui.panel.config.tags.add_tag")} |           .label=${this.hass.localize("ui.panel.config.tags.add_tag")} | ||||||
|           extended |           extended | ||||||
|           @click=${this._addTag} |           @click=${this._addTag} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|         </mwc-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage-data-table> |       </hass-tabs-subpage-data-table> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -98,7 +98,7 @@ export class DialogAddUser extends LitElement { | |||||||
|                 class="name" |                 class="name" | ||||||
|                 name="name" |                 name="name" | ||||||
|                 .label=${this.hass.localize( |                 .label=${this.hass.localize( | ||||||
|                   "ui.panel.config.users.add_user.name" |                   "ui.panel.config.users.editor.name" | ||||||
|                 )} |                 )} | ||||||
|                 .value=${this._name} |                 .value=${this._name} | ||||||
|                 required |                 required | ||||||
| @@ -113,7 +113,7 @@ export class DialogAddUser extends LitElement { | |||||||
|             class="username" |             class="username" | ||||||
|             name="username" |             name="username" | ||||||
|             .label=${this.hass.localize( |             .label=${this.hass.localize( | ||||||
|               "ui.panel.config.users.add_user.username" |               "ui.panel.config.users.editor.username" | ||||||
|             )} |             )} | ||||||
|             .value=${this._username} |             .value=${this._username} | ||||||
|             required |             required | ||||||
| @@ -241,7 +241,7 @@ export class DialogAddUser extends LitElement { | |||||||
|       user = userResponse.user; |       user = userResponse.user; | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       this._loading = false; |       this._loading = false; | ||||||
|       this._error = err.code; |       this._error = err.message; | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -255,10 +255,11 @@ export class DialogAddUser extends LitElement { | |||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       await deleteUser(this.hass, user.id); |       await deleteUser(this.hass, user.id); | ||||||
|       this._loading = false; |       this._loading = false; | ||||||
|       this._error = err.code; |       this._error = err.message; | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     user.username = this._username; | ||||||
|     this._params!.userAddedCallback(user); |     this._params!.userAddedCallback(user); | ||||||
|     this._close(); |     this._close(); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ import { | |||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { computeRTLDirection } from "../../../common/util/compute_rtl"; | import { computeRTLDirection } from "../../../common/util/compute_rtl"; | ||||||
| import { createCloseHeading } from "../../../components/ha-dialog"; | import { createCloseHeading } from "../../../components/ha-dialog"; | ||||||
|  | import "../../../components/ha-help-tooltip"; | ||||||
| import "../../../components/ha-formfield"; | import "../../../components/ha-formfield"; | ||||||
| import "../../../components/ha-switch"; | import "../../../components/ha-switch"; | ||||||
| import { adminChangePassword } from "../../../data/auth"; | import { adminChangePassword } from "../../../data/auth"; | ||||||
| @@ -37,6 +38,8 @@ class DialogUserDetail extends LitElement { | |||||||
|  |  | ||||||
|   @internalProperty() private _isAdmin?: boolean; |   @internalProperty() private _isAdmin?: boolean; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _isActive?: boolean; | ||||||
|  |  | ||||||
|   @internalProperty() private _error?: string; |   @internalProperty() private _error?: string; | ||||||
|  |  | ||||||
|   @internalProperty() private _params?: UserDetailDialogParams; |   @internalProperty() private _params?: UserDetailDialogParams; | ||||||
| @@ -48,6 +51,7 @@ class DialogUserDetail extends LitElement { | |||||||
|     this._error = undefined; |     this._error = undefined; | ||||||
|     this._name = params.entry.name || ""; |     this._name = params.entry.name || ""; | ||||||
|     this._isAdmin = params.entry.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); |     this._isAdmin = params.entry.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); | ||||||
|  |     this._isActive = params.entry.is_active; | ||||||
|     await this.updateComplete; |     await this.updateComplete; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -67,7 +71,10 @@ class DialogUserDetail extends LitElement { | |||||||
|         <div> |         <div> | ||||||
|           ${this._error ? html` <div class="error">${this._error}</div> ` : ""} |           ${this._error ? html` <div class="error">${this._error}</div> ` : ""} | ||||||
|           <div class="secondary"> |           <div class="secondary"> | ||||||
|             ${this.hass.localize("ui.panel.config.users.editor.id")}: ${user.id} |             ${this.hass.localize("ui.panel.config.users.editor.id")}: | ||||||
|  |             ${user.id}<br /> | ||||||
|  |             ${this.hass.localize("ui.panel.config.users.editor.username")}: | ||||||
|  |             ${user.username} | ||||||
|           </div> |           </div> | ||||||
|           <div> |           <div> | ||||||
|             ${user.is_owner |             ${user.is_owner | ||||||
| @@ -88,15 +95,6 @@ class DialogUserDetail extends LitElement { | |||||||
|                   </span> |                   </span> | ||||||
|                 ` |                 ` | ||||||
|               : ""} |               : ""} | ||||||
|             ${user.is_active |  | ||||||
|               ? html` |  | ||||||
|                   <span class="state" |  | ||||||
|                     >${this.hass.localize( |  | ||||||
|                       "ui.panel.config.users.editor.active" |  | ||||||
|                     )}</span |  | ||||||
|                   > |  | ||||||
|                 ` |  | ||||||
|               : ""} |  | ||||||
|           </div> |           </div> | ||||||
|           <div class="form"> |           <div class="form"> | ||||||
|             <paper-input |             <paper-input | ||||||
| @@ -107,17 +105,21 @@ class DialogUserDetail extends LitElement { | |||||||
|                 "ui.panel.config.users.editor.name" |                 "ui.panel.config.users.editor.name" | ||||||
|               )}" |               )}" | ||||||
|             ></paper-input> |             ></paper-input> | ||||||
|             <ha-formfield |             <div class="row"> | ||||||
|               .label=${this.hass.localize("ui.panel.config.users.editor.admin")} |               <ha-formfield | ||||||
|               .dir=${computeRTLDirection(this.hass)} |                 .label=${this.hass.localize( | ||||||
|             > |                   "ui.panel.config.users.editor.admin" | ||||||
|               <ha-switch |                 )} | ||||||
|                 .disabled=${user.system_generated || user.is_owner} |                 .dir=${computeRTLDirection(this.hass)} | ||||||
|                 .checked=${this._isAdmin} |  | ||||||
|                 @change=${this._adminChanged} |  | ||||||
|               > |               > | ||||||
|               </ha-switch> |                 <ha-switch | ||||||
|             </ha-formfield> |                   .disabled=${user.system_generated || user.is_owner} | ||||||
|  |                   .checked=${this._isAdmin} | ||||||
|  |                   @change=${this._adminChanged} | ||||||
|  |                 > | ||||||
|  |                 </ha-switch> | ||||||
|  |               </ha-formfield> | ||||||
|  |             </div> | ||||||
|             ${!this._isAdmin |             ${!this._isAdmin | ||||||
|               ? html` |               ? html` | ||||||
|                   <br /> |                   <br /> | ||||||
| @@ -126,6 +128,27 @@ class DialogUserDetail extends LitElement { | |||||||
|                   )} |                   )} | ||||||
|                 ` |                 ` | ||||||
|               : ""} |               : ""} | ||||||
|  |             <div class="row"> | ||||||
|  |               <ha-formfield | ||||||
|  |                 .label=${this.hass.localize( | ||||||
|  |                   "ui.panel.config.users.editor.active" | ||||||
|  |                 )} | ||||||
|  |                 .dir=${computeRTLDirection(this.hass)} | ||||||
|  |               > | ||||||
|  |                 <ha-switch | ||||||
|  |                   .disabled=${user.system_generated || user.is_owner} | ||||||
|  |                   .checked=${this._isActive} | ||||||
|  |                   @change=${this._activeChanged} | ||||||
|  |                 > | ||||||
|  |                 </ha-switch> | ||||||
|  |               </ha-formfield> | ||||||
|  |               <ha-help-tooltip | ||||||
|  |                 .label=${this.hass.localize( | ||||||
|  |                   "ui.panel.config.users.editor.active_tooltip" | ||||||
|  |                 )} | ||||||
|  |               > | ||||||
|  |               </ha-help-tooltip> | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
| @@ -189,11 +212,16 @@ class DialogUserDetail extends LitElement { | |||||||
|     this._isAdmin = ev.target.checked; |     this._isAdmin = ev.target.checked; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private async _activeChanged(ev): Promise<void> { | ||||||
|  |     this._isActive = ev.target.checked; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private async _updateEntry() { |   private async _updateEntry() { | ||||||
|     this._submitting = true; |     this._submitting = true; | ||||||
|     try { |     try { | ||||||
|       await this._params!.updateEntry({ |       await this._params!.updateEntry({ | ||||||
|         name: this._name.trim(), |         name: this._name.trim(), | ||||||
|  |         is_active: this._isActive, | ||||||
|         group_ids: [ |         group_ids: [ | ||||||
|           this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER, |           this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER, | ||||||
|         ], |         ], | ||||||
| @@ -290,8 +318,13 @@ class DialogUserDetail extends LitElement { | |||||||
|         .state:not(:first-child) { |         .state:not(:first-child) { | ||||||
|           margin-left: 8px; |           margin-left: 8px; | ||||||
|         } |         } | ||||||
|         ha-switch { |         .row { | ||||||
|           margin-top: 8px; |           display: flex; | ||||||
|  |           padding: 8px 0; | ||||||
|  |         } | ||||||
|  |         ha-help-tooltip { | ||||||
|  |           margin-left: 4px; | ||||||
|  |           position: relative; | ||||||
|         } |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "@material/mwc-fab"; | import "../../../components/ha-fab"; | ||||||
| import { mdiPlus } from "@mdi/js"; | import { mdiPlus } from "@mdi/js"; | ||||||
| import { | import { | ||||||
|   customElement, |   customElement, | ||||||
| @@ -35,18 +35,40 @@ export class HaConfigUsers extends LitElement { | |||||||
|   @property() public route!: Route; |   @property() public route!: Route; | ||||||
|  |  | ||||||
|   private _columns = memoizeOne( |   private _columns = memoizeOne( | ||||||
|     (_language): DataTableColumnContainer => { |     (narrow: boolean, _language): DataTableColumnContainer => { | ||||||
|       return { |       const columns: DataTableColumnContainer = { | ||||||
|         name: { |         name: { | ||||||
|           title: this.hass.localize( |           title: this.hass.localize( | ||||||
|             "ui.panel.config.users.picker.headers.name" |             "ui.panel.config.users.picker.headers.name" | ||||||
|           ), |           ), | ||||||
|           sortable: true, |           sortable: true, | ||||||
|           filterable: true, |           filterable: true, | ||||||
|  |           width: "25%", | ||||||
|           direction: "asc", |           direction: "asc", | ||||||
|           grows: true, |           grows: true, | ||||||
|           template: (name) => html` |           template: (name, user: any) => | ||||||
|             ${name || |             narrow | ||||||
|  |               ? html` ${name}<br /> | ||||||
|  |                   <div class="secondary"> | ||||||
|  |                     ${user.username} | | ||||||
|  |                     ${this.hass.localize(`groups.${user.group_ids[0]}`)} | ||||||
|  |                   </div>` | ||||||
|  |               : html` ${name || | ||||||
|  |                 this.hass!.localize( | ||||||
|  |                   "ui.panel.config.users.editor.unnamed_user" | ||||||
|  |                 )}`, | ||||||
|  |         }, | ||||||
|  |         username: { | ||||||
|  |           title: this.hass.localize( | ||||||
|  |             "ui.panel.config.users.picker.headers.username" | ||||||
|  |           ), | ||||||
|  |           sortable: true, | ||||||
|  |           filterable: true, | ||||||
|  |           width: "20%", | ||||||
|  |           direction: "asc", | ||||||
|  |           hidden: narrow, | ||||||
|  |           template: (username) => html` | ||||||
|  |             ${username || | ||||||
|             this.hass!.localize("ui.panel.config.users.editor.unnamed_user")} |             this.hass!.localize("ui.panel.config.users.editor.unnamed_user")} | ||||||
|           `, |           `, | ||||||
|         }, |         }, | ||||||
| @@ -56,26 +78,38 @@ export class HaConfigUsers extends LitElement { | |||||||
|           ), |           ), | ||||||
|           sortable: true, |           sortable: true, | ||||||
|           filterable: true, |           filterable: true, | ||||||
|           width: "30%", |           width: "20%", | ||||||
|  |           direction: "asc", | ||||||
|  |           hidden: narrow, | ||||||
|           template: (groupIds) => html` |           template: (groupIds) => html` | ||||||
|             ${this.hass.localize(`groups.${groupIds[0]}`)} |             ${this.hass.localize(`groups.${groupIds[0]}`)} | ||||||
|           `, |           `, | ||||||
|         }, |         }, | ||||||
|  |         is_active: { | ||||||
|  |           title: this.hass.localize( | ||||||
|  |             "ui.panel.config.users.picker.headers.is_active" | ||||||
|  |           ), | ||||||
|  |           type: "icon", | ||||||
|  |           sortable: true, | ||||||
|  |           filterable: true, | ||||||
|  |           width: "80px", | ||||||
|  |           template: (is_active) => | ||||||
|  |             is_active ? html`<ha-icon icon="hass:check"> </ha-icon>` : "", | ||||||
|  |         }, | ||||||
|         system_generated: { |         system_generated: { | ||||||
|           title: this.hass.localize( |           title: this.hass.localize( | ||||||
|             "ui.panel.config.users.picker.headers.system" |             "ui.panel.config.users.picker.headers.system" | ||||||
|           ), |           ), | ||||||
|           type: "icon", |           type: "icon", | ||||||
|           width: "80px", |  | ||||||
|           sortable: true, |           sortable: true, | ||||||
|           filterable: true, |           filterable: true, | ||||||
|           template: (generated) => html` |           width: "160px", | ||||||
|             ${generated |           template: (generated) => | ||||||
|               ? html` <ha-icon icon="hass:check-circle-outline"></ha-icon> ` |             generated ? html`<ha-icon icon="hass:check"> </ha-icon>` : "", | ||||||
|               : ""} |  | ||||||
|           `, |  | ||||||
|         }, |         }, | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|  |       return columns; | ||||||
|     } |     } | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
| @@ -92,26 +126,32 @@ export class HaConfigUsers extends LitElement { | |||||||
|         .route=${this.route} |         .route=${this.route} | ||||||
|         backPath="/config" |         backPath="/config" | ||||||
|         .tabs=${configSections.persons} |         .tabs=${configSections.persons} | ||||||
|         .columns=${this._columns(this.hass.language)} |         .columns=${this._columns(this.narrow, this.hass.language)} | ||||||
|         .data=${this._users} |         .data=${this._users} | ||||||
|         @row-click=${this._editUser} |         @row-click=${this._editUser} | ||||||
|         hasFab |         hasFab | ||||||
|         clickable |         clickable | ||||||
|       > |       > | ||||||
|         <mwc-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           .label=${this.hass.localize("ui.panel.config.users.picker.add_user")} |           .label=${this.hass.localize("ui.panel.config.users.picker.add_user")} | ||||||
|           extended |           extended | ||||||
|           @click=${this._addUser} |           @click=${this._addUser} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|         </mwc-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage-data-table> |       </hass-tabs-subpage-data-table> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _fetchUsers() { |   private async _fetchUsers() { | ||||||
|     this._users = await fetchUsers(this.hass); |     this._users = await fetchUsers(this.hass); | ||||||
|  |  | ||||||
|  |     this._users.forEach(function (user) { | ||||||
|  |       if (user.is_owner) { | ||||||
|  |         user.group_ids.unshift("owner"); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _editUser(ev: HASSDomEvent<RowClickedEvent>) { |   private _editUser(ev: HASSDomEvent<RowClickedEvent>) { | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import "@material/mwc-fab"; | import "../../../components/ha-fab"; | ||||||
| import "@material/mwc-icon-button"; | import "@material/mwc-icon-button"; | ||||||
| import { mdiPencil, mdiPencilOff, mdiPlus } from "@mdi/js"; | import { mdiPencil, mdiPencilOff, mdiPlus } from "@mdi/js"; | ||||||
| import "@polymer/paper-item/paper-icon-item"; | import "@polymer/paper-item/paper-icon-item"; | ||||||
| @@ -255,14 +255,14 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { | |||||||
|               </div> |               </div> | ||||||
|             ` |             ` | ||||||
|           : ""} |           : ""} | ||||||
|         <mwc-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           .label=${hass.localize("ui.panel.config.zone.add_zone")} |           .label=${hass.localize("ui.panel.config.zone.add_zone")} | ||||||
|           extended |           extended | ||||||
|           @click=${this._createZone} |           @click=${this._createZone} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|         </mwc-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage> |       </hass-tabs-subpage> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -109,7 +109,7 @@ class PanelDeveloperTools extends LitElement { | |||||||
|         ha-tabs { |         ha-tabs { | ||||||
|           margin-left: max(env(safe-area-inset-left), 24px); |           margin-left: max(env(safe-area-inset-left), 24px); | ||||||
|           margin-right: max(env(safe-area-inset-right), 24px); |           margin-right: max(env(safe-area-inset-right), 24px); | ||||||
|           --paper-tabs-selection-bar-color: #fff; |           --paper-tabs-selection-bar-color: var(--text-primary-color, #fff); | ||||||
|           text-transform: uppercase; |           text-transform: uppercase; | ||||||
|         } |         } | ||||||
|       `, |       `, | ||||||
|   | |||||||
| @@ -84,9 +84,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public setConfig(config: ButtonCardConfig): void { |   public setConfig(config: ButtonCardConfig): void { | ||||||
|     if (!config.entity) { |  | ||||||
|       throw new Error("Entity must be specified"); |  | ||||||
|     } |  | ||||||
|     if (config.entity && !isValidEntityId(config.entity)) { |     if (config.entity && !isValidEntityId(config.entity)) { | ||||||
|       throw new Error("Invalid entity"); |       throw new Error("Invalid entity"); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -109,7 +109,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public setConfig(config: EntitiesCardConfig): void { |   public setConfig(config: EntitiesCardConfig): void { | ||||||
|     if (!config || !config.entities.length) { |     if (!config.entities || !Array.isArray(config.entities)) { | ||||||
|       throw new Error("Entities must be specified"); |       throw new Error("Entities must be specified"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,7 +2,14 @@ import { customElement } from "lit-element"; | |||||||
| import { HuiButtonCard } from "./hui-button-card"; | import { HuiButtonCard } from "./hui-button-card"; | ||||||
|  |  | ||||||
| @customElement("hui-entity-button-card") | @customElement("hui-entity-button-card") | ||||||
| class HuiEntityButtonCard extends HuiButtonCard {} | class HuiEntityButtonCard extends HuiButtonCard { | ||||||
|  |   public setConfig(config): void { | ||||||
|  |     if (!config.entity) { | ||||||
|  |       throw new Error("Entity must be specified"); | ||||||
|  |     } | ||||||
|  |     super.setConfig(config); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
|   | |||||||
| @@ -16,7 +16,6 @@ import "../../../components/state-history-charts"; | |||||||
| import { CacheConfig, getRecentWithCache } from "../../../data/cached-history"; | import { CacheConfig, getRecentWithCache } from "../../../data/cached-history"; | ||||||
| import { HistoryResult } from "../../../data/history"; | import { HistoryResult } from "../../../data/history"; | ||||||
| import { HomeAssistant } from "../../../types"; | import { HomeAssistant } from "../../../types"; | ||||||
| import { findEntities } from "../common/find-entites"; |  | ||||||
| import { hasConfigOrEntitiesChanged } from "../common/has-changed"; | import { hasConfigOrEntitiesChanged } from "../common/has-changed"; | ||||||
| import { processConfigEntities } from "../common/process-config-entities"; | import { processConfigEntities } from "../common/process-config-entities"; | ||||||
| import { EntityConfig } from "../entity-rows/types"; | import { EntityConfig } from "../entity-rows/types"; | ||||||
| @@ -30,22 +29,9 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { | |||||||
|     return document.createElement("hui-history-graph-card-editor"); |     return document.createElement("hui-history-graph-card-editor"); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public static getStubConfig( |   public static getStubConfig(): HistoryGraphCardConfig { | ||||||
|     hass: HomeAssistant, |     // Hard coded to sun.sun to prevent high server load when it would pick an entity with a lot of state changes | ||||||
|     entities: string[], |     return { type: "history-graph", entities: ["sun.sun"] }; | ||||||
|     entitiesFallback: string[] |  | ||||||
|   ): HistoryGraphCardConfig { |  | ||||||
|     const includeDomains = ["sensor"]; |  | ||||||
|     const maxEntities = 1; |  | ||||||
|     const foundEntities = findEntities( |  | ||||||
|       hass, |  | ||||||
|       maxEntities, |  | ||||||
|       entities, |  | ||||||
|       entitiesFallback, |  | ||||||
|       includeDomains |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     return { type: "history-graph", entities: foundEntities }; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @property({ attribute: false }) public hass?: HomeAssistant; |   @property({ attribute: false }) public hass?: HomeAssistant; | ||||||
| @@ -71,12 +57,12 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public setConfig(config: HistoryGraphCardConfig): void { |   public setConfig(config: HistoryGraphCardConfig): void { | ||||||
|     if (!config.entities.length) { |     if (!config.entities || !Array.isArray(config.entities)) { | ||||||
|       throw new Error("Entities must be specified"); |       throw new Error("Entities need to be an array"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (config.entities && !Array.isArray(config.entities)) { |     if (!config.entities.length) { | ||||||
|       throw new Error("Entities need to be an array"); |       throw new Error("You must include at least one entity"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     this._config = config; |     this._config = config; | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ class HuiHorizontalStackCard extends HuiStackCard { | |||||||
|         } |         } | ||||||
|         #root > * { |         #root > * { | ||||||
|           flex: 1 1 0; |           flex: 1 1 0; | ||||||
|           margin: 0 4px; |           margin: var(--horizontal-stack-card-margin, var(--stack-card-margin, 0 4px)); | ||||||
|           min-width: 0; |           min-width: 0; | ||||||
|         } |         } | ||||||
|         #root > *:first-child { |         #root > *:first-child { | ||||||
|   | |||||||
| @@ -246,78 +246,73 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { | |||||||
|           ${!isUnavailable && |           ${!isUnavailable && | ||||||
|           (mediaDescription || stateObj.attributes.media_title || showControls) |           (mediaDescription || stateObj.attributes.media_title || showControls) | ||||||
|             ? html` |             ? html` | ||||||
|                 <div |                 <div> | ||||||
|                   class="title-controls" |                   <div class="title-controls"> | ||||||
|                   style=${styleMap({ |                     ${!mediaDescription && !stateObj.attributes.media_title | ||||||
|                     paddingRight: isOffState |                       ? "" | ||||||
|                       ? "0" |                       : html` | ||||||
|                       : `${this._cardHeight - 40}px`, |                           <div class="media-info"> | ||||||
|                   })} |                             <hui-marquee | ||||||
|                 > |                               .text=${stateObj.attributes.media_title || | ||||||
|                   ${!mediaDescription && !stateObj.attributes.media_title |                               mediaDescription} | ||||||
|                     ? "" |                               .active=${this._marqueeActive} | ||||||
|                     : html` |                               @mouseover=${this._marqueeMouseOver} | ||||||
|                         <div class="media-info"> |                               @mouseleave=${this._marqueeMouseLeave} | ||||||
|                           <hui-marquee |                             ></hui-marquee> | ||||||
|                             .text=${stateObj.attributes.media_title || |                             ${!stateObj.attributes.media_title | ||||||
|                             mediaDescription} |                               ? "" | ||||||
|                             .active=${this._marqueeActive} |                               : mediaDescription} | ||||||
|                             @mouseover=${this._marqueeMouseOver} |                           </div> | ||||||
|                             @mouseleave=${this._marqueeMouseLeave} |                         `} | ||||||
|                           ></hui-marquee> |                     ${!showControls | ||||||
|                           ${!stateObj.attributes.media_title |                       ? "" | ||||||
|                             ? "" |                       : html` | ||||||
|                             : mediaDescription} |                           <div class="controls"> | ||||||
|                         </div> |                             ${controls!.map( | ||||||
|                       `} |                               (control) => html` | ||||||
|                   ${!showControls |                                 <ha-icon-button | ||||||
|                     ? "" |  | ||||||
|                     : html` |  | ||||||
|                         <div class="controls"> |  | ||||||
|                           ${controls!.map( |  | ||||||
|                             (control) => html` |  | ||||||
|                               <ha-icon-button |  | ||||||
|                                 .title=${this.hass.localize( |  | ||||||
|                                   `ui.card.media_player.${control.action}` |  | ||||||
|                                 )} |  | ||||||
|                                 .icon=${control.icon} |  | ||||||
|                                 action=${control.action} |  | ||||||
|                                 @click=${this._handleClick} |  | ||||||
|                               ></ha-icon-button> |  | ||||||
|                             ` |  | ||||||
|                           )} |  | ||||||
|                           ${supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA) |  | ||||||
|                             ? html` |  | ||||||
|                                 <mwc-icon-button |  | ||||||
|                                   class="browse-media" |  | ||||||
|                                   .title=${this.hass.localize( |                                   .title=${this.hass.localize( | ||||||
|                                     "ui.card.media_player.browse_media" |                                     `ui.card.media_player.${control.action}` | ||||||
|                                   )} |                                   )} | ||||||
|                                   @click=${this._handleBrowseMedia} |                                   .icon=${control.icon} | ||||||
|                                   ><ha-svg-icon |                                   action=${control.action} | ||||||
|                                     .path=${mdiPlayBoxMultiple} |                                   @click=${this._handleClick} | ||||||
|                                   ></ha-svg-icon |                                 ></ha-icon-button> | ||||||
|                                 ></mwc-icon-button> |  | ||||||
|                               ` |                               ` | ||||||
|                             : ""} |                             )} | ||||||
|                         </div> |                             ${supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA) | ||||||
|  |                               ? html` | ||||||
|  |                                   <mwc-icon-button | ||||||
|  |                                     class="browse-media" | ||||||
|  |                                     .title=${this.hass.localize( | ||||||
|  |                                       "ui.card.media_player.browse_media" | ||||||
|  |                                     )} | ||||||
|  |                                     @click=${this._handleBrowseMedia} | ||||||
|  |                                     ><ha-svg-icon | ||||||
|  |                                       .path=${mdiPlayBoxMultiple} | ||||||
|  |                                     ></ha-svg-icon | ||||||
|  |                                   ></mwc-icon-button> | ||||||
|  |                                 ` | ||||||
|  |                               : ""} | ||||||
|  |                           </div> | ||||||
|  |                         `} | ||||||
|  |                   </div> | ||||||
|  |                   ${!this._showProgressBar | ||||||
|  |                     ? "" | ||||||
|  |                     : html` | ||||||
|  |                         <paper-progress | ||||||
|  |                           .max=${stateObj.attributes.media_duration} | ||||||
|  |                           style=${styleMap({ | ||||||
|  |                             "--paper-progress-active-color": | ||||||
|  |                               this._foregroundColor || "var(--accent-color)", | ||||||
|  |                             cursor: supportsFeature(stateObj, SUPPORT_SEEK) | ||||||
|  |                               ? "pointer" | ||||||
|  |                               : "initial", | ||||||
|  |                           })} | ||||||
|  |                           @click=${this._handleSeek} | ||||||
|  |                         ></paper-progress> | ||||||
|                       `} |                       `} | ||||||
|                 </div> |                 </div> | ||||||
|                 ${!this._showProgressBar |  | ||||||
|                   ? "" |  | ||||||
|                   : html` |  | ||||||
|                       <paper-progress |  | ||||||
|                         .max=${stateObj.attributes.media_duration} |  | ||||||
|                         style=${styleMap({ |  | ||||||
|                           "--paper-progress-active-color": |  | ||||||
|                             this._foregroundColor || "var(--accent-color)", |  | ||||||
|                           cursor: supportsFeature(stateObj, SUPPORT_SEEK) |  | ||||||
|                             ? "pointer" |  | ||||||
|                             : "initial", |  | ||||||
|                         })} |  | ||||||
|                         @click=${this._handleSeek} |  | ||||||
|                       ></paper-progress> |  | ||||||
|                     `} |  | ||||||
|               ` |               ` | ||||||
|             : ""} |             : ""} | ||||||
|         </div> |         </div> | ||||||
| @@ -635,6 +630,11 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { | |||||||
|       .player { |       .player { | ||||||
|         position: relative; |         position: relative; | ||||||
|         padding: 16px; |         padding: 16px; | ||||||
|  |         height: 100%; | ||||||
|  |         box-sizing: border-box; | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: column; | ||||||
|  |         justify-content: space-between; | ||||||
|         color: var(--text-primary-color); |         color: var(--text-primary-color); | ||||||
|         transition-property: color, padding; |         transition-property: color, padding; | ||||||
|         transition-duration: 0.4s; |         transition-duration: 0.4s; | ||||||
| @@ -671,7 +671,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { | |||||||
|  |  | ||||||
|       mwc-icon-button.browse-media { |       mwc-icon-button.browse-media { | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         right: 0; |         right: 4px; | ||||||
|         --mdc-icon-size: 24px; |         --mdc-icon-size: 24px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -693,7 +693,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { | |||||||
|       .more-info { |       .more-info { | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         top: 4px; |         top: 4px; | ||||||
|         right: 0px; |         right: 4px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .media-info { |       .media-info { | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ class HuiVerticalStackCard extends HuiStackCard { | |||||||
|           height: 100%; |           height: 100%; | ||||||
|         } |         } | ||||||
|         #root > * { |         #root > * { | ||||||
|           margin: 4px 0 4px 0; |           margin: var(--vertical-stack-card-margin, var(--stack-card-margin, 4px 0)); | ||||||
|         } |         } | ||||||
|         #root > *:first-child { |         #root > *:first-child { | ||||||
|           margin-top: 0; |           margin-top: 0; | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| import "@material/mwc-button"; | import "@material/mwc-button"; | ||||||
| import "@material/mwc-list/mwc-list-item"; |  | ||||||
| import "@material/mwc-icon-button"; | import "@material/mwc-icon-button"; | ||||||
| import "../../../components/ha-button-menu"; | import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; | ||||||
|  | import "@material/mwc-list/mwc-list-item"; | ||||||
|  | import { mdiArrowDown, mdiArrowUp, mdiDotsVertical } from "@mdi/js"; | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResult, |   CSSResult, | ||||||
| @@ -9,21 +10,20 @@ import { | |||||||
|   html, |   html, | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|   TemplateResult, |  | ||||||
|   queryAssignedNodes, |   queryAssignedNodes, | ||||||
|  |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { HomeAssistant } from "../../../types"; | import { fireEvent } from "../../../common/dom/fire_event"; | ||||||
| import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; | import "../../../components/ha-button-menu"; | ||||||
| import { swapCard, moveCard, addCard, deleteCard } from "../editor/config-util"; |  | ||||||
| import { confDeleteCard } from "../editor/delete-card"; |  | ||||||
| import { Lovelace, LovelaceCard } from "../types"; |  | ||||||
| import { computeCardSize } from "../common/compute-card-size"; |  | ||||||
| import { mdiDotsVertical, mdiArrowDown, mdiArrowUp } from "@mdi/js"; |  | ||||||
| import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; |  | ||||||
| import { showSelectViewDialog } from "../editor/select-view/show-select-view-dialog"; |  | ||||||
| import { saveConfig } from "../../../data/lovelace"; | import { saveConfig } from "../../../data/lovelace"; | ||||||
| import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; | import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; | ||||||
|  | import { HomeAssistant } from "../../../types"; | ||||||
| import { showSaveSuccessToast } from "../../../util/toast-saved-success"; | import { showSaveSuccessToast } from "../../../util/toast-saved-success"; | ||||||
|  | import { computeCardSize } from "../common/compute-card-size"; | ||||||
|  | import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; | ||||||
|  | import { addCard, deleteCard, moveCard, swapCard } from "../editor/config-util"; | ||||||
|  | import { showSelectViewDialog } from "../editor/select-view/show-select-view-dialog"; | ||||||
|  | import { Lovelace, LovelaceCard } from "../types"; | ||||||
|  |  | ||||||
| @customElement("hui-card-options") | @customElement("hui-card-options") | ||||||
| export class HuiCardOptions extends LitElement { | export class HuiCardOptions extends LitElement { | ||||||
| @@ -168,11 +168,7 @@ export class HuiCardOptions extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _editCard(): void { |   private _editCard(): void { | ||||||
|     showEditCardDialog(this, { |     fireEvent(this, "ll-edit-card", { path: this.path! }); | ||||||
|       lovelaceConfig: this.lovelace!.config, |  | ||||||
|       saveConfig: this.lovelace!.saveConfig, |  | ||||||
|       path: this.path!, |  | ||||||
|     }); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _cardUp(): void { |   private _cardUp(): void { | ||||||
| @@ -229,7 +225,7 @@ export class HuiCardOptions extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _deleteCard(): void { |   private _deleteCard(): void { | ||||||
|     confDeleteCard(this, this.hass!, this.lovelace!, this.path!); |     fireEvent(this, "ll-delete-card", { path: this.path! }); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -72,7 +72,7 @@ class HuiMarquee extends LitElement { | |||||||
|         display: flex; |         display: flex; | ||||||
|         position: relative; |         position: relative; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|         height: 1em; |         height: 1.2em; | ||||||
|         contain: strict; |         contain: strict; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -59,6 +59,9 @@ export class HuiThemeSelectEditor extends LitElement { | |||||||
|       paper-dropdown-menu { |       paper-dropdown-menu { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|       } |       } | ||||||
|  |       paper-item { | ||||||
|  |         cursor: pointer; | ||||||
|  |       } | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -353,11 +353,9 @@ export class HuiCardPicker extends LitElement { | |||||||
|           max-width: 500px; |           max-width: 500px; | ||||||
|           display: flex; |           display: flex; | ||||||
|           flex-direction: column; |           flex-direction: column; | ||||||
|           border-radius: 4px; |           border-radius: var(--ha-card-border-radius, 4px); | ||||||
|           border: 1px solid var(--divider-color); |  | ||||||
|           background: var(--primary-background-color, #fafafa); |           background: var(--primary-background-color, #fafafa); | ||||||
|           cursor: pointer; |           cursor: pointer; | ||||||
|           box-sizing: border-box; |  | ||||||
|           position: relative; |           position: relative; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -375,7 +373,6 @@ export class HuiCardPicker extends LitElement { | |||||||
|             --ha-card-background, |             --ha-card-background, | ||||||
|             var(--card-background-color, white) |             var(--card-background-color, white) | ||||||
|           ); |           ); | ||||||
|           border-radius: 0 0 4px 4px; |  | ||||||
|           border-bottom: 1px solid var(--divider-color); |           border-bottom: 1px solid var(--divider-color); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -408,6 +405,10 @@ export class HuiCardPicker extends LitElement { | |||||||
|           width: 100%; |           width: 100%; | ||||||
|           height: 100%; |           height: 100%; | ||||||
|           z-index: 1; |           z-index: 1; | ||||||
|  |           box-sizing: border-box; | ||||||
|  |           border: var(--ha-card-border-width, 1px) solid | ||||||
|  |             var(--ha-card-border-color, var(--divider-color)); | ||||||
|  |           border-radius: var(--ha-card-border-radius, 4px); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .manual { |         .manual { | ||||||
|   | |||||||
| @@ -450,6 +450,10 @@ export class HuiDialogEditCard extends LitElement | |||||||
|         } |         } | ||||||
|         .element-preview { |         .element-preview { | ||||||
|           position: relative; |           position: relative; | ||||||
|  |           height: max-content; | ||||||
|  |           background: var(--primary-background-color); | ||||||
|  |           padding: 4px; | ||||||
|  |           border-radius: 4px; | ||||||
|         } |         } | ||||||
|         .element-preview ha-circular-progress { |         .element-preview ha-circular-progress { | ||||||
|           top: 50%; |           top: 50%; | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export interface CreateCardDialogParams { | |||||||
|   entities?: string[]; // We can pass entity id's that will be added to the config when a card is picked |   entities?: string[]; // We can pass entity id's that will be added to the config when a card is picked | ||||||
| } | } | ||||||
|  |  | ||||||
| const importCreateCardDialog = () => import("./hui-dialog-create-card"); | export const importCreateCardDialog = () => import("./hui-dialog-create-card"); | ||||||
|  |  | ||||||
| export const showCreateCardDialog = ( | export const showCreateCardDialog = ( | ||||||
|   element: HTMLElement, |   element: HTMLElement, | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user