diff --git a/pyproject.toml b/pyproject.toml index 9b9d6431ab..3cff776d8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20230426.0" +version = "20230427.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 518e554e4a..b489ba505e 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -666,6 +666,7 @@ export class HaDataTable extends LitElement { .mdc-data-table__cell.mdc-data-table__cell--flex { display: flex; + overflow: initial; } .mdc-data-table__cell.mdc-data-table__cell--icon { diff --git a/src/data/alexa.ts b/src/data/alexa.ts index bdc079c60f..ea8710e033 100644 --- a/src/data/alexa.ts +++ b/src/data/alexa.ts @@ -9,5 +9,11 @@ export interface AlexaEntity { export const fetchCloudAlexaEntities = (hass: HomeAssistant) => hass.callWS({ type: "cloud/alexa/entities" }); +export const fetchCloudAlexaEntity = (hass: HomeAssistant, entity_id: string) => + hass.callWS({ + type: "cloud/alexa/entities/get", + entity_id, + }); + export const syncCloudAlexaEntities = (hass: HomeAssistant) => hass.callWS({ type: "cloud/alexa/sync" }); diff --git a/src/dialogs/more-info/ha-more-info-history.ts b/src/dialogs/more-info/ha-more-info-history.ts index 464d0a0e09..f9e14f6338 100644 --- a/src/dialogs/more-info/ha-more-info-history.ts +++ b/src/dialogs/more-info/ha-more-info-history.ts @@ -27,7 +27,7 @@ declare global { } } -const statTypes: StatisticsTypes = ["min", "mean", "max"]; +const statTypes: StatisticsTypes = ["state", "min", "mean", "max"]; @customElement("ha-more-info-history") export class MoreInfoHistory extends LitElement { diff --git a/src/panels/config/voice-assistants/dialog-expose-entity.ts b/src/panels/config/voice-assistants/dialog-expose-entity.ts index 3acf0d1c65..6e6da8088e 100644 --- a/src/panels/config/voice-assistants/dialog-expose-entity.ts +++ b/src/panels/config/voice-assistants/dialog-expose-entity.ts @@ -13,7 +13,7 @@ import { ExtEntityRegistryEntry, } from "../../../data/entity_registry"; import { voiceAssistants } from "../../../data/voice"; -import { haStyle, haStyleDialog } from "../../../resources/styles"; +import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import "./entity-voice-settings"; import { ExposeEntityDialogParams } from "./show-dialog-expose-entity"; @@ -48,6 +48,11 @@ class DialogExposeEntity extends LitElement { "ui.panel.config.voice_assistants.expose.expose_dialog.header" ); + const entities = this._filterEntities( + this._params.extendedEntities, + this._filter + ); + return html`
@@ -76,10 +81,14 @@ class DialogExposeEntity extends LitElement { >
- ${this._filterEntities( - this._params.extendedEntities, - this._filter - ).map((entity) => this._renderItem(entity))} + + { const entityId = ev.target.value; if (ev.detail.selected) { if (this._selected.includes(entityId)) { @@ -108,6 +114,11 @@ class DialogExposeEntity extends LitElement { } else { this._selected = this._selected.filter((item) => item !== entityId); } + }; + + private _itemClicked(ev) { + const listItem = ev.target.closest("ha-check-list-item"); + listItem.selected = !listItem.selected; } private _filterChanged(e) { @@ -133,21 +144,23 @@ class DialogExposeEntity extends LitElement { private _renderItem = (entity: ExtEntityRegistryEntry) => { const entityState = this.hass.states[entity.entity_id]; - return html` - - ${computeEntityRegistryName(this.hass!, entity)} - ${entity.entity_id} - `; + return html` + + + ${computeEntityRegistryName(this.hass!, entity)} + ${entity.entity_id} + + `; }; private _expose() { @@ -158,21 +171,36 @@ class DialogExposeEntity extends LitElement { static get styles(): CSSResultGroup { return [ haStyle, - haStyleDialog, css` ha-dialog { --dialog-content-padding: 0; + --mdc-dialog-min-width: 500px; + --mdc-dialog-max-width: 600px; } - @media all and (min-width: 600px) { + lit-virtualizer { + height: 500px; + } + @media all and (max-width: 500px), all and (max-height: 800px) { ha-dialog { - --mdc-dialog-min-width: 600px; - --mdc-dialog-max-height: 80%; + --mdc-dialog-min-width: calc( + 100vw - env(safe-area-inset-right) - env(safe-area-inset-left) + ); + --mdc-dialog-max-width: calc( + 100vw - env(safe-area-inset-right) - env(safe-area-inset-left) + ); + --mdc-dialog-min-height: 100%; + --mdc-dialog-max-height: 100%; + --vertical-align-dialog: flex-end; + --ha-dialog-border-radius: 0px; + } + lit-virtualizer { + height: calc(100vh - 234px); } } search-input { width: 100%; display: block; - padding: 24px 16px 0; + padding: 16px 16px 0; box-sizing: border-box; } .header { @@ -231,6 +259,20 @@ class DialogExposeEntity extends LitElement { inset-inline-end: 16px; direction: var(--direction); } + lit-virtualizer { + width: 100%; + contain: size layout !important; + } + ha-check-list-item { + width: 100%; + height: 72px; + } + ha-check-list-item ha-state-icon { + margin-left: 24px; + margin-inline-start: 24; + margin-inline-end: initial; + direction: var(--direction); + } `, ]; } diff --git a/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts index 62c35bdc2b..89f1222fe8 100644 --- a/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts +++ b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts @@ -1,9 +1,19 @@ +import { + mdiBug, + mdiClose, + mdiDotsVertical, + mdiStar, + mdiStarOutline, +} from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; +import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; +import { navigate } from "../../../common/navigate"; import "../../../components/ha-button"; -import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-form/ha-form"; +import "../../../components/ha-header-bar"; import { AssistPipeline, AssistPipelineMutableParams, @@ -11,8 +21,8 @@ import { } from "../../../data/assist_pipeline"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; -import "./assist-pipeline-detail/assist-pipeline-detail-conversation"; import "./assist-pipeline-detail/assist-pipeline-detail-config"; +import "./assist-pipeline-detail/assist-pipeline-detail-conversation"; import "./assist-pipeline-detail/assist-pipeline-detail-stt"; import "./assist-pipeline-detail/assist-pipeline-detail-tts"; import "./debug/assist-render-pipeline-events"; @@ -74,21 +84,62 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { return nothing; } + const title = this._params.pipeline?.id + ? this._params.pipeline.name + : this.hass.localize( + "ui.panel.config.voice_assistants.assistants.pipeline.detail.add_assistant_title" + ); + return html` + + +
${title}
+ ${this._params.pipeline?.id + ? html` + + + + + + ${this.hass.localize( + "ui.panel.config.voice_assistants.assistants.pipeline.detail.debug" + )} + + + + ` + : nothing} +
${this._error ? html`${this._error}` @@ -111,8 +162,8 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { (this._data.tts_engine === "cloud" || this._data.stt_engine === "cloud") ? html` - ${this.hass.localize( + + ${this.hass.localize( "ui.panel.config.voice_assistants.assistants.pipeline.detail.no_cloud_message" )} ${this.hass.localize("ui.common.delete")} - Set as preferred - Debug - ` : nothing} + > = {}; + protected willUpdate(changedProps: PropertyValues) { if (!isComponentLoaded(this.hass, "cloud")) { return; } if (changedProps.has("entry") && this.entry) { - fetchCloudGoogleEntity(this.hass, this.entry.entity_id).then( - (googleEntity) => { - this._googleEntity = googleEntity; - } - ); + this._fetchEntities(); } if (!this.hasUpdated) { fetchCloudStatus(this.hass).then((status) => { @@ -71,6 +72,31 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { } } + private async _fetchEntities() { + try { + const googleEntity = await fetchCloudGoogleEntity( + this.hass, + this.entry.entity_id + ); + this._googleEntity = googleEntity; + this.requestUpdate("_googleEntity"); + } catch (err: any) { + if (err.code === "not_supported") { + this._unsupported["cloud.google_assistant"] = true; + this.requestUpdate("_unsupported"); + } + } + + try { + await fetchCloudAlexaEntity(this.hass, this.entry.entity_id); + } catch (err: any) { + if (err.code === "not_supported") { + this._unsupported["cloud.alexa"] = true; + this.requestUpdate("_unsupported"); + } + } + } + private _getEntityFilterFuncs = memoizeOne( (googleFilter: EntityFilter, alexaFilter: EntityFilter) => ({ google: generateFilter( @@ -163,9 +189,28 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { > ${anyExposed - ? showAssistants.map( - (key) => html` - + ? showAssistants.map((key) => { + const supported = !this._unsupported[key]; + + const exposed = + alexaManual && key === "cloud.alexa" + ? manExposedAlexa + : googleManual && key === "cloud.google_assistant" + ? manExposedGoogle + : this.entry.options?.[key]?.should_expose; + + const manualConfig = + (alexaManual && key === "cloud.alexa") || + (googleManual && key === "cloud.google_assistant"); + + const support2fa = + key === "cloud.google_assistant" && + !googleManual && + supported && + this._googleEntity?.might_2fa; + + return html` + ${voiceAssistants[key].name} - ${key === "cloud.google_assistant" && - !googleManual && - this._googleEntity?.might_2fa + ${!supported + ? html`
+ ${this.hass.localize( + "ui.dialogs.voice-settings.unsupported" + )} +
` + : nothing} + ${manualConfig + ? html` +
+ ${this.hass.localize( + "ui.dialogs.voice-settings.manual_config" + )} +
+ ` + : nothing} + ${support2fa ? html` ` - : (alexaManual && key === "cloud.alexa") || - (googleManual && key === "cloud.google_assistant") - ? html` - - ${this.hass.localize( - "ui.dialogs.voice-settings.manual_config" - )} - - ` : nothing}
- ` - ) + `; + }) : nothing}

@@ -283,9 +328,15 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { } private async _toggleAll(ev) { + const expose = ev.target.checked; + + const assistants = expose + ? ev.target.assistants.filter((key) => !this._unsupported[key]) + : ev.target.assistants; + exposeEntities( this.hass, - ev.target.assistants, + assistants, [this.entry.entity_id], ev.target.checked ); @@ -305,6 +356,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { margin: 32px; margin-top: 0; --settings-row-prefix-display: contents; + --settings-row-content-display: contents; } ha-settings-row { padding: 0; diff --git a/src/panels/config/voice-assistants/expose/expose-assistant-icon.ts b/src/panels/config/voice-assistants/expose/expose-assistant-icon.ts new file mode 100644 index 0000000000..ed4daf7086 --- /dev/null +++ b/src/panels/config/voice-assistants/expose/expose-assistant-icon.ts @@ -0,0 +1,102 @@ +import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; +import { mdiAlertCircle } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { voiceAssistants } from "../../../../data/voice"; +import { HomeAssistant } from "../../../../types"; +import { brandsUrl } from "../../../../util/brands-url"; +import "../../../../components/ha-svg-icon"; + +@customElement("voice-assistants-expose-assistant-icon") +export class VoiceAssistantExposeAssistantIcon extends LitElement { + @property() public hass!: HomeAssistant; + + @property({ type: Boolean }) public unsupported!: boolean; + + @property({ type: Boolean }) public manual?: boolean; + + @property() public assistant?: + | "conversation" + | "cloud.alexa" + | "cloud.google_assistant"; + + render() { + if (!this.assistant || !voiceAssistants[this.assistant]) return nothing; + + return html` +
+ + ${this.unsupported + ? html` + + ` + : nothing} + ${this.manual || this.unsupported + ? html` + + ${this.unsupported + ? this.hass.localize( + "ui.panel.config.voice_assistants.expose.not_supported" + ) + : ""} + ${this.unsupported && this.manual ? html`
` : nothing} + ${this.manual + ? this.hass.localize( + "ui.panel.config.voice_assistants.expose.manually_configured" + ) + : nothing} +
+ ` + : ""} +
+ `; + } + + static get styles(): CSSResultGroup { + return css` + .container { + position: relative; + } + .logo { + position: relative; + height: 24px; + margin-right: 16px; + } + .unsupported { + color: var(--error-color); + position: absolute; + --mdc-icon-size: 16px; + right: 10px; + top: -7px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "voice-assistants-expose-assistant-icon": VoiceAssistantExposeAssistantIcon; + } +} diff --git a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts index c26159d756..707e1af95f 100644 --- a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts +++ b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts @@ -3,14 +3,21 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import { mdiCloseBoxMultiple, mdiCloseCircleOutline, + mdiFilterVariant, mdiPlus, mdiPlusBoxMultiple, } from "@mdi/js"; -import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + PropertyValues, +} from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; -import { styleMap } from "lit/directives/style-map"; import memoize from "memoize-one"; import { HASSDomEvent } from "../../../common/dom/fire_event"; import { @@ -27,6 +34,7 @@ import { SelectionChangedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/ha-fab"; +import { AlexaEntity, fetchCloudAlexaEntities } from "../../../data/alexa"; import { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud"; import { entitiesContext } from "../../../data/context"; import { @@ -35,6 +43,10 @@ import { ExtEntityRegistryEntry, getExtendedEntityRegistryEntries, } from "../../../data/entity_registry"; +import { + fetchCloudGoogleEntities, + GoogleEntity, +} from "../../../data/google_assistant"; import { exposeEntities, voiceAssistants } from "../../../data/voice"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-loading-screen"; @@ -42,10 +54,10 @@ import "../../../layouts/hass-tabs-subpage-data-table"; import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; -import { brandsUrl } from "../../../util/brands-url"; import { voiceAssistantTabs } from "./ha-config-voice-assistants"; import { showExposeEntityDialog } from "./show-dialog-expose-entity"; import { showVoiceSettingsDialog } from "./show-dialog-voice-settings"; +import "./expose/expose-assistant-icon"; @customElement("ha-config-voice-assistants-expose") export class VoiceAssistantsExpose extends LitElement { @@ -73,6 +85,11 @@ export class VoiceAssistantsExpose extends LitElement { @state() private _selectedEntities: string[] = []; + @state() private _supportedEntities?: Record< + "cloud.google_assistant" | "cloud.alexa" | "conversation", + string[] | undefined + >; + @query("hass-tabs-subpage-data-table", true) private _dataTable!: HaTabsSubpageDataTable; @@ -139,35 +156,23 @@ export class VoiceAssistantsExpose extends LitElement { width: "160px", type: "flex", template: (assistants, entry) => - html`${availableAssistants.map((key) => - assistants.includes(key) - ? html`
- ${entry.manAssistants?.includes(key) - ? html` - Configured in YAML, not editable in UI - ` - : ""} -
` - : html`
` - )}`, + html`${availableAssistants.map((key) => { + const supported = + !this._supportedEntities?.[key] || + this._supportedEntities[key].includes(entry.entity_id); + const manual = entry.manAssistants?.includes(key); + return assistants.includes(key) + ? html` + + + ` + : html`
`; + })}`, }, aliases: { title: this.hass.localize( @@ -197,6 +202,12 @@ export class VoiceAssistantsExpose extends LitElement { .path=${mdiCloseCircleOutline} >`, }, + // For search + entity_id: { + title: "", + hidden: true, + filterable: true, + }, }) ); @@ -423,16 +434,36 @@ export class VoiceAssistantsExpose extends LitElement { }); } - private async _fetchExtendedEntities() { + private async _fetchEntities() { this._extEntities = await getExtendedEntityRegistryEntries( this.hass, Object.keys(this._entities) ); + let alexaEntitiesProm: Promise | undefined; + let googleEntitiesProm: Promise | undefined; + if (this.cloudStatus?.logged_in && this.cloudStatus.prefs.alexa_enabled) { + alexaEntitiesProm = fetchCloudAlexaEntities(this.hass); + } + if (this.cloudStatus?.logged_in && this.cloudStatus.prefs.google_enabled) { + googleEntitiesProm = fetchCloudGoogleEntities(this.hass); + } + const [alexaEntities, googleEntities] = await Promise.all([ + alexaEntitiesProm, + googleEntitiesProm, + ]); + this._supportedEntities = { + "cloud.alexa": alexaEntities?.map((entity) => entity.entity_id), + "cloud.google_assistant": googleEntities?.map( + (entity) => entity.entity_id + ), + // TODO add supported entity for assit + conversation: undefined, + }; } public willUpdate(changedProperties: PropertyValues): void { if (changedProperties.has("_entities")) { - this._fetchExtendedEntities(); + this._fetchEntities(); } } @@ -560,6 +591,26 @@ export class VoiceAssistantsExpose extends LitElement { > + ${this.narrow && activeFilters?.length + ? html` + + + + ${this.hass.localize("ui.components.data-table.filtering_by")} + ${activeFilters.join(", ")} + + ${this.hass.localize("ui.common.clear")} + + + + ` + : nothing} `; } diff --git a/src/translations/en.json b/src/translations/en.json index 429e122a68..7b359c878c 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1083,7 +1083,8 @@ "aliases_header": "Aliases", "aliases_description": "Aliases are supported by Assist and Google Assistant.", "ask_pin": "Ask for PIN", - "manual_config": "Managed with filters in configuration.yaml" + "manual_config": "Managed in configuration.yaml", + "unsupported": "Unsupported" }, "restart": { "heading": "Restart Home Assistant", @@ -2039,6 +2040,8 @@ "add_assistant_title": "Add assistant", "add_assistant_action": "Create", "try_tts": "Try voice", + "debug": "Debug", + "set_as_preferred": "Set as preferred", "form": { "name": "Name", "conversation_engine": "Conversation agent", @@ -2105,6 +2108,8 @@ "expose_confirm_text": "Do you want to expose {entities} entities to {assistants}?", "unexpose_confirm_title": "Stop exposing selected entities?", "unexpose_confirm_text": "Do you want to stop exposing {entities} entities to {assistants}?", + "manually_configured": "Configured in YAML, not editable in UI", + "not_supported": "Not supported by this assistant", "expose_dialog": { "header": "Expose entities", "expose_to": "to {assistants}", diff --git a/yarn.lock b/yarn.lock index e924891a48..ff5990f393 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16585,9 +16585,9 @@ __metadata: linkType: hard "yaml@npm:^2.2.1": - version: 2.2.1 - resolution: "yaml@npm:2.2.1" - checksum: 84f68cbe462d5da4e7ded4a8bded949ffa912bc264472e5a684c3d45b22d8f73a3019963a32164023bdf3d83cfb6f5b58ff7b2b10ef5b717c630f40bd6369a23 + version: 2.2.2 + resolution: "yaml@npm:2.2.2" + checksum: d90c235e099e30094dcff61ba3350437aef53325db4a6bcd04ca96e1bfe7e348b191f6a7a52b5211e2dbc4eeedb22a00b291527da030de7c189728ef3f2b4eb3 languageName: node linkType: hard