diff --git a/pyproject.toml b/pyproject.toml index d373f50e27d8..1803d94af103 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20241127.1" +version = "20241127.2" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/data/hassio/supervisor.ts b/src/data/hassio/supervisor.ts index b14ffd8e56c0..a013aab07d8c 100644 --- a/src/data/hassio/supervisor.ts +++ b/src/data/hassio/supervisor.ts @@ -226,6 +226,25 @@ export const fetchHassioLogsFollow = async ( signal ); +export const fetchHassioLogsFollowSkip = async ( + hass: HomeAssistant, + provider: string, + signal: AbortSignal, + cursor: string, + skipLines: number, + lines = 100, + boot = 0 +) => + hass.callApiRaw( + "GET", + `hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs${boot !== 0 ? `/boots/${boot}` : ""}/follow`, + undefined, + { + Range: `entries=${cursor}:${skipLines}:${lines}`, + }, + signal + ); + export const getHassioLogDownloadUrl = (provider: string) => `/api/hassio/${ provider.includes("_") ? `addons/${provider}` : provider diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-local.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-local.ts index 1fb690601f9d..69c50ef57192 100644 --- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-local.ts +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-local.ts @@ -37,6 +37,8 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement { @state() private _detailState?: string; + @state() private _error?: string; + @state() private _localTts?: EntityRegistryDisplayEntry[]; @state() private _localStt?: EntityRegistryDisplayEntry[]; @@ -62,6 +64,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement { alt="Casita Home Assistant error logo" />

Failed to install add-ons

+

${this._error}

