mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-30 22:19:55 +00:00 
			
		
		
		
	Compare commits
	
		
			17 Commits
		
	
	
		
			fix-entity
			...
			update-int
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 3703ffc42d | ||
|   | 9ea8e13c87 | ||
|   | 604b79696e | ||
|   | 8c445f6409 | ||
|   | 797c871137 | ||
|   | 24829bd903 | ||
|   | add92a559d | ||
|   | 17018c0f26 | ||
|   | cd6a478130 | ||
|   | 4f6d7ca5c9 | ||
|   | c2994343b4 | ||
|   | e5f77c35d4 | ||
|   | a9e5a5dd44 | ||
|   | 1159798b8d | ||
|   | 437de42c55 | ||
|   | 89e0bb3f16 | ||
|   | 28c9631b6c | 
| @@ -279,7 +279,7 @@ class DemoHaSelector extends LitElement implements ProvideHassElement { | ||||
|           can_play: true, | ||||
|           can_expand: false, | ||||
|           children_media_class: null, | ||||
|           thumbnail: null, | ||||
|           thumbnail: "https://brands.home-assistant.io/_/image/logo.png", | ||||
|         }, | ||||
|         { | ||||
|           title: "movie.mp4", | ||||
|   | ||||
| @@ -110,8 +110,6 @@ class HassioAddonStore extends LitElement { | ||||
|               <div class="search"> | ||||
|                 <search-input | ||||
|                   .hass=${this.hass} | ||||
|                   no-label-float | ||||
|                   no-underline | ||||
|                   .filter=${this._filter} | ||||
|                   @value-changed=${this._filterChanged} | ||||
|                 ></search-input> | ||||
|   | ||||
| @@ -80,8 +80,6 @@ class HassioHardwareDialog extends LitElement { | ||||
|           ></ha-icon-button> | ||||
|           <search-input | ||||
|             .hass=${this.hass} | ||||
|             dialogInitialFocus | ||||
|             no-label-float | ||||
|             .filter=${this._filter} | ||||
|             @value-changed=${this._handleSearchChange} | ||||
|             .label=${this._dialogParams.supervisor.localize( | ||||
|   | ||||
| @@ -106,6 +106,9 @@ class HassioRepositoriesDialog extends LitElement { | ||||
|                     </paper-item-body> | ||||
|                     <div class="delete"> | ||||
|                       <ha-icon-button | ||||
|                         .label=${this._dialogParams!.supervisor.localize( | ||||
|                           "dialog.repositories.remove" | ||||
|                         )} | ||||
|                         .disabled=${usedRepositories.includes(repo.slug)} | ||||
|                         .slug=${repo.slug} | ||||
|                         .path=${usedRepositories.includes(repo.slug) | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| // Compat needs to be first import | ||||
| import "../../src/resources/compatibility"; | ||||
| import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings"; | ||||
| import "../../src/resources/roboto"; | ||||
| import "../../src/resources/safari-14-attachshadow-patch"; | ||||
| import "./hassio-main"; | ||||
|  | ||||
| setCancelSyntheticClickEvents(false); | ||||
|  | ||||
| const styleEl = document.createElement("style"); | ||||
| styleEl.innerHTML = ` | ||||
| body { | ||||
|   | ||||
| @@ -45,7 +45,6 @@ import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; | ||||
| import "../../../src/layouts/hass-loading-screen"; | ||||
| import "../../../src/layouts/hass-subpage"; | ||||
| import "../../../src/layouts/hass-tabs-subpage"; | ||||
| import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates"; | ||||
| import { HomeAssistant, Route } from "../../../src/types"; | ||||
| import { addonArchIsSupported, extractChangelog } from "../util/addon"; | ||||
|  | ||||
| @@ -57,6 +56,12 @@ declare global { | ||||
|  | ||||
| type updateType = "os" | "supervisor" | "core" | "addon"; | ||||
|  | ||||
| const SUPERVISOR_UPDATE_NAMES = { | ||||
|   core: "Home Assistant Core", | ||||
|   os: "Home Assistant Operating System", | ||||
|   supervisor: "Home Assistant Supervisor", | ||||
| }; | ||||
|  | ||||
| const changelogUrl = ( | ||||
|   entry: updateType, | ||||
|   version: string | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| [metadata] | ||||
| name         = home-assistant-frontend | ||||
| version      = 20220226.0 | ||||
| version      = 20220301.0 | ||||
| author       = The Home Assistant Authors | ||||
| author_email = hello@home-assistant.io | ||||
| license      = Apache-2.0 | ||||
|   | ||||
| @@ -115,6 +115,9 @@ class DateRangePickerElement extends WrappedElement { | ||||
|             color: var(--primary-text-color); | ||||
|             min-width: initial !important; | ||||
|           } | ||||
|           .daterangepicker:before { | ||||
|             display: none; | ||||
|           } | ||||
|           .daterangepicker:after { | ||||
|             border-bottom: 6px solid var(--card-background-color); | ||||
|           } | ||||
|   | ||||
| @@ -51,10 +51,11 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { | ||||
|   private _filterEntities = (entity: HassEntity): boolean => { | ||||
|     if (this.selector.entity?.domain) { | ||||
|       const filterDomain = this.selector.entity.domain; | ||||
|       const filterDomainIsArray = Array.isArray(filterDomain); | ||||
|       const entityDomain = computeStateDomain(entity); | ||||
|       if ( | ||||
|         (Array.isArray(filterDomain) && !filterDomain.includes(entityDomain)) || | ||||
|         entityDomain !== filterDomain | ||||
|         (filterDomainIsArray && !filterDomain.includes(entityDomain)) || | ||||
|         (!filterDomainIsArray && entityDomain !== filterDomain) | ||||
|       ) { | ||||
|         return false; | ||||
|       } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import { | ||||
| } from "../../data/media-player"; | ||||
| import type { MediaSelector, MediaSelectorValue } from "../../data/selector"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url"; | ||||
| import "../ha-alert"; | ||||
| import "../ha-form/ha-form"; | ||||
| import type { HaFormSchema } from "../ha-form/types"; | ||||
| @@ -50,6 +51,18 @@ export class HaMediaSelector extends LitElement { | ||||
|         getSignedPath(this.hass, thumbnail).then((signedPath) => { | ||||
|           this._thumbnailUrl = signedPath.path; | ||||
|         }); | ||||
|       } else if ( | ||||
|         thumbnail && | ||||
|         thumbnail.startsWith("https://brands.home-assistant.io") | ||||
|       ) { | ||||
|         // The backend is not aware of the theme used by the users, | ||||
|         // so we rewrite the URL to show a proper icon | ||||
|         this._thumbnailUrl = brandsUrl({ | ||||
|           domain: extractDomainFromBrandUrl(thumbnail), | ||||
|           type: "icon", | ||||
|           useFallback: true, | ||||
|           darkOptimized: this.hass.themes?.darkMode, | ||||
|         }); | ||||
|       } else { | ||||
|         this._thumbnailUrl = thumbnail; | ||||
|       } | ||||
|   | ||||
| @@ -42,9 +42,7 @@ export class HaTab extends LitElement { | ||||
|         @keydown=${this._handleKeyDown} | ||||
|       > | ||||
|         ${this.narrow ? html`<slot name="icon"></slot>` : ""} | ||||
|         ${!this.narrow || this.active | ||||
|           ? html`<span class="name">${this.name}</span>` | ||||
|           : ""} | ||||
|         <span class="name">${this.name}</span> | ||||
|         ${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""} | ||||
|       </div> | ||||
|     `; | ||||
|   | ||||
| @@ -34,23 +34,24 @@ import { | ||||
|   MediaPickedEvent, | ||||
|   MediaPlayerBrowseAction, | ||||
| } from "../../data/media-player"; | ||||
| import { browseLocalMediaPlayer } from "../../data/media_source"; | ||||
| import { isTTSMediaSource } from "../../data/tts"; | ||||
| import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; | ||||
| import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer"; | ||||
| import { haStyle } from "../../resources/styles"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url"; | ||||
| import { documentationUrl } from "../../util/documentation-url"; | ||||
| import "../entity/ha-entity-picker"; | ||||
| import "../ha-button-menu"; | ||||
| import "../ha-card"; | ||||
| import type { HaCard } from "../ha-card"; | ||||
| import "../ha-circular-progress"; | ||||
| import "../ha-fab"; | ||||
| import "../ha-icon-button"; | ||||
| import "../ha-svg-icon"; | ||||
| import "../ha-fab"; | ||||
| import { browseLocalMediaPlayer } from "../../data/media_source"; | ||||
| import { isTTSMediaSource } from "../../data/tts"; | ||||
| import type { TtsMediaPickedEvent } from "./ha-browse-media-tts"; | ||||
| import "./ha-browse-media-tts"; | ||||
| import type { TtsMediaPickedEvent } from "./ha-browse-media-tts"; | ||||
|  | ||||
| declare global { | ||||
|   interface HASSDomEvents { | ||||
| @@ -681,6 +682,17 @@ export class HaMediaPlayerBrowse extends LitElement { | ||||
|                 // Thumbnails served by local API require authentication | ||||
|                 const signedPath = await getSignedPath(this.hass, thumbnailUrl); | ||||
|                 thumbnailUrl = signedPath.path; | ||||
|               } else if ( | ||||
|                 thumbnailUrl.startsWith("https://brands.home-assistant.io") | ||||
|               ) { | ||||
|                 // The backend is not aware of the theme used by the users, | ||||
|                 // so we rewrite the URL to show a proper icon | ||||
|                 thumbnailUrl = brandsUrl({ | ||||
|                   domain: extractDomainFromBrandUrl(thumbnailUrl), | ||||
|                   type: "icon", | ||||
|                   useFallback: true, | ||||
|                   darkOptimized: this.hass.themes?.darkMode, | ||||
|                 }); | ||||
|               } | ||||
|               thumbnailCard.style.backgroundImage = `url(${thumbnailUrl})`; | ||||
|               observer.unobserve(thumbnailCard); // loaded, so no need to observe anymore | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { HomeAssistant } from "../types"; | ||||
|  | ||||
| export interface InputDateTime { | ||||
| @@ -17,6 +18,19 @@ export interface InputDateTimeMutableParams { | ||||
|   has_date: boolean; | ||||
| } | ||||
|  | ||||
| export const stateToIsoDateString = (entityState: HassEntity) => | ||||
|   `${entityState.attributes.year || "1970"}-${String( | ||||
|     entityState.attributes.month || "01" | ||||
|   ).padStart(2, "0")}-${String(entityState.attributes.day || "01").padStart( | ||||
|     2, | ||||
|     "0" | ||||
|   )}T${String(entityState.attributes.hour || "00").padStart(2, "0")}:${String( | ||||
|     entityState.attributes.minute || "00" | ||||
|   ).padStart(2, "0")}:${String(entityState.attributes.second || "00").padStart( | ||||
|     2, | ||||
|     "0" | ||||
|   )}`; | ||||
|  | ||||
| export const setInputDateTimeValue = ( | ||||
|   hass: HomeAssistant, | ||||
|   entityId: string, | ||||
|   | ||||
| @@ -1,58 +0,0 @@ | ||||
| import { HomeAssistant } from "../../types"; | ||||
|  | ||||
| interface SupervisorBaseAvailableUpdates { | ||||
|   panel_path?: string; | ||||
|   update_type?: string; | ||||
|   version_latest?: string; | ||||
| } | ||||
|  | ||||
| interface SupervisorAddonAvailableUpdates | ||||
|   extends SupervisorBaseAvailableUpdates { | ||||
|   update_type?: "addon"; | ||||
|   icon?: string; | ||||
|   name?: string; | ||||
| } | ||||
|  | ||||
| interface SupervisorCoreAvailableUpdates | ||||
|   extends SupervisorBaseAvailableUpdates { | ||||
|   update_type?: "core"; | ||||
| } | ||||
|  | ||||
| interface SupervisorOsAvailableUpdates extends SupervisorBaseAvailableUpdates { | ||||
|   update_type?: "os"; | ||||
| } | ||||
|  | ||||
| interface SupervisorSupervisorAvailableUpdates | ||||
|   extends SupervisorBaseAvailableUpdates { | ||||
|   update_type?: "supervisor"; | ||||
| } | ||||
|  | ||||
| export type SupervisorAvailableUpdates = | ||||
|   | SupervisorAddonAvailableUpdates | ||||
|   | SupervisorCoreAvailableUpdates | ||||
|   | SupervisorOsAvailableUpdates | ||||
|   | SupervisorSupervisorAvailableUpdates; | ||||
|  | ||||
| export interface SupervisorAvailableUpdatesResponse { | ||||
|   available_updates: SupervisorAvailableUpdates[]; | ||||
| } | ||||
|  | ||||
| export const fetchSupervisorAvailableUpdates = async ( | ||||
|   hass: HomeAssistant | ||||
| ): Promise<SupervisorAvailableUpdates[]> => | ||||
|   ( | ||||
|     await hass.callWS<SupervisorAvailableUpdatesResponse>({ | ||||
|       type: "supervisor/api", | ||||
|       endpoint: "/available_updates", | ||||
|       method: "get", | ||||
|     }) | ||||
|   ).available_updates; | ||||
|  | ||||
| export const refreshSupervisorAvailableUpdates = async ( | ||||
|   hass: HomeAssistant | ||||
| ): Promise<void> => | ||||
|   hass.callWS<void>({ | ||||
|     type: "supervisor/api", | ||||
|     endpoint: "/refresh_updates", | ||||
|     method: "post", | ||||
|   }); | ||||
							
								
								
									
										37
									
								
								src/data/update.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/data/update.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| import { HomeAssistant } from "../types"; | ||||
|  | ||||
| export interface UpdateDescription { | ||||
|   identifier: string; | ||||
|   name: string; | ||||
|   domain: string; | ||||
|   current_version: string; | ||||
|   available_version: string; | ||||
|   changelog_content: string | null; | ||||
|   changelog_url: string | null; | ||||
|   icon_url: string | null; | ||||
|   supports_backup: boolean; | ||||
| } | ||||
|  | ||||
| export interface SkipUpdateParams { | ||||
|   domain: string; | ||||
|   version: string; | ||||
|   identifier: string; | ||||
| } | ||||
|  | ||||
| export interface PerformUpdateParams extends SkipUpdateParams { | ||||
|   backup?: boolean; | ||||
| } | ||||
|  | ||||
| export const fetchUpdateInfo = ( | ||||
|   hass: HomeAssistant | ||||
| ): Promise<UpdateDescription[]> => hass.callWS({ type: "update/info" }); | ||||
|  | ||||
| export const skipUpdate = ( | ||||
|   hass: HomeAssistant, | ||||
|   params: SkipUpdateParams | ||||
| ): Promise<void> => hass.callWS({ type: "update/skip", ...params }); | ||||
|  | ||||
| export const performUpdate = ( | ||||
|   hass: HomeAssistant, | ||||
|   params: PerformUpdateParams | ||||
| ): Promise<void> => hass.callWS({ type: "update/update", ...params }); | ||||
| @@ -117,13 +117,17 @@ class DataEntryFlowDialog extends LitElement { | ||||
|         ); | ||||
|       } catch (err: any) { | ||||
|         this.closeDialog(); | ||||
|         let message = err.message || err.body || "Unknown error"; | ||||
|         if (typeof message !== "string") { | ||||
|           message = JSON.stringify(message); | ||||
|         } | ||||
|         showAlertDialog(this, { | ||||
|           title: this.hass.localize( | ||||
|             "ui.panel.config.integrations.config_flow.error" | ||||
|           ), | ||||
|           text: `${this.hass.localize( | ||||
|             "ui.panel.config.integrations.config_flow.could_not_load" | ||||
|           )}: ${err.message || err.body}`, | ||||
|           )}: ${message}`, | ||||
|         }); | ||||
|         return; | ||||
|       } | ||||
|   | ||||
| @@ -4,7 +4,10 @@ import { customElement, property } from "lit/decorators"; | ||||
| import "../../../components/ha-date-input"; | ||||
| import "../../../components/ha-time-input"; | ||||
| import { UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity"; | ||||
| import { setInputDateTimeValue } from "../../../data/input_datetime"; | ||||
| import { | ||||
|   setInputDateTimeValue, | ||||
|   stateToIsoDateString, | ||||
| } from "../../../data/input_datetime"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
|  | ||||
| @customElement("more-info-input_datetime") | ||||
| @@ -24,7 +27,7 @@ class MoreInfoInputDatetime extends LitElement { | ||||
|             ? html` | ||||
|                 <ha-date-input | ||||
|                   .locale=${this.hass.locale} | ||||
|                   .value=${`${this.stateObj.attributes.year}-${this.stateObj.attributes.month}-${this.stateObj.attributes.day}`} | ||||
|                   .value=${stateToIsoDateString(this.stateObj)} | ||||
|                   .disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)} | ||||
|                   @value-changed=${this._dateChanged} | ||||
|                 > | ||||
|   | ||||
| @@ -86,11 +86,11 @@ export class QuickBar extends LitElement { | ||||
|  | ||||
|   @state() private _search = ""; | ||||
|  | ||||
|   @state() private _opened = false; | ||||
|   @state() private _open = false; | ||||
|  | ||||
|   @state() private _commandMode = false; | ||||
|  | ||||
|   @state() private _done = false; | ||||
|   @state() private _opened = false; | ||||
|  | ||||
|   @state() private _narrow = false; | ||||
|  | ||||
| @@ -109,12 +109,12 @@ export class QuickBar extends LitElement { | ||||
|       "all and (max-width: 450px), all and (max-height: 500px)" | ||||
|     ).matches; | ||||
|     this._initializeItemsIfNeeded(); | ||||
|     this._opened = true; | ||||
|     this._open = true; | ||||
|   } | ||||
|  | ||||
|   public closeDialog() { | ||||
|     this._open = false; | ||||
|     this._opened = false; | ||||
|     this._done = false; | ||||
|     this._focusSet = false; | ||||
|     this._filter = ""; | ||||
|     this._search = ""; | ||||
| @@ -133,7 +133,7 @@ export class QuickBar extends LitElement { | ||||
|   ); | ||||
|  | ||||
|   protected render() { | ||||
|     if (!this._opened) { | ||||
|     if (!this._open) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
| @@ -218,24 +218,26 @@ export class QuickBar extends LitElement { | ||||
|             ` | ||||
|           : html` | ||||
|               <mwc-list> | ||||
|                 <lit-virtualizer | ||||
|                   scroller | ||||
|                   @keydown=${this._handleListItemKeyDown} | ||||
|                   @rangechange=${this._handleRangeChanged} | ||||
|                   @click=${this._handleItemClick} | ||||
|                   class="ha-scrollbar" | ||||
|                   style=${styleMap({ | ||||
|                     height: this._narrow | ||||
|                       ? "calc(100vh - 56px)" | ||||
|                       : `${Math.min( | ||||
|                           items.length * (this._commandMode ? 56 : 72) + 26, | ||||
|                           this._done ? 500 : 0 | ||||
|                         )}px`, | ||||
|                   })} | ||||
|                   .items=${items} | ||||
|                   .renderItem=${this._renderItem} | ||||
|                 > | ||||
|                 </lit-virtualizer> | ||||
|                 ${this._opened | ||||
|                   ? html`<lit-virtualizer | ||||
|                       scroller | ||||
|                       @keydown=${this._handleListItemKeyDown} | ||||
|                       @rangechange=${this._handleRangeChanged} | ||||
|                       @click=${this._handleItemClick} | ||||
|                       class="ha-scrollbar" | ||||
|                       style=${styleMap({ | ||||
|                         height: this._narrow | ||||
|                           ? "calc(100vh - 56px)" | ||||
|                           : `${Math.min( | ||||
|                               items.length * (this._commandMode ? 56 : 72) + 26, | ||||
|                               500 | ||||
|                             )}px`, | ||||
|                       })} | ||||
|                       .items=${items} | ||||
|                       .renderItem=${this._renderItem} | ||||
|                     > | ||||
|                     </lit-virtualizer>` | ||||
|                   : ""} | ||||
|               </mwc-list> | ||||
|             `} | ||||
|         ${this._hint ? html`<div class="hint">${this._hint}</div>` : ""} | ||||
| @@ -252,9 +254,7 @@ export class QuickBar extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _handleOpened() { | ||||
|     this.updateComplete.then(() => { | ||||
|       this._done = true; | ||||
|     }); | ||||
|     this._opened = true; | ||||
|   } | ||||
|  | ||||
|   private async _handleRangeChanged(e) { | ||||
| @@ -454,9 +454,10 @@ export class QuickBar extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _handleItemClick(ev) { | ||||
|     const listItem = ev.target.closest("mwc-list-item"); | ||||
|     this.processItemAndCloseDialog( | ||||
|       (ev.target as any).item, | ||||
|       Number((ev.target as HTMLElement).getAttribute("index")) | ||||
|       listItem.item, | ||||
|       Number(listItem.getAttribute("index")) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   | ||||
							
								
								
									
										211
									
								
								src/dialogs/update-dialog/ha-update-dialog.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								src/dialogs/update-dialog/ha-update-dialog.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,211 @@ | ||||
| import "@material/mwc-button/mwc-button"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { computeRTL } from "../../common/util/compute_rtl"; | ||||
| import "../../components/ha-alert"; | ||||
| import "../../components/ha-checkbox"; | ||||
| import "../../components/ha-circular-progress"; | ||||
| import { createCloseHeading } from "../../components/ha-dialog"; | ||||
| import "../../components/ha-faded"; | ||||
| import "../../components/ha-formfield"; | ||||
| import "../../components/ha-icon-button"; | ||||
| import "../../components/ha-markdown"; | ||||
| import { | ||||
|   performUpdate, | ||||
|   skipUpdate, | ||||
|   UpdateDescription, | ||||
| } from "../../data/update"; | ||||
| import { haStyleDialog } from "../../resources/styles"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import { UpdateDialogParams } from "./show-ha-update-dialog"; | ||||
|  | ||||
| @customElement("ha-update-dialog") | ||||
| export class HaUpdateDialog extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @state() private _opened = false; | ||||
|  | ||||
|   @state() private _updating = false; | ||||
|  | ||||
|   @state() private _error?: string; | ||||
|  | ||||
|   @state() private _update!: UpdateDescription; | ||||
|  | ||||
|   _refreshCallback!: () => void; | ||||
|  | ||||
|   public async showDialog(dialogParams: UpdateDialogParams): Promise<void> { | ||||
|     this._opened = true; | ||||
|     this._update = dialogParams.update; | ||||
|     this._refreshCallback = dialogParams.refreshCallback; | ||||
|   } | ||||
|  | ||||
|   public async closeDialog(): Promise<void> { | ||||
|     this._opened = false; | ||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this._opened) { | ||||
|       return html``; | ||||
|     } | ||||
|     return html` | ||||
|       <ha-dialog | ||||
|         open | ||||
|         @closed=${this.closeDialog} | ||||
|         scrimClickAction | ||||
|         .heading=${createCloseHeading( | ||||
|           this.hass, | ||||
|           this.hass.localize("ui.panel.config.updates.dialog.title", { | ||||
|             name: this._update.name, | ||||
|           }) | ||||
|         )} | ||||
|       > | ||||
|         <div> | ||||
|           ${this._error | ||||
|             ? html`<ha-alert alert-type="error" .rtl=${computeRTL(this.hass)}> | ||||
|                 ${this._error} | ||||
|               </ha-alert>` | ||||
|             : ""} | ||||
|           ${!this._updating | ||||
|             ? html` | ||||
|                 ${this._update.changelog_content | ||||
|                   ? html` | ||||
|                       <ha-faded> | ||||
|                         <ha-markdown .content=${this._update.changelog_content}> | ||||
|                         </ha-markdown> | ||||
|                       </ha-faded> | ||||
|                     ` | ||||
|                   : ""} | ||||
|                 ${this._update.changelog_url | ||||
|                   ? html`<a href=${this._update.changelog_url} target="_blank"> | ||||
|                       Full changelog | ||||
|                     </a> ` | ||||
|                   : ""} | ||||
|                 <p> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.updates.dialog.description", | ||||
|                     { | ||||
|                       name: this._update.name, | ||||
|                       version: this._update.current_version, | ||||
|                       newest_version: this._update.available_version, | ||||
|                     } | ||||
|                   )} | ||||
|                 </p> | ||||
|                 ${this._update.supports_backup | ||||
|                   ? html` | ||||
|                       <ha-formfield | ||||
|                         .label=${this.hass.localize( | ||||
|                           "ui.panel.config.updates.dialog.create_backup" | ||||
|                         )} | ||||
|                       > | ||||
|                         <ha-checkbox checked></ha-checkbox> | ||||
|                       </ha-formfield> | ||||
|                     ` | ||||
|                   : ""} | ||||
|               ` | ||||
|             : html`<ha-circular-progress alt="Updating" size="large" active> | ||||
|                 </ha-circular-progress> | ||||
|                 <p class="progress-text"> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.updates.dialog.updating", | ||||
|                     { | ||||
|                       name: this._update.name, | ||||
|                       version: this._update.available_version, | ||||
|                     } | ||||
|                   )} | ||||
|                 </p>`} | ||||
|         </div> | ||||
|         ${!this._updating | ||||
|           ? html` | ||||
|               <mwc-button slot="secondaryAction" @click=${this._skipUpdate}> | ||||
|                 ${this.hass.localize("ui.common.skip")} | ||||
|               </mwc-button> | ||||
|               <mwc-button | ||||
|                 .disabled=${this._updating} | ||||
|                 slot="primaryAction" | ||||
|                 @click=${this._performUpdate} | ||||
|               > | ||||
|                 ${this.hass.localize("ui.panel.config.updates.dialog.update")} | ||||
|               </mwc-button> | ||||
|             ` | ||||
|           : ""} | ||||
|       </ha-dialog> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   get _shouldCreateBackup(): boolean { | ||||
|     if (!this._update.supports_backup) { | ||||
|       return false; | ||||
|     } | ||||
|     const checkbox = this.shadowRoot?.querySelector("ha-checkbox"); | ||||
|     if (checkbox) { | ||||
|       return checkbox.checked; | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   private async _performUpdate() { | ||||
|     this._error = undefined; | ||||
|     this._updating = true; | ||||
|     try { | ||||
|       await performUpdate(this.hass, { | ||||
|         domain: this._update.domain, | ||||
|         identifier: this._update.identifier, | ||||
|         version: this._update.available_version, | ||||
|         backup: this._shouldCreateBackup, | ||||
|       }); | ||||
|     } catch (err: any) { | ||||
|       this._error = err.message; | ||||
|       this._updating = false; | ||||
|       return; | ||||
|     } | ||||
|     this._updating = false; | ||||
|     this._refreshCallback(); | ||||
|     this.closeDialog(); | ||||
|   } | ||||
|  | ||||
|   private async _skipUpdate() { | ||||
|     this._error = undefined; | ||||
|     try { | ||||
|       await skipUpdate(this.hass, { | ||||
|         domain: this._update.domain, | ||||
|         identifier: this._update.identifier, | ||||
|         version: this._update.available_version, | ||||
|       }); | ||||
|     } catch (err: any) { | ||||
|       this._error = err.message; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this._refreshCallback(); | ||||
|     this.closeDialog(); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyleDialog, | ||||
|       css` | ||||
|         ha-circular-progress { | ||||
|           display: block; | ||||
|           margin: 32px; | ||||
|           text-align: center; | ||||
|         } | ||||
|  | ||||
|         .progress-text { | ||||
|           text-align: center; | ||||
|         } | ||||
|  | ||||
|         ha-markdown { | ||||
|           padding-bottom: 8px; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-update-dialog": HaUpdateDialog; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										18
									
								
								src/dialogs/update-dialog/show-ha-update-dialog.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/dialogs/update-dialog/show-ha-update-dialog.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { UpdateDescription } from "../../data/update"; | ||||
|  | ||||
| export interface UpdateDialogParams { | ||||
|   update: UpdateDescription; | ||||
|   refreshCallback: () => void; | ||||
| } | ||||
|  | ||||
| export const showUpdateDialog = ( | ||||
|   element: HTMLElement, | ||||
|   dialogParams: UpdateDialogParams | ||||
| ): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "ha-update-dialog", | ||||
|     dialogImport: () => import("./ha-update-dialog"), | ||||
|     dialogParams, | ||||
|   }); | ||||
| }; | ||||
| @@ -13,6 +13,9 @@ export const SubscribeMixin = <T extends Constructor<ReactiveElement>>( | ||||
|   class SubscribeClass extends superClass { | ||||
|     @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|     // we wait with subscribing till these properties are set on the host element | ||||
|     protected hassSubscribeRequiredHostProps?: string[]; | ||||
|  | ||||
|     private __unsubs?: Array<UnsubscribeFunc | Promise<UnsubscribeFunc>>; | ||||
|  | ||||
|     public connectedCallback() { | ||||
| @@ -39,6 +42,16 @@ export const SubscribeMixin = <T extends Constructor<ReactiveElement>>( | ||||
|       super.updated(changedProps); | ||||
|       if (changedProps.has("hass")) { | ||||
|         this.__checkSubscribed(); | ||||
|         return; | ||||
|       } | ||||
|       if (!this.hassSubscribeRequiredHostProps) { | ||||
|         return; | ||||
|       } | ||||
|       for (const key of changedProps.keys()) { | ||||
|         if (this.hassSubscribeRequiredHostProps.includes(key as string)) { | ||||
|           this.__checkSubscribed(); | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -52,7 +65,10 @@ export const SubscribeMixin = <T extends Constructor<ReactiveElement>>( | ||||
|       if ( | ||||
|         this.__unsubs !== undefined || | ||||
|         !(this as unknown as Element).isConnected || | ||||
|         this.hass === undefined | ||||
|         this.hass === undefined || | ||||
|         this.hassSubscribeRequiredHostProps?.some( | ||||
|           (prop) => this[prop] === undefined | ||||
|         ) | ||||
|       ) { | ||||
|         return; | ||||
|       } | ||||
|   | ||||
| @@ -26,10 +26,6 @@ import "../../../components/ha-menu-button"; | ||||
| import "../../../components/ha-button-menu"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import { CloudStatus } from "../../../data/cloud"; | ||||
| import { | ||||
|   refreshSupervisorAvailableUpdates, | ||||
|   SupervisorAvailableUpdates, | ||||
| } from "../../../data/supervisor/root"; | ||||
| import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar"; | ||||
| import "../../../layouts/ha-app-layout"; | ||||
| import { haStyle } from "../../../resources/styles"; | ||||
| @@ -38,10 +34,11 @@ import "../ha-config-section"; | ||||
| import { configSections } from "../ha-panel-config"; | ||||
| import "./ha-config-navigation"; | ||||
| import "./ha-config-updates"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; | ||||
| import { showToast } from "../../../util/toast"; | ||||
| import { documentationUrl } from "../../../util/documentation-url"; | ||||
| import { UpdateDescription } from "../../../data/update"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { computeRTL } from "../../../common/util/compute_rtl"; | ||||
|  | ||||
| const randomTip = (hass: HomeAssistant) => { | ||||
|   const weighted: string[] = []; | ||||
| @@ -114,14 +111,12 @@ class HaConfigDashboard extends LitElement { | ||||
|   @property() public cloudStatus?: CloudStatus; | ||||
|  | ||||
|   // null means not available | ||||
|   @property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null; | ||||
|   @property() public updates?: UpdateDescription[] | null; | ||||
|  | ||||
|   @property() public showAdvanced!: boolean; | ||||
|  | ||||
|   @state() private _tip?: string; | ||||
|  | ||||
|   private _notifyUpdates = false; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <ha-app-layout> | ||||
| @@ -133,6 +128,7 @@ class HaConfigDashboard extends LitElement { | ||||
|             ></ha-menu-button> | ||||
|             <div main-title>${this.hass.localize("panel.config")}</div> | ||||
|             <ha-icon-button | ||||
|               .label=${this.hass.localize("ui.dialogs.quick-bar.title")} | ||||
|               .path=${mdiMagnify} | ||||
|               @click=${this._showQuickBar} | ||||
|             ></ha-icon-button> | ||||
| @@ -159,50 +155,53 @@ class HaConfigDashboard extends LitElement { | ||||
|           .isWide=${this.isWide} | ||||
|           full-width | ||||
|         > | ||||
|           ${this.supervisorUpdates === undefined | ||||
|             ? // Hide everything until updates loaded | ||||
|               html`` | ||||
|             : html`${this.supervisorUpdates?.length | ||||
|                   ? html`<ha-card> | ||||
|                       <ha-config-updates | ||||
|                         .hass=${this.hass} | ||||
|                         .narrow=${this.narrow} | ||||
|                         .supervisorUpdates=${this.supervisorUpdates} | ||||
|                       ></ha-config-updates> | ||||
|                     </ha-card>` | ||||
|                   : ""} | ||||
|                 <ha-card> | ||||
|                   ${this.narrow && this.supervisorUpdates?.length | ||||
|                     ? html`<div class="title"> | ||||
|                         ${this.hass.localize("panel.config")} | ||||
|                       </div>` | ||||
|                     : ""} | ||||
|                   ${this.cloudStatus && isComponentLoaded(this.hass, "cloud") | ||||
|                     ? html` | ||||
|                         <ha-config-navigation | ||||
|                           .hass=${this.hass} | ||||
|                           .narrow=${this.narrow} | ||||
|                           .showAdvanced=${this.showAdvanced} | ||||
|                           .pages=${[ | ||||
|                             { | ||||
|                               component: "cloud", | ||||
|                               path: "/config/cloud", | ||||
|                               name: "Home Assistant Cloud", | ||||
|                               info: this.cloudStatus, | ||||
|                               iconPath: mdiCloudLock, | ||||
|                               iconColor: "#3B808E", | ||||
|                             }, | ||||
|                           ]} | ||||
|                         ></ha-config-navigation> | ||||
|                       ` | ||||
|                     : ""} | ||||
|           ${this.updates === undefined | ||||
|             ? html`<ha-alert .rtl=${computeRTL(this.hass)}> | ||||
|                 ${this.hass.localize( | ||||
|                   "ui.panel.config.updates.checking_updates" | ||||
|                 )} | ||||
|               </ha-alert>` | ||||
|             : this.updates?.length | ||||
|             ? html`<ha-card> | ||||
|                 <ha-config-updates | ||||
|                   .hass=${this.hass} | ||||
|                   .narrow=${this.narrow} | ||||
|                   .updates=${this.updates} | ||||
|                 ></ha-config-updates> | ||||
|               </ha-card>` | ||||
|             : ""} | ||||
|           <ha-card> | ||||
|             ${this.narrow && this.updates?.length | ||||
|               ? html`<div class="title"> | ||||
|                   ${this.hass.localize("panel.config")} | ||||
|                 </div>` | ||||
|               : ""} | ||||
|             ${this.cloudStatus && isComponentLoaded(this.hass, "cloud") | ||||
|               ? html` | ||||
|                   <ha-config-navigation | ||||
|                     .hass=${this.hass} | ||||
|                     .narrow=${this.narrow} | ||||
|                     .showAdvanced=${this.showAdvanced} | ||||
|                     .pages=${configSections.dashboard} | ||||
|                     .pages=${[ | ||||
|                       { | ||||
|                         component: "cloud", | ||||
|                         path: "/config/cloud", | ||||
|                         name: "Home Assistant Cloud", | ||||
|                         info: this.cloudStatus, | ||||
|                         iconPath: mdiCloudLock, | ||||
|                         iconColor: "#3B808E", | ||||
|                       }, | ||||
|                     ]} | ||||
|                   ></ha-config-navigation> | ||||
|                 </ha-card>`} | ||||
|                 ` | ||||
|               : ""} | ||||
|             <ha-config-navigation | ||||
|               .hass=${this.hass} | ||||
|               .narrow=${this.narrow} | ||||
|               .showAdvanced=${this.showAdvanced} | ||||
|               .pages=${configSections.dashboard} | ||||
|             ></ha-config-navigation> | ||||
|           </ha-card> | ||||
|           <div class="tips"> | ||||
|             <ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon> | ||||
|             <span class="tip-word">Tip!</span> | ||||
| @@ -219,22 +218,6 @@ class HaConfigDashboard extends LitElement { | ||||
|     if (!this._tip && changedProps.has("hass")) { | ||||
|       this._tip = randomTip(this.hass); | ||||
|     } | ||||
|  | ||||
|     if (!changedProps.has("supervisorUpdates") || !this._notifyUpdates) { | ||||
|       return; | ||||
|     } | ||||
|     this._notifyUpdates = false; | ||||
|     if (this.supervisorUpdates?.length) { | ||||
|       showToast(this, { | ||||
|         message: this.hass.localize( | ||||
|           "ui.panel.config.updates.updates_refreshed" | ||||
|         ), | ||||
|       }); | ||||
|     } else { | ||||
|       showToast(this, { | ||||
|         message: this.hass.localize("ui.panel.config.updates.no_new_updates"), | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _showQuickBar(): void { | ||||
| @@ -247,18 +230,16 @@ class HaConfigDashboard extends LitElement { | ||||
|   private async _handleMenuAction(ev: CustomEvent<ActionDetail>) { | ||||
|     switch (ev.detail.index) { | ||||
|       case 0: | ||||
|         if (isComponentLoaded(this.hass, "hassio")) { | ||||
|           this._notifyUpdates = true; | ||||
|           await refreshSupervisorAvailableUpdates(this.hass); | ||||
|           fireEvent(this, "ha-refresh-supervisor"); | ||||
|         if (isComponentLoaded(this.hass, "update")) { | ||||
|           fireEvent(this, "ha-refresh-updates"); | ||||
|           return; | ||||
|         } | ||||
|         showAlertDialog(this, { | ||||
|           title: this.hass.localize( | ||||
|             "ui.panel.config.updates.check_unavailable.title" | ||||
|             "ui.panel.config.updates.update_not_loaded.title" | ||||
|           ), | ||||
|           text: this.hass.localize( | ||||
|             "ui.panel.config.updates.check_unavailable.description" | ||||
|             "ui.panel.config.updates.update_not_loaded.description" | ||||
|           ), | ||||
|           warning: true, | ||||
|         }); | ||||
|   | ||||
| @@ -1,21 +1,48 @@ | ||||
| import "@material/mwc-button/mwc-button"; | ||||
| import { mdiPackageVariant } from "@mdi/js"; | ||||
| import "@polymer/paper-item/paper-icon-item"; | ||||
| import "@polymer/paper-item/paper-item-body"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import "../../../components/ha-alert"; | ||||
| import "../../../components/ha-icon-next"; | ||||
| import "../../../components/ha-logo-svg"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import { SupervisorAvailableUpdates } from "../../../data/supervisor/root"; | ||||
| import { UpdateDescription } from "../../../data/update"; | ||||
| import { showUpdateDialog } from "../../../dialogs/update-dialog/show-ha-update-dialog"; | ||||
| import { HomeAssistant } from "../../../types"; | ||||
| import "../../../components/ha-icon-next"; | ||||
| import { brandsUrl } from "../../../util/brands-url"; | ||||
|  | ||||
| export const SUPERVISOR_UPDATE_NAMES = { | ||||
|   core: "Home Assistant Core", | ||||
|   os: "Home Assistant Operating System", | ||||
|   supervisor: "Home Assistant Supervisor", | ||||
| }; | ||||
| const sortUpdates = memoizeOne((a: UpdateDescription, b: UpdateDescription) => { | ||||
|   if (a.domain === "hassio" && b.domain === "hassio") { | ||||
|     if (a.identifier === "core") { | ||||
|       return -1; | ||||
|     } | ||||
|     if (b.identifier === "core") { | ||||
|       return 1; | ||||
|     } | ||||
|     if (a.identifier === "supervisor") { | ||||
|       return -1; | ||||
|     } | ||||
|     if (b.identifier === "supervisor") { | ||||
|       return 1; | ||||
|     } | ||||
|     if (a.identifier === "os") { | ||||
|       return -1; | ||||
|     } | ||||
|     if (b.identifier === "os") { | ||||
|       return 1; | ||||
|     } | ||||
|   } | ||||
|   if (a.domain === "hassio") { | ||||
|     return -1; | ||||
|   } | ||||
|   if (b.domain === "hassio") { | ||||
|     return 1; | ||||
|   } | ||||
|   return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1; | ||||
| }); | ||||
|  | ||||
| @customElement("ha-config-updates") | ||||
| class HaConfigUpdates extends LitElement { | ||||
| @@ -24,62 +51,62 @@ class HaConfigUpdates extends LitElement { | ||||
|   @property({ type: Boolean }) public narrow!: boolean; | ||||
|  | ||||
|   @property({ attribute: false }) | ||||
|   public supervisorUpdates?: SupervisorAvailableUpdates[] | null; | ||||
|   public updates?: UpdateDescription[] | null; | ||||
|  | ||||
|   @state() private _showAll = false; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.supervisorUpdates?.length) { | ||||
|     if (!this.updates?.length) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     // Make sure the first updates shown are for the Supervisor | ||||
|     const sortedUpdates = this.updates.sort((a, b) => sortUpdates(a, b)); | ||||
|  | ||||
|     const updates = | ||||
|       this._showAll || this.supervisorUpdates.length <= 3 | ||||
|         ? this.supervisorUpdates | ||||
|         : this.supervisorUpdates.slice(0, 2); | ||||
|       this._showAll || sortedUpdates.length <= 3 | ||||
|         ? sortedUpdates | ||||
|         : sortedUpdates.slice(0, 2); | ||||
|  | ||||
|     return html` | ||||
|       <div class="title"> | ||||
|         ${this.hass.localize("ui.panel.config.updates.title", { | ||||
|           count: this.supervisorUpdates.length, | ||||
|           count: sortedUpdates.length, | ||||
|         })} | ||||
|       </div> | ||||
|       ${updates.map( | ||||
|         (update) => html` | ||||
|           <a href="/hassio${update.panel_path}"> | ||||
|             <paper-icon-item> | ||||
|               <span slot="item-icon" class="icon"> | ||||
|                 ${update.update_type === "addon" | ||||
|                   ? update.icon | ||||
|                     ? html`<img src="/api/hassio${update.icon}" />` | ||||
|                     : html`<ha-svg-icon | ||||
|                         .path=${mdiPackageVariant} | ||||
|                       ></ha-svg-icon>` | ||||
|                   : html`<ha-logo-svg></ha-logo-svg>`} | ||||
|               </span> | ||||
|               <paper-item-body two-line> | ||||
|                 ${update.update_type === "addon" | ||||
|                   ? update.name | ||||
|                   : SUPERVISOR_UPDATE_NAMES[update.update_type!]} | ||||
|                 <div secondary> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.updates.version_available", | ||||
|                     { | ||||
|                       version_available: update.version_latest, | ||||
|                     } | ||||
|                   )} | ||||
|                 </div> | ||||
|               </paper-item-body> | ||||
|               ${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""} | ||||
|             </paper-icon-item> | ||||
|           </a> | ||||
|           <paper-icon-item @click=${this._showUpdate} .update=${update}> | ||||
|             <span slot="item-icon" class="icon"> | ||||
|               <img | ||||
|                 src=${update.icon_url || | ||||
|                 brandsUrl({ | ||||
|                   domain: update.domain, | ||||
|                   type: "icon", | ||||
|                   useFallback: true, | ||||
|                   darkOptimized: this.hass.themes?.darkMode, | ||||
|                 })} | ||||
|               /> | ||||
|             </span> | ||||
|             <paper-item-body two-line> | ||||
|               ${update.name} | ||||
|               <div secondary> | ||||
|                 ${this.hass.localize( | ||||
|                   "ui.panel.config.updates.version_available", | ||||
|                   { | ||||
|                     version_available: update.available_version, | ||||
|                   } | ||||
|                 )} | ||||
|               </div> | ||||
|             </paper-item-body> | ||||
|           </paper-icon-item> | ||||
|         ` | ||||
|       )} | ||||
|       ${!this._showAll && this.supervisorUpdates.length >= 4 | ||||
|       ${!this._showAll && this.updates.length >= 4 | ||||
|         ? html` | ||||
|             <button class="show-more" @click=${this._showAllClicked}> | ||||
|               ${this.hass.localize("ui.panel.config.updates.more_updates", { | ||||
|                 count: this.supervisorUpdates!.length - updates.length, | ||||
|                 count: this.updates!.length - updates.length, | ||||
|               })} | ||||
|             </button> | ||||
|           ` | ||||
| @@ -91,6 +118,14 @@ class HaConfigUpdates extends LitElement { | ||||
|     this._showAll = true; | ||||
|   } | ||||
|  | ||||
|   private _showUpdate(ev) { | ||||
|     const update = ev.currentTarget.update as UpdateDescription; | ||||
|     showUpdateDialog(this, { | ||||
|       update, | ||||
|       refreshCallback: () => fireEvent(this, "ha-refresh-updates"), | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup[] { | ||||
|     return [ | ||||
|       css` | ||||
| @@ -139,6 +174,9 @@ class HaConfigUpdates extends LitElement { | ||||
|           outline: none; | ||||
|           text-decoration: underline; | ||||
|         } | ||||
|         paper-icon-item { | ||||
|           cursor: pointer; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -86,6 +86,7 @@ export class EnergyDeviceSettings extends LitElement { | ||||
|                     : device.stat_consumption}</span | ||||
|                 > | ||||
|                 <ha-icon-button | ||||
|                   .label=${this.hass.localize("ui.common.delete")} | ||||
|                   @click=${this._deleteDevice} | ||||
|                   .device=${device} | ||||
|                   .path=${mdiDelete} | ||||
|   | ||||
| @@ -27,20 +27,18 @@ import { customElement, property, state } from "lit/decorators"; | ||||
| import { isComponentLoaded } from "../../common/config/is_component_loaded"; | ||||
| import { listenMediaQuery } from "../../common/dom/media_query"; | ||||
| import { CloudStatus, fetchCloudStatus } from "../../data/cloud"; | ||||
| import { | ||||
|   fetchSupervisorAvailableUpdates, | ||||
|   SupervisorAvailableUpdates, | ||||
| } from "../../data/supervisor/root"; | ||||
| import { fetchUpdateInfo, UpdateDescription } from "../../data/update"; | ||||
| import "../../layouts/hass-loading-screen"; | ||||
| import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; | ||||
| import { PageNavigation } from "../../layouts/hass-tabs-subpage"; | ||||
| import { HomeAssistant, Route } from "../../types"; | ||||
| import { showToast } from "../../util/toast"; | ||||
|  | ||||
| declare global { | ||||
|   // for fire event | ||||
|   interface HASSDomEvents { | ||||
|     "ha-refresh-cloud-status": undefined; | ||||
|     "ha-refresh-supervisor": undefined; | ||||
|     "ha-refresh-updates": undefined; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -407,7 +405,7 @@ class HaPanelConfig extends HassRouterPage { | ||||
|  | ||||
|   @state() private _cloudStatus?: CloudStatus; | ||||
|  | ||||
|   @state() private _supervisorUpdates?: SupervisorAvailableUpdates[] | null; | ||||
|   @state() private _updates?: UpdateDescription[] | null; | ||||
|  | ||||
|   private _listeners: Array<() => void> = []; | ||||
|  | ||||
| @@ -443,18 +441,18 @@ class HaPanelConfig extends HassRouterPage { | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|     if (isComponentLoaded(this.hass, "hassio")) { | ||||
|       this._loadSupervisorUpdates(); | ||||
|       this.addEventListener("ha-refresh-supervisor", () => { | ||||
|         this._loadSupervisorUpdates(); | ||||
|     if (isComponentLoaded(this.hass, "update")) { | ||||
|       this._loadUpdates(); | ||||
|       this.addEventListener("ha-refresh-updates", () => { | ||||
|         this._loadUpdates(); | ||||
|       }); | ||||
|       this.addEventListener("connection-status", (ev) => { | ||||
|         if (ev.detail === "connected") { | ||||
|           this._loadSupervisorUpdates(); | ||||
|           this._loadUpdates(); | ||||
|         } | ||||
|       }); | ||||
|     } else { | ||||
|       this._supervisorUpdates = null; | ||||
|       this._updates = null; | ||||
|     } | ||||
|     this.addEventListener("ha-refresh-cloud-status", () => | ||||
|       this._updateCloudStatus() | ||||
| @@ -486,7 +484,7 @@ class HaPanelConfig extends HassRouterPage { | ||||
|         isWide, | ||||
|         narrow: this.narrow, | ||||
|         cloudStatus: this._cloudStatus, | ||||
|         supervisorUpdates: this._supervisorUpdates, | ||||
|         updates: this._updates, | ||||
|       }); | ||||
|     } else { | ||||
|       el.route = this.routeTail; | ||||
| @@ -495,7 +493,7 @@ class HaPanelConfig extends HassRouterPage { | ||||
|       el.isWide = isWide; | ||||
|       el.narrow = this.narrow; | ||||
|       el.cloudStatus = this._cloudStatus; | ||||
|       el.supervisorUpdates = this._supervisorUpdates; | ||||
|       el.updates = this._updates; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -514,13 +512,33 @@ class HaPanelConfig extends HassRouterPage { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async _loadSupervisorUpdates(): Promise<void> { | ||||
|   private async _loadUpdates(): Promise<void> { | ||||
|     const _showToast = this._updates !== undefined; | ||||
|  | ||||
|     if (_showToast) { | ||||
|       showToast(this, { | ||||
|         message: this.hass.localize("ui.panel.config.updates.checking_updates"), | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       this._supervisorUpdates = await fetchSupervisorAvailableUpdates( | ||||
|         this.hass | ||||
|       ); | ||||
|       this._updates = await fetchUpdateInfo(this.hass); | ||||
|     } catch (err) { | ||||
|       this._supervisorUpdates = null; | ||||
|       this._updates = null; | ||||
|     } | ||||
|  | ||||
|     if (_showToast) { | ||||
|       if (this._updates?.length) { | ||||
|         showToast(this, { | ||||
|           message: this.hass.localize( | ||||
|             "ui.panel.config.updates.updates_refreshed" | ||||
|           ), | ||||
|         }); | ||||
|       } else { | ||||
|         showToast(this, { | ||||
|           message: this.hass.localize("ui.panel.config.updates.no_new_updates"), | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -144,8 +144,6 @@ export class ZHANetworkVisualizationPage extends LitElement { | ||||
|               <div slot="header"> | ||||
|                 <search-input | ||||
|                   .hass=${this.hass} | ||||
|                   no-label-float | ||||
|                   no-underline | ||||
|                   class="header" | ||||
|                   @value-changed=${this._handleSearchChange} | ||||
|                   .filter=${this._filter} | ||||
| @@ -161,8 +159,6 @@ export class ZHANetworkVisualizationPage extends LitElement { | ||||
|           ${!this.narrow | ||||
|             ? html`<search-input | ||||
|                 .hass=${this.hass} | ||||
|                 no-label-float | ||||
|                 no-underline | ||||
|                 @value-changed=${this._handleSearchChange} | ||||
|                 .filter=${this._filter} | ||||
|                 .label=${this.hass.localize( | ||||
|   | ||||
| @@ -43,8 +43,6 @@ export class HaConfigLogs extends LitElement { | ||||
|           <div slot="header"> | ||||
|             <search-input | ||||
|               class="header" | ||||
|               no-label-float | ||||
|               no-underline | ||||
|               @value-changed=${this._filterChanged} | ||||
|               .hass=${this.hass} | ||||
|               .filter=${this._filter} | ||||
| @@ -55,9 +53,6 @@ export class HaConfigLogs extends LitElement { | ||||
|       : html` | ||||
|           <div class="search"> | ||||
|             <search-input | ||||
|               autofocus | ||||
|               no-label-float | ||||
|               no-underline | ||||
|               @value-changed=${this._filterChanged} | ||||
|               .hass=${this.hass} | ||||
|               .filter=${this._filter} | ||||
|   | ||||
| @@ -32,6 +32,8 @@ class HuiEnergyCarbonGaugeCard | ||||
|  | ||||
|   @state() private _data?: EnergyData; | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public getCardSize(): number { | ||||
|     return 4; | ||||
|   } | ||||
|   | ||||
| @@ -49,6 +49,8 @@ export class HuiEnergyDevicesGraphCard | ||||
|  | ||||
|   @query("ha-chart-base") private _chart?: HaChartBase; | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       getEnergyDataCollection(this.hass, { | ||||
|   | ||||
| @@ -43,6 +43,8 @@ class HuiEnergyDistrubutionCard | ||||
|  | ||||
|   @state() private _data?: EnergyData; | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public setConfig(config: EnergyDistributionCardConfig): void { | ||||
|     this._config = config; | ||||
|   } | ||||
|   | ||||
| @@ -62,6 +62,8 @@ export class HuiEnergyGasGraphCard | ||||
|  | ||||
|   @state() private _unit?: string; | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       getEnergyDataCollection(this.hass, { | ||||
|   | ||||
| @@ -35,6 +35,8 @@ class HuiEnergyGridGaugeCard | ||||
|  | ||||
|   @state() private _data?: EnergyData; | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       getEnergyDataCollection(this.hass!, { | ||||
|   | ||||
| @@ -30,6 +30,8 @@ class HuiEnergySolarGaugeCard | ||||
|  | ||||
|   @state() private _data?: EnergyData; | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       getEnergyDataCollection(this.hass!, { | ||||
|   | ||||
| @@ -61,6 +61,8 @@ export class HuiEnergySolarGraphCard | ||||
|  | ||||
|   @state() private _end = endOfToday(); | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       getEnergyDataCollection(this.hass, { | ||||
|   | ||||
| @@ -45,6 +45,8 @@ export class HuiEnergySourcesTableCard | ||||
|  | ||||
|   @state() private _data?: EnergyData; | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       getEnergyDataCollection(this.hass, { | ||||
|   | ||||
| @@ -50,6 +50,8 @@ export class HuiEnergyUsageGraphCard | ||||
|  | ||||
|   @state() private _end = endOfToday(); | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       getEnergyDataCollection(this.hass, { | ||||
|   | ||||
| @@ -9,7 +9,10 @@ import { | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import "../../../components/ha-date-input"; | ||||
| import { UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity"; | ||||
| import { setInputDateTimeValue } from "../../../data/input_datetime"; | ||||
| import { | ||||
|   setInputDateTimeValue, | ||||
|   stateToIsoDateString, | ||||
| } from "../../../data/input_datetime"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { hasConfigOrEntityChanged } from "../common/has-changed"; | ||||
| import "../components/hui-generic-entity-row"; | ||||
| @@ -65,7 +68,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { | ||||
|                 .label=${stateObj.attributes.has_time ? name : undefined} | ||||
|                 .locale=${this.hass.locale} | ||||
|                 .disabled=${UNAVAILABLE_STATES.includes(stateObj.state)} | ||||
|                 .value=${`${stateObj.attributes.year}-${stateObj.attributes.month}-${stateObj.attributes.day}`} | ||||
|                 .value=${stateToIsoDateString(stateObj)} | ||||
|                 @value-changed=${this._dateChanged} | ||||
|               > | ||||
|               </ha-date-input> | ||||
|   | ||||
| @@ -9,6 +9,8 @@ import { | ||||
| import { ResolvedMediaSource } from "../../data/media_source"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
|  | ||||
| export const ERR_UNSUPPORTED_MEDIA = "Unsupported Media"; | ||||
|  | ||||
| export class BrowserMediaPlayer { | ||||
|   private player: HTMLAudioElement; | ||||
|  | ||||
| @@ -25,6 +27,9 @@ export class BrowserMediaPlayer { | ||||
|     private onChange: () => void | ||||
|   ) { | ||||
|     const player = new Audio(this.resolved.url); | ||||
|     if (player.canPlayType(resolved.mime_type) === "") { | ||||
|       throw new Error(ERR_UNSUPPORTED_MEDIA); | ||||
|     } | ||||
|     player.autoplay = true; | ||||
|     player.volume = volume; | ||||
|     player.addEventListener("play", this._handleChange); | ||||
|   | ||||
| @@ -49,9 +49,13 @@ import { | ||||
|   SUPPORT_VOLUME_SET, | ||||
| } from "../../data/media-player"; | ||||
| import { ResolvedMediaSource } from "../../data/media_source"; | ||||
| import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import "../lovelace/components/hui-marquee"; | ||||
| import { BrowserMediaPlayer } from "./browser-media-player"; | ||||
| import { | ||||
|   BrowserMediaPlayer, | ||||
|   ERR_UNSUPPORTED_MEDIA, | ||||
| } from "./browser-media-player"; | ||||
|  | ||||
| declare global { | ||||
|   interface HASSDomEvents { | ||||
| @@ -125,13 +129,25 @@ export class BarMediaPlayer extends LitElement { | ||||
|       throw Error("Only browser supported"); | ||||
|     } | ||||
|     this._tearDownBrowserPlayer(); | ||||
|     this._browserPlayer = new BrowserMediaPlayer( | ||||
|       this.hass, | ||||
|       item, | ||||
|       resolved, | ||||
|       this._browserPlayerVolume, | ||||
|       () => this.requestUpdate("_browserPlayer") | ||||
|     ); | ||||
|     try { | ||||
|       this._browserPlayer = new BrowserMediaPlayer( | ||||
|         this.hass, | ||||
|         item, | ||||
|         resolved, | ||||
|         this._browserPlayerVolume, | ||||
|         () => this.requestUpdate("_browserPlayer") | ||||
|       ); | ||||
|     } catch (err: any) { | ||||
|       if (err.message === ERR_UNSUPPORTED_MEDIA) { | ||||
|         showAlertDialog(this, { | ||||
|           text: this.hass.localize( | ||||
|             "ui.components.media-browser.media_not_supported" | ||||
|           ), | ||||
|         }); | ||||
|       } else { | ||||
|         throw err; | ||||
|       } | ||||
|     } | ||||
|     this._newMediaExpected = false; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -27,7 +27,10 @@ import { | ||||
|   MediaPickedEvent, | ||||
|   MediaPlayerItem, | ||||
| } from "../../data/media-player"; | ||||
| import { resolveMediaSource } from "../../data/media_source"; | ||||
| import { | ||||
|   ResolvedMediaSource, | ||||
|   resolveMediaSource, | ||||
| } from "../../data/media_source"; | ||||
| import "../../layouts/ha-app-layout"; | ||||
| import { haStyle } from "../../resources/styles"; | ||||
| import type { HomeAssistant, Route } from "../../types"; | ||||
| @@ -224,11 +227,19 @@ class PanelMediaBrowser extends LitElement { | ||||
|     } | ||||
|  | ||||
|     this._player.showResolvingNewMediaPicked(); | ||||
|  | ||||
|     const resolvedUrl = await resolveMediaSource( | ||||
|       this.hass, | ||||
|       item.media_content_id | ||||
|     ); | ||||
|     let resolvedUrl: ResolvedMediaSource; | ||||
|     try { | ||||
|       resolvedUrl = await resolveMediaSource(this.hass, item.media_content_id); | ||||
|     } catch (err: any) { | ||||
|       showAlertDialog(this, { | ||||
|         title: this.hass.localize( | ||||
|           "ui.components.media-browser.media_browsing_error" | ||||
|         ), | ||||
|         text: err.message, | ||||
|       }); | ||||
|       this._player.hideResolvingNewMediaPicked(); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (resolvedUrl.mime_type.startsWith("audio/")) { | ||||
|       this._player.playItem(item, resolvedUrl); | ||||
|   | ||||
| @@ -1047,18 +1047,27 @@ | ||||
|           "learn_more": "Learn more" | ||||
|         }, | ||||
|         "updates": { | ||||
|           "check_unavailable": { | ||||
|           "update_not_loaded": { | ||||
|             "title": "Unable to check for updates", | ||||
|             "description": "You need to run the Home Assistant operating system to be able to check and install updates from the Home Assistant user interface." | ||||
|             "description": "You need to enable the update integrtion to be able to check and install updates from the Home Assistant user interface." | ||||
|           }, | ||||
|           "check_updates": "Check for updates", | ||||
|           "checking_updates": "Checking for available updates", | ||||
|           "no_new_updates": "No new updates found", | ||||
|           "updates_refreshed": "Updates refreshed", | ||||
|           "title": "{count} {count, plural,\n  one {update}\n  other {updates}\n}", | ||||
|           "unable_to_fetch": "Unable to load updates", | ||||
|           "version_available": "Version {version_available} is available", | ||||
|           "more_updates": "+{count} updates", | ||||
|           "show": "show" | ||||
|           "show": "show", | ||||
|           "dialog": { | ||||
|             "title": "[%key:supervisor::update_available::update_name%]", | ||||
|             "create_backup": "[%key:supervisor::update_available::create_backup%]", | ||||
|             "open_changelog": "Open changelog", | ||||
|             "updating": "[%key:supervisor::update_available::updating%]", | ||||
|             "update": "[%key:supervisor::common::update%]", | ||||
|             "description": "[%key:supervisor::update_available::description%]" | ||||
|           } | ||||
|         }, | ||||
|         "areas": { | ||||
|           "caption": "Areas", | ||||
|   | ||||
| @@ -9,3 +9,5 @@ export const brandsUrl = (options: BrandsOptions): string => | ||||
|   `https://brands.home-assistant.io/${options.useFallback ? "_/" : ""}${ | ||||
|     options.domain | ||||
|   }/${options.darkOptimized ? "dark_" : ""}${options.type}.png`; | ||||
|  | ||||
| export const extractDomainFromBrandUrl = (url: string) => url.split("/")[4]; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user