diff --git a/pyproject.toml b/pyproject.toml index 06f5f3e167..900493bc2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20221027.0" +version = "20221031.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/common/entity/state_active.ts b/src/common/entity/state_active.ts index f67c395b52..78f94e8e59 100644 --- a/src/common/entity/state_active.ts +++ b/src/common/entity/state_active.ts @@ -25,7 +25,7 @@ export function stateActive(stateObj: HassEntity): boolean { case "person": return state !== "not_home"; case "media-player": - return state !== "idle"; + return state !== "idle" && state !== "standby"; case "vacuum": return state === "on" || state === "cleaning"; case "plant": diff --git a/src/components/ha-camera-stream.ts b/src/components/ha-camera-stream.ts index c0b5ef6f0e..6ad12fb942 100644 --- a/src/components/ha-camera-stream.ts +++ b/src/components/ha-camera-stream.ts @@ -24,7 +24,7 @@ import "./ha-hls-player"; import "./ha-web-rtc-player"; @customElement("ha-camera-stream") -class HaCameraStream extends LitElement { +export class HaCameraStream extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public stateObj?: CameraEntity; @@ -81,7 +81,7 @@ class HaCameraStream extends LitElement { return html``; } if (__DEMO__ || this._shouldRenderMJPEG) { - return html` string; + + @property() public computeHelper?: (schema: HaFormSchema) => string; + + protected render(): TemplateResult { + return html` + +
+ ${this.schema.icon + ? html` ` + : this.schema.iconPath + ? html` ` + : null} + ${this.schema.title} +
+
+ +
+
+ `; + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: flex !important; + flex-direction: column; + } + :host ha-form { + display: block; + } + .content { + padding: 12px; + } + ha-expansion-panel { + display: block; + --expansion-panel-content-padding: 0; + border-radius: 6px; + } + ha-svg-icon, + ha-icon { + color: var(--secondary-text-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-form-expandable": HaFormExpendable; + } +} diff --git a/src/components/ha-form/ha-form-grid.ts b/src/components/ha-form/ha-form-grid.ts index c9fccfe4f3..2ccb082c8a 100644 --- a/src/components/ha-form/ha-form-grid.ts +++ b/src/components/ha-form/ha-form-grid.ts @@ -33,11 +33,6 @@ export class HaFormGrid extends LitElement implements HaFormElement { @property() public computeHelper?: (schema: HaFormSchema) => string; - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); - this.setAttribute("own-margin", ""); - } - protected updated(changedProps: PropertyValues): void { super.updated(changedProps); if (changedProps.has("schema")) { @@ -78,7 +73,8 @@ export class HaFormGrid extends LitElement implements HaFormElement { var(--form-grid-column-count, auto-fit), minmax(var(--form-grid-min-width, 200px), 1fr) ); - grid-gap: 8px; + grid-column-gap: 8px; + grid-row-gap: 24px; } :host > ha-form { display: block; diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index c90c4d0c12..8148b30d26 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -9,6 +9,7 @@ import "./ha-form-boolean"; import "./ha-form-constant"; import "./ha-form-float"; import "./ha-form-grid"; +import "./ha-form-expandable"; import "./ha-form-integer"; import "./ha-form-multi_select"; import "./ha-form-positive_time_period_dict"; diff --git a/src/components/ha-form/types.ts b/src/components/ha-form/types.ts index 3e3c649670..bce171a562 100644 --- a/src/components/ha-form/types.ts +++ b/src/components/ha-form/types.ts @@ -12,7 +12,8 @@ export type HaFormSchema = | HaFormMultiSelectSchema | HaFormTimeSchema | HaFormSelector - | HaFormGridSchema; + | HaFormGridSchema + | HaFormExpandableSchema; export interface HaFormBaseSchema { name: string; @@ -34,6 +35,17 @@ export interface HaFormGridSchema extends HaFormBaseSchema { schema: readonly HaFormSchema[]; } +export interface HaFormExpandableSchema extends HaFormBaseSchema { + type: "expandable"; + name: ""; + title: string; + icon?: string; + iconPath?: string; + expanded?: boolean; + headingLevel?: 1 | 2 | 3 | 4 | 5 | 6; + schema: readonly HaFormSchema[]; +} + export interface HaFormSelector extends HaFormBaseSchema { type?: never; selector: Selector; @@ -86,7 +98,9 @@ export interface HaFormTimeSchema extends HaFormBaseSchema { export type SchemaUnion< SchemaArray extends readonly HaFormSchema[], Schema = SchemaArray[number] -> = Schema extends HaFormGridSchema ? SchemaUnion : Schema; +> = Schema extends HaFormGridSchema | HaFormExpandableSchema + ? SchemaUnion + : Schema; export interface HaFormDataContainer { [key: string]: HaFormData; diff --git a/src/components/ha-hls-player.ts b/src/components/ha-hls-player.ts index 895bba4d18..e20b0ce694 100644 --- a/src/components/ha-hls-player.ts +++ b/src/components/ha-hls-player.ts @@ -8,6 +8,7 @@ import { TemplateResult, } from "lit"; import { customElement, property, query, state } from "lit/decorators"; +import { fireEvent } from "../common/dom/fire_event"; import { nextRender } from "../common/util/render-status"; import type { HomeAssistant } from "../types"; import "./ha-alert"; @@ -85,6 +86,7 @@ class HaHLSPlayer extends LitElement { .muted=${this.muted} ?playsinline=${this.playsInline} ?controls=${this.controls} + @loadeddata=${this._loadedData} >` : ""} `; @@ -318,6 +320,11 @@ class HaHLSPlayer extends LitElement { this._errorIsFatal = false; } + private _loadedData() { + // @ts-ignore + fireEvent(this, "load"); + } + static get styles(): CSSResultGroup { return css` :host, diff --git a/src/components/ha-web-rtc-player.ts b/src/components/ha-web-rtc-player.ts index 0e7d31bc3a..5de5e0c49e 100644 --- a/src/components/ha-web-rtc-player.ts +++ b/src/components/ha-web-rtc-player.ts @@ -8,6 +8,7 @@ import { } from "lit"; import { customElement, property, state, query } from "lit/decorators"; import { isComponentLoaded } from "../common/config/is_component_loaded"; +import { fireEvent } from "../common/dom/fire_event"; import { handleWebRtcOffer, WebRtcAnswer } from "../data/camera"; import { fetchWebRtcSettings } from "../data/rtsp_to_webrtc"; import type { HomeAssistant } from "../types"; @@ -59,6 +60,7 @@ class HaWebRtcPlayer extends LitElement { ?playsinline=${this.playsInline} ?controls=${this.controls} .poster=${this.posterUrl} + @loadeddata=${this._loadedData} > `; } @@ -188,6 +190,11 @@ class HaWebRtcPlayer extends LitElement { } } + private _loadedData() { + // @ts-ignore + fireEvent(this, "load"); + } + static get styles(): CSSResultGroup { return css` :host, diff --git a/src/components/tile/ha-tile-badge.ts b/src/components/tile/ha-tile-badge.ts new file mode 100644 index 0000000000..183f217109 --- /dev/null +++ b/src/components/tile/ha-tile-badge.ts @@ -0,0 +1,51 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import "../ha-icon"; + +@customElement("ha-tile-badge") +export class HaTileBadge extends LitElement { + @property() public iconPath?: string; + + @property() public icon?: string; + + protected render(): TemplateResult { + return html` +
+ ${this.icon + ? html`` + : html``} +
+ `; + } + + static get styles(): CSSResultGroup { + return css` + :host { + --tile-badge-background-color: rgb(var(--rgb-primary-color)); + --tile-badge-icon-color: rgb(var(--rgb-white-color)); + --mdc-icon-size: 12px; + } + .badge { + display: flex; + align-items: center; + justify-content: center; + line-height: 0; + width: 16px; + height: 16px; + border-radius: 8px; + background-color: var(--tile-badge-background-color); + transition: background-color 280ms ease-in-out; + } + .badge ha-icon, + .badge ha-svg-icon { + color: var(--tile-badge-icon-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-tile-badge": HaTileBadge; + } +} diff --git a/src/data/energy.ts b/src/data/energy.ts index 9b5c012dca..2ec9df45e3 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -272,11 +272,15 @@ export interface EnergyData { export const getReferencedStatisticIds = ( prefs: EnergyPreferences, - info: EnergyInfo + info: EnergyInfo, + exclude?: string[] ): string[] => { const statIDs: string[] = []; for (const source of prefs.energy_sources) { + if (exclude?.includes(source.type)) { + continue; + } if (source.type === "solar") { statIDs.push(source.stat_energy_from); continue; @@ -362,6 +366,7 @@ const getEnergyData = async ( } } + const waterStatIds: string[] = []; const consumptionStatIDs: string[] = []; for (const source of prefs.energy_sources) { // grid source @@ -370,8 +375,13 @@ const getEnergyData = async ( consumptionStatIDs.push(flowFrom.stat_energy_from); } } + if (source.type === "water") { + waterStatIds.push(source.stat_energy_from); + } } - const statIDs = getReferencedStatisticIds(prefs, info); + const energyStatIds = getReferencedStatisticIds(prefs, info, ["water"]); + + const allStatIDs = [...energyStatIds, ...waterStatIds]; const dayDifference = differenceInDays(end || new Date(), start); const period = @@ -381,19 +391,32 @@ const getEnergyData = async ( const startMinHour = addHours(start, -1); const lengthUnit = hass.config.unit_system.length || ""; - const units: StatisticsUnitConfiguration = { + const energyUnits: StatisticsUnitConfiguration = { energy: "kWh", volume: lengthUnit === "km" ? "m³" : "ft³", }; + const waterUnits: StatisticsUnitConfiguration = { + volume: lengthUnit === "km" ? "L" : "gal", + }; - const stats = await fetchStatistics( - hass!, - startMinHour, - end, - statIDs, - period, - units - ); + const stats = { + ...(await fetchStatistics( + hass!, + startMinHour, + end, + energyStatIds, + period, + energyUnits + )), + ...(await fetchStatistics( + hass!, + startMinHour, + end, + waterStatIds, + period, + waterUnits + )), + }; let statsCompare; let startCompare; @@ -409,14 +432,24 @@ const getEnergyData = async ( const compareStartMinHour = addHours(startCompare, -1); endCompare = addMilliseconds(start, -1); - statsCompare = await fetchStatistics( - hass!, - compareStartMinHour, - endCompare, - statIDs, - period, - units - ); + statsCompare = { + ...(await fetchStatistics( + hass!, + compareStartMinHour, + endCompare, + energyStatIds, + period, + energyUnits + )), + ...(await fetchStatistics( + hass!, + startMinHour, + end, + waterStatIds, + period, + waterUnits + )), + }; } let fossilEnergyConsumption: FossilEnergyConsumption | undefined; @@ -456,7 +489,7 @@ const getEnergyData = async ( } }); - const statsMetadataArray = await getStatisticMetadata(hass, statIDs); + const statsMetadataArray = await getStatisticMetadata(hass, allStatIDs); const statsMetadata: Record = {}; statsMetadataArray.forEach((x) => { statsMetadata[x.statistic_id] = x; @@ -671,4 +704,4 @@ export const getEnergyGasUnit = ( }; export const getEnergyWaterUnit = (hass: HomeAssistant): string | undefined => - hass.config.unit_system.length === "km" ? "m³" : "ft³"; + hass.config.unit_system.length === "km" ? "L" : "gal"; diff --git a/src/data/recorder.ts b/src/data/recorder.ts index 2d8115cdb3..f9a1711afd 100644 --- a/src/data/recorder.ts +++ b/src/data/recorder.ts @@ -88,7 +88,7 @@ export interface StatisticsUnitConfiguration { | "psi" | "mmHg"; temperature?: "°C" | "°F" | "K"; - volume?: "ft³" | "m³"; + volume?: "L" | "gal" | "ft³" | "m³"; } export interface StatisticsValidationResults { diff --git a/src/data/zha.ts b/src/data/zha.ts index f2e1d6363f..3cd291e22d 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -36,6 +36,8 @@ export interface Neighbor { ieee: string; nwk: string; lqi: string; + depth: string; + relationship: string; } export interface ZHADeviceEndpoint { diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index 32370c14c1..ca01aac40c 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -297,10 +297,6 @@ export class MoreInfoDialog extends LitElement { var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); } - :host([tab="info"]) ha-dialog[data-domain="camera"] { - --mdc-dialog-max-width: auto; - } - :host([tab="settings"]) ha-dialog { --dialog-content-padding: 0px; } @@ -319,8 +315,7 @@ export class MoreInfoDialog extends LitElement { cursor: default; } - :host([large]) ha-dialog:not([data-domain="camera"]), - :host([tab="info"][large]) ha-dialog[data-domain="camera"] { + :host([tab="info"][large]) { --mdc-dialog-min-width: 90vw; --mdc-dialog-max-width: 90vw; } @@ -328,6 +323,8 @@ export class MoreInfoDialog extends LitElement { :host([tab="info"]) ha-dialog[data-domain="camera"] { --dialog-content-padding: 0; + /* max height of the video is full screen, minus the height of the header of the dialog and the padding of the dialog (mdc-dialog-max-height: calc(100% - 72px)) */ + --video-max-height: calc(100vh - 113px - 72px); } `, ]; diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 9479740dbb..5fb9d6bf9c 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -934,6 +934,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { this._showDisabled = true; this._showReadOnly = true; this._showUnavailable = true; + this._showHidden = true; } static get styles(): CSSResultGroup { diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-manage-zigbee-device.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-manage-zigbee-device.ts index 4104f9edda..1be029cfe9 100644 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-manage-zigbee-device.ts +++ b/src/panels/config/integrations/integration-panels/zha/dialog-zha-manage-zigbee-device.ts @@ -27,7 +27,7 @@ import "./zha-cluster-commands"; import "./zha-manage-clusters"; import "./zha-device-binding"; import "./zha-group-binding"; -import "./zha-device-children"; +import "./zha-device-neighbors"; import "./zha-device-signature"; import { Tab, @@ -179,10 +179,11 @@ class DialogZHAManageZigbeeDevice extends LitElement { > ` : html` - + .narrow=${!this.large} + > ` )} @@ -221,7 +222,7 @@ class DialogZHAManageZigbeeDevice extends LitElement { device && (device.device_type === "Router" || device.device_type === "Coordinator") ) { - tabs.push("children"); + tabs.push("neighbors"); } return tabs; diff --git a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device.ts b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device.ts index a2854748ff..259e41dbdb 100644 --- a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device.ts +++ b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device.ts @@ -1,7 +1,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event"; import { ZHADevice } from "../../../../../data/zha"; -export type Tab = "clusters" | "bindings" | "signature" | "children"; +export type Tab = "clusters" | "bindings" | "signature" | "neighbors"; export interface ZHAManageZigbeeDeviceDialogParams { device: ZHADevice; diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-children.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-neighbors.ts similarity index 53% rename from src/panels/config/integrations/integration-panels/zha/zha-device-children.ts rename to src/panels/config/integrations/integration-panels/zha/zha-device-neighbors.ts index 52a1bdf1b3..e292f403f5 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-device-children.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-neighbors.ts @@ -16,12 +16,16 @@ export interface DeviceRowData extends DataTableRowData { id: string; name: string; lqi: number; + depth: number; + relationship: string; } -@customElement("zha-device-children") -class ZHADeviceChildren extends LitElement { +@customElement("zha-device-neighbors") +class ZHADeviceNeighbors extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; + @property({ type: Boolean }) public narrow!: boolean; + @property() public device: ZHADevice | undefined; @state() private _devices: Map | undefined; @@ -33,20 +37,22 @@ class ZHADeviceChildren extends LitElement { } } - private _deviceChildren = memoizeOne( + private _deviceNeighbors = memoizeOne( ( device: ZHADevice | undefined, devices: Map | undefined ) => { const outputDevices: DeviceRowData[] = []; if (device && devices) { - device.neighbors.forEach((child) => { - const zhaDevice: ZHADevice | undefined = devices.get(child.ieee); + device.neighbors.forEach((neighbor) => { + const zhaDevice: ZHADevice | undefined = devices.get(neighbor.ieee); if (zhaDevice) { outputDevices.push({ name: zhaDevice.user_given_name || zhaDevice.name, id: zhaDevice.device_reg_id, - lqi: parseInt(child.lqi), + lqi: parseInt(neighbor.lqi), + depth: parseInt(neighbor.depth), + relationship: neighbor.relationship, }); } }); @@ -55,22 +61,57 @@ class ZHADeviceChildren extends LitElement { } ); - private _columns: DataTableColumnContainer = { - name: { - title: "Name", - sortable: true, - filterable: true, - direction: "asc", - grows: true, - }, - lqi: { - title: "LQI", - sortable: true, - filterable: true, - type: "numeric", - width: "75px", - }, - }; + private _columns = memoizeOne( + (narrow: boolean): DataTableColumnContainer => + narrow + ? { + name: { + title: this.hass.localize("ui.panel.config.zha.neighbors.name"), + sortable: true, + filterable: true, + direction: "asc", + grows: true, + }, + lqi: { + title: this.hass.localize("ui.panel.config.zha.neighbors.lqi"), + sortable: true, + filterable: true, + type: "numeric", + width: "75px", + }, + } + : { + name: { + title: this.hass.localize("ui.panel.config.zha.neighbors.name"), + sortable: true, + filterable: true, + direction: "asc", + grows: true, + }, + lqi: { + title: this.hass.localize("ui.panel.config.zha.neighbors.lqi"), + sortable: true, + filterable: true, + type: "numeric", + width: "75px", + }, + relationship: { + title: this.hass.localize( + "ui.panel.config.zha.neighbors.relationship" + ), + sortable: true, + filterable: true, + width: "150px", + }, + depth: { + title: this.hass.localize("ui.panel.config.zha.neighbors.depth"), + sortable: true, + filterable: true, + type: "numeric", + width: "75px", + }, + } + ); protected render(): TemplateResult { if (!this.device) { @@ -85,8 +126,8 @@ class ZHADeviceChildren extends LitElement { >` : html`
- +
+ +
- ${imageUrl - ? html` - - ` - : html` - - `} +
+ ${imageUrl + ? html` + + ` + : html` + + `} + ${badge + ? html` + + ` + : null} +
= { + cooling: "var(--rgb-state-climate-cool-color)", + drying: "var(--rgb-state-climate-dry-color)", + heating: "var(--rgb-state-climate-heat-color)", + idle: "var(--rgb-state-climate-idle-color)", + off: "var(--rgb-state-climate-off-color)", +}; + +export const CLIMATE_HVAC_ACTION_ICONS: Record = { + cooling: mdiSnowflake, + drying: mdiWaterPercent, + heating: mdiFire, + idle: mdiClockOutline, + off: mdiPower, +}; + +export const computeClimateBadge: ComputeBadgeFunction = (stateObj) => { + const hvacAction = (stateObj as ClimateEntity).attributes.hvac_action; + + if (!hvacAction || hvacAction === "off") { + return undefined; + } + + return { + iconPath: CLIMATE_HVAC_ACTION_ICONS[hvacAction], + color: CLIMATE_HVAC_ACTION_COLORS[hvacAction], + }; +}; diff --git a/src/panels/lovelace/cards/tile/badges/tile-badge-person.ts b/src/panels/lovelace/cards/tile/badges/tile-badge-person.ts new file mode 100644 index 0000000000..7eaa3ea340 --- /dev/null +++ b/src/panels/lovelace/cards/tile/badges/tile-badge-person.ts @@ -0,0 +1,44 @@ +import { mdiHelp, mdiHome, mdiHomeExportOutline } from "@mdi/js"; +import { HassEntity } from "home-assistant-js-websocket"; +import { UNAVAILABLE_STATES } from "../../../../../data/entity"; +import { HomeAssistant } from "../../../../../types"; +import { ComputeBadgeFunction } from "./tile-badge"; + +function getZone(entity: HassEntity, hass: HomeAssistant) { + const state = entity.state; + if (state === "home" || state === "not_home") return undefined; + + const zones = Object.values(hass.states).filter((stateObj) => + stateObj.entity_id.startsWith("zone.") + ); + + return zones.find((z) => state === z.attributes.friendly_name); +} + +function personBadgeIcon(entity: HassEntity) { + const state = entity.state; + if (UNAVAILABLE_STATES.includes(state)) { + return mdiHelp; + } + return state === "not_home" ? mdiHomeExportOutline : mdiHome; +} + +function personBadgeColor(entity: HassEntity, inZone?: boolean) { + if (inZone) { + return "var(--rgb-state-person-zone-color)"; + } + const state = entity.state; + return state === "not_home" + ? "var(--rgb-state-person-not-home-color)" + : "var(--rgb-state-person-home-color)"; +} + +export const computePersonBadge: ComputeBadgeFunction = (stateObj, hass) => { + const zone = getZone(stateObj, hass); + + return { + iconPath: personBadgeIcon(stateObj), + icon: zone?.attributes.icon, + color: personBadgeColor(stateObj, Boolean(zone)), + }; +}; diff --git a/src/panels/lovelace/cards/tile/badges/tile-badge.ts b/src/panels/lovelace/cards/tile/badges/tile-badge.ts new file mode 100644 index 0000000000..eebda6b5e4 --- /dev/null +++ b/src/panels/lovelace/cards/tile/badges/tile-badge.ts @@ -0,0 +1,29 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { computeDomain } from "../../../../../common/entity/compute_domain"; +import { HomeAssistant } from "../../../../../types"; +import { computeClimateBadge } from "./tile-badge-climate"; +import { computePersonBadge } from "./tile-badge-person"; + +export type TileBadge = { + color: string; + icon?: string; + iconPath?: string; +}; + +export type ComputeBadgeFunction = ( + stateObj: HassEntity, + hass: HomeAssistant +) => TileBadge | undefined; + +export const computeTileBadge: ComputeBadgeFunction = (stateObj, hass) => { + const domain = computeDomain(stateObj.entity_id); + switch (domain) { + case "person": + case "device_tracker": + return computePersonBadge(stateObj, hass); + case "climate": + return computeClimateBadge(stateObj, hass); + default: + return undefined; + } +}; diff --git a/src/panels/lovelace/components/hui-image.ts b/src/panels/lovelace/components/hui-image.ts index fcf1654a0a..555bca93ad 100644 --- a/src/panels/lovelace/components/hui-image.ts +++ b/src/panels/lovelace/components/hui-image.ts @@ -16,6 +16,7 @@ import { CameraEntity, fetchThumbnailUrlWithCache } from "../../../data/camera"; import { UNAVAILABLE } from "../../../data/entity"; import { HomeAssistant } from "../../../types"; import "../../../components/ha-circular-progress"; +import type { HaCameraStream } from "../../../components/ha-camera-stream"; const UPDATE_INTERVAL = 10000; const DEFAULT_FILTER = "grayscale(100%)"; @@ -65,9 +66,9 @@ export class HuiImage extends LitElement { @state() private _loadedImageSrc?: string; - private _intersectionObserver?: IntersectionObserver; + @state() private _lastImageHeight?: number; - private _lastImageHeight?: number; + private _intersectionObserver?: IntersectionObserver; private _cameraUpdater?: number; @@ -192,6 +193,8 @@ export class HuiImage extends LitElement { style=${styleMap({ paddingBottom: useRatio ? `${((100 * this._ratio!.h) / this._ratio!.w).toFixed(2)}%` + : !this._lastImageHeight + ? "56.25%" : undefined, backgroundImage: useRatio && this._loadedImageSrc @@ -203,7 +206,7 @@ export class HuiImage extends LitElement { : undefined, })} class="container ${classMap({ - ratio: useRatio, + ratio: useRatio || !this._lastImageHeight, })}" > ${this.cameraImage && this.cameraView === "live" @@ -212,6 +215,7 @@ export class HuiImage extends LitElement { muted .hass=${this.hass} .stateObj=${cameraObj} + @load=${this._onVideoLoad} > ` : imageSrc === undefined @@ -235,7 +239,7 @@ export class HuiImage extends LitElement { id="brokenImage" style=${styleMap({ height: !useRatio - ? `${this._lastImageHeight || "100"}px` + ? `${this._lastImageHeight}px` || "100%" : undefined, })} >
` @@ -245,7 +249,7 @@ export class HuiImage extends LitElement { class="progress-container" style=${styleMap({ height: !useRatio - ? `${this._lastImageHeight || "100"}px` + ? `${this._lastImageHeight}px` || "100%" : undefined, })} > @@ -322,6 +326,13 @@ export class HuiImage extends LitElement { this._lastImageHeight = imgEl.offsetHeight; } + private async _onVideoLoad(ev: Event): Promise { + this._loadState = LoadState.Loaded; + const videoEl = ev.currentTarget as HaCameraStream; + await this.updateComplete; + this._lastImageHeight = videoEl.offsetHeight; + } + private async _updateCameraImageSrcAtInterval(): Promise { // If we hit the interval and it was still loading // it means we timed out so we should show the error. diff --git a/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts index c495d0f1b0..d5a4b84f43 100644 --- a/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts @@ -9,7 +9,6 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { computeDomain } from "../../../../common/entity/compute_domain"; import { domainIcon } from "../../../../common/entity/domain_icon"; import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter"; -import { LocalizeFunc } from "../../../../common/translations/localize"; import "../../../../components/ha-form/ha-form"; import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../types"; @@ -45,56 +44,82 @@ export class HuiTileCardEditor this._config = config; } - private _mainSchema = [{ name: "entity", selector: { entity: {} } }] as const; - - private _appearanceSchema = memoizeOne( - ( - localize: LocalizeFunc, - entity: string, - icon?: string, - entityState?: HassEntity - ) => + private _schema = memoizeOne( + (entity: string, icon?: string, entityState?: HassEntity) => [ + { name: "entity", selector: { entity: {} } }, { name: "", - type: "grid", + type: "expandable", + iconPath: mdiPalette, + title: this.hass!.localize( + `ui.panel.lovelace.editor.card.tile.appearance` + ), schema: [ - { name: "name", selector: { text: {} } }, { - name: "icon", - selector: { - icon: { - placeholder: icon || entityState?.attributes.icon, - fallbackPath: - !icon && !entityState?.attributes.icon && entityState - ? domainIcon(computeDomain(entity), entityState) - : undefined, - }, - }, - }, - { - name: "color", - selector: { - select: { - options: [ - { - label: localize( - `ui.panel.lovelace.editor.card.tile.default_color` - ), - value: "default", + name: "", + type: "grid", + schema: [ + { name: "name", selector: { text: {} } }, + { + name: "icon", + selector: { + icon: { + placeholder: icon || entityState?.attributes.icon, + fallbackPath: + !icon && !entityState?.attributes.icon && entityState + ? domainIcon(computeDomain(entity), entityState) + : undefined, }, - ...Array.from(THEME_COLORS).map((color) => ({ - label: capitalizeFirstLetter(color), - value: color, - })), - ], + }, }, + { + name: "color", + selector: { + select: { + options: [ + { + label: this.hass!.localize( + `ui.panel.lovelace.editor.card.tile.default_color` + ), + value: "default", + }, + ...Array.from(THEME_COLORS).map((color) => ({ + label: capitalizeFirstLetter(color), + value: color, + })), + ], + }, + }, + }, + { + name: "show_entity_picture", + selector: { + boolean: {}, + }, + }, + ] as const, + }, + ] as const, + }, + { + name: "", + type: "expandable", + title: this.hass!.localize( + `ui.panel.lovelace.editor.card.tile.actions` + ), + iconPath: mdiGestureTap, + schema: [ + { + name: "tap_action", + selector: { + "ui-action": {}, }, }, { - name: "show_entity_picture", + name: "icon_tap_action", selector: { - boolean: {}, + "ui-action": {}, }, }, ] as const, @@ -102,21 +127,6 @@ export class HuiTileCardEditor ] as const ); - private _actionsSchema = [ - { - name: "tap_action", - selector: { - "ui-action": {}, - }, - }, - { - name: "icon_tap_action", - selector: { - "ui-action": {}, - }, - }, - ] as const; - protected render(): TemplateResult { if (!this.hass || !this._config) { return html``; @@ -126,14 +136,7 @@ export class HuiTileCardEditor | HassEntity | undefined; - const mainSchema = this._mainSchema; - const appareanceSchema = this._appearanceSchema( - this.hass.localize, - this._config.entity, - this._config.icon, - entity - ); - const actionsSchema = this._actionsSchema; + const schema = this._schema(this._config.entity, this._config.icon, entity); const data = { color: "default", @@ -141,55 +144,13 @@ export class HuiTileCardEditor }; return html` -
-
- -
-
- -
- - ${this.hass!.localize( - `ui.panel.lovelace.editor.card.tile.appearance` - )} -
-
- -
-
-
-
- -
- - ${this.hass!.localize( - `ui.panel.lovelace.editor.card.tile.actions` - )} -
-
- -
-
-
-
+ `; } @@ -204,10 +165,7 @@ export class HuiTileCardEditor } private _computeLabelCallback = ( - schema: - | SchemaUnion - | SchemaUnion> - | SchemaUnion + schema: SchemaUnion> ) => { switch (schema.name) { case "color": @@ -230,20 +188,6 @@ export class HuiTileCardEditor display: flex; flex-direction: column; } - .group:not(:last-child) { - margin-bottom: 12px; - } - .content { - padding: 12px; - } - ha-expansion-panel { - --expansion-panel-content-padding: 0; - border: 1px solid var(--divider-color); - border-radius: 6px; - } - ha-svg-icon { - color: var(--secondary-text-color); - } `; } } diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts index 96c99e47b3..49e3e8a8f6 100644 --- a/src/resources/ha-style.ts +++ b/src/resources/ha-style.ts @@ -152,6 +152,9 @@ documentContainer.innerHTML = ` --rgb-state-lock-unlocked-color: var(--rgb-red-color); --rgb-state-media-player-color: var(--rgb-indigo-color); --rgb-state-person-color: var(--rgb-blue-grey-color); + --rgb-state-person-home-color: var(--rgb-green-color); + --rgb-state-person-not-home-color: var(--rgb-red-color); + --rgb-state-person-zone-color: var(--rgb-blue-color); --rgb-state-sensor-battery-high-color: var(--rgb-green-color); --rgb-state-sensor-battery-low-color: var(--rgb-red-color); --rgb-state-sensor-battery-medium-color: var(--rgb-orange-color); @@ -173,6 +176,7 @@ documentContainer.innerHTML = ` --rgb-state-climate-fan-only-color: var(--rgb-teal-color); --rgb-state-climate-heat-color: var(--rgb-deep-orange-color); --rgb-state-climate-heat-cool-color: var(--rgb-green-color); + --rgb-state-climate-idle-color: var(--rgb-disabled-color); /* input components */ --input-idle-line-color: rgba(0, 0, 0, 0.42); diff --git a/src/translations/en.json b/src/translations/en.json index bd2a812e7e..3eca0b3c6e 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1024,7 +1024,7 @@ "clusters": "Clusters", "bindings": "Bindings", "signature": "Signature", - "children": "Children" + "neighbors": "Neighbors" } }, "zha_device_info": { @@ -3247,6 +3247,12 @@ "unbind_button_label": "Unbind Group", "bind_button_help": "Bind the selected group to the selected device clusters.", "unbind_button_help": "Unbind the selected group from the selected device clusters." + }, + "neighbors": { + "name": "Name", + "lqi": "LQI", + "relationship": "Relationship", + "depth": "Depth" } }, "zwave_js": {