We could not automatically install a local TTS and STT provider for you. Read the documentation to learn how to install them. @@ -179,8 +182,9 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement { } this._detailState = "Creating assistant"; await this._findEntitiesAndCreatePipeline(); - } catch (e) { + } catch (e: any) { this._state = "ERROR"; + this._error = e.message; } } @@ -199,11 +203,13 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement { private async _setupConfigEntry(addon: string) { const configFlow = await createConfigFlow(this.hass, "wyoming"); const step = await handleConfigFlowStep(this.hass, configFlow.flow_id, { - host: `core_${addon}`, + host: `core-${addon}`, port: addon === "piper" ? 10200 : 10300, }); if (step.type !== "create_entry") { - throw new Error("Failed to create entry"); + throw new Error( + `Failed to create entry for ${addon}${"errors" in step ? `: ${step.errors.base}` : ""}` + ); } } @@ -321,7 +327,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement { this._findLocalEntities(); if (!this._localTts?.length || !this._localStt?.length) { if (tryNo > 3) { - throw new Error("Timeout searching for local TTS and STT entities"); + throw new Error("Could not find local TTS and STT entities"); } await new Promise((resolve) => { setTimeout(resolve, 2000); diff --git a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts index 4b7d02013459..42c311bef2de 100644 --- a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts +++ b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts @@ -134,12 +134,13 @@ export class HaVoiceCommandDialog extends LitElement { const controlHA = !this._pipeline ? false - : this.hass.states[this._pipeline?.conversation_engine] - ? supportsFeature( - this.hass.states[this._pipeline?.conversation_engine], - ConversationEntityFeature.CONTROL - ) - : true; + : this._pipeline.prefer_local_intents || + (this.hass.states[this._pipeline.conversation_engine] + ? supportsFeature( + this.hass.states[this._pipeline.conversation_engine], + ConversationEntityFeature.CONTROL + ) + : true); const supportsMicrophone = AudioRecorder.isSupported; const supportsSTT = this._pipeline?.stt_engine; diff --git a/src/panels/config/logs/error-log-card.ts b/src/panels/config/logs/error-log-card.ts index a5bd9e0b5f5c..0f551df687dd 100644 --- a/src/panels/config/logs/error-log-card.ts +++ b/src/panels/config/logs/error-log-card.ts @@ -50,6 +50,7 @@ import { fetchHassioBoots, fetchHassioLogs, fetchHassioLogsFollow, + fetchHassioLogsFollowSkip, fetchHassioLogsLegacy, getHassioLogDownloadLinesUrl, getHassioLogDownloadUrl, @@ -428,13 +429,21 @@ class ErrorLogCard extends LitElement { } } - private async _loadLogs(): Promise { + private async _loadLogs(retry = false): Promise { this._error = undefined; this._loadingState = "loading"; - this._loadingPrevState = undefined; - this._firstCursor = undefined; - this._numberOfLines = 0; - this._ansiToHtmlElement?.clear(); + this._numberOfLines = retry ? (this._numberOfLines ?? 0) : 0; + + if (!retry) { + this._loadingPrevState = undefined; + this._firstCursor = undefined; + this._ansiToHtmlElement?.clear(); + } + + const streamLogs = + this._streamSupported && + isComponentLoaded(this.hass, "hassio") && + this.provider; try { if (this._logStreamAborter) { @@ -442,32 +451,44 @@ class ErrorLogCard extends LitElement { this._logStreamAborter = undefined; } - if ( - this._streamSupported && - isComponentLoaded(this.hass, "hassio") && - this.provider - ) { + if (streamLogs) { this._logStreamAborter = new AbortController(); - // check if there are any logs at all - const testResponse = await fetchHassioLogs( - this.hass, - this.provider, - `entries=:-1:`, - this._boot - ); - const testLogs = await testResponse.text(); - if (!testLogs.trim()) { - this._loadingState = "empty"; + if (!retry) { + // check if there are any logs at all + const testResponse = await fetchHassioLogs( + this.hass, + this.provider!, + `entries=:-1:`, + this._boot + ); + const testLogs = await testResponse.text(); + if (!testLogs.trim()) { + this._loadingState = "empty"; + } } - const response = await fetchHassioLogsFollow( - this.hass, - this.provider, - this._logStreamAborter.signal, - NUMBER_OF_LINES, - this._boot - ); + let response: Response; + + if (retry && this._firstCursor) { + response = await fetchHassioLogsFollowSkip( + this.hass, + this.provider!, + this._logStreamAborter.signal, + this._firstCursor, + this._numberOfLines, + NUMBER_OF_LINES, + this._boot + ); + } else { + response = await fetchHassioLogsFollow( + this.hass, + this.provider!, + this._logStreamAborter.signal, + NUMBER_OF_LINES, + this._boot + ); + } if (response.headers.has("X-First-Cursor")) { this._firstCursor = response.headers.get("X-First-Cursor")!; @@ -524,7 +545,7 @@ class ErrorLogCard extends LitElement { if (!this._downloadSupported) { const downloadUrl = getHassioLogDownloadLinesUrl( - this.provider, + this.provider!, this._numberOfLines, this._boot ); @@ -532,6 +553,9 @@ class ErrorLogCard extends LitElement { this._logsFileLink = signedUrl.path; }); } + + // first chunk loads successfully, reset retry param + retry = false; } } } else { @@ -554,6 +578,13 @@ class ErrorLogCard extends LitElement { if (err.name === "AbortError") { return; } + + // The stream can fail if the connection is lost or firefox service worker intercept the connection + if (!retry && streamLogs) { + this._loadLogs(true); + return; + } + this._error = (this.localizeFunc || this.hass.localize)( "ui.panel.config.logs.failed_get_logs", { @@ -590,9 +621,10 @@ class ErrorLogCard extends LitElement { private _handleConnectionStatus = (ev: HASSDomEvent) => { if (ev.detail === "disconnected" && this._logStreamAborter) { this._logStreamAborter.abort(); + this._loadingState = "loading"; } if (ev.detail === "connected") { - this._loadLogs(); + this._loadLogs(true); } }; diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index 80b6a97918a7..410aff18f97a 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -7,7 +7,9 @@ import { mdiContentSave, mdiDelete, mdiDotsVertical, + mdiEye, mdiInformationOutline, + mdiMotionPlayOutline, mdiPlay, mdiTag, } from "@mdi/js"; @@ -204,6 +206,14 @@ export class HaSceneEditor extends SubscribeMixin( } ); + public connectedCallback() { + super.connectedCallback(); + if (!this.sceneId) { + this._mode = "live"; + this._subscribeEvents(); + } + } + public disconnectedCallback() { super.disconnectedCallback(); if (this._unsubscribeEvents) { @@ -387,15 +397,22 @@ export class HaSceneEditor extends SubscribeMixin( alert-type="info" .narrow=${this.narrow} .title=${this.hass.localize( - `ui.panel.config.scene.editor.${this._mode === "live" ? "live_preview" : "review_mode"}` + `ui.panel.config.scene.editor.${this._mode === "live" ? "live_edit" : "review_mode"}` )} > ${this.hass.localize( - `ui.panel.config.scene.editor.${this._mode === "live" ? "live_preview_detail" : "review_mode_detail"}` + `ui.panel.config.scene.editor.${this._mode === "live" ? "live_edit_detail" : "review_mode_detail"}` )} + + + ${this.hass.localize( - `ui.panel.config.scene.editor.${this._mode === "live" ? "back_to_review_mode" : "live_preview"}` + `ui.panel.config.scene.editor.${this._mode === "live" ? "switch_to_review_mode" : "live_edit"}` )} @@ -542,6 +559,7 @@ export class HaSceneEditor extends SubscribeMixin( } return html` this._storeState(entity)); @@ -1309,6 +1329,9 @@ export class HaSceneEditor extends SubscribeMixin( li[role="separator"] { border-bottom-color: var(--divider-color); } + ha-list-item.entity { + padding-right: 28px; + } `, ]; } diff --git a/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts index 2b76aa30a97e..81ce443d72ab 100644 --- a/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts @@ -68,7 +68,7 @@ export class HuiHistoryGraphCardEditor { name: "hours_to_show", default: DEFAULT_HOURS_TO_SHOW, - selector: { number: { min: 1, mode: "box" } }, + selector: { number: { min: 0, step: "any", mode: "box" } }, }, ], }, diff --git a/src/translations/en.json b/src/translations/en.json index 8069cf58ad32..193d38ce1a94 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3878,11 +3878,12 @@ }, "editor": { "review_mode": "Review Mode", - "review_mode_detail": "You can adjust the scene's details and remove devices or entities. To fully edit, switch to Live Preview, which will apply the scene.", - "live_preview": "Live Preview", - "live_preview_detail": "In Live Preview, all changes to this scene are applied in real-time to your devices and entities.", - "enter_live_mode_unsaved": "You have unsaved changes to this scene. Continuing to live preview will apply the saved scene, which may overwrite your unsaved changes. Consider if you would like to save the scene first before activating it.", - "back_to_review_mode": "Back to review mode", + "review_mode_detail": "You can adjust the scene's details and remove devices or entities. To fully edit, switch to Live Edit, which will apply the scene.", + "live_edit": "Live Edit", + "live_edit_detail": "In Live Edit, all changes to this scene are applied in real-time to your devices and entities.", + "enter_live_mode_unsaved": "Before proceeding to Live Edit, please save your current changes.", + "save_before_live": "Save and Live Edit", + "switch_to_review_mode": "Switch to review mode", "default_name": "New scene", "load_error_not_editable": "Only scenes in scenes.yaml are editable.", "load_error_unknown": "Error loading scene ({err_no}).",