mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-15 16:09:43 +00:00
Compare commits
6 Commits
sidebar-re
...
dev
Author | SHA1 | Date | |
---|---|---|---|
![]() |
98c4e34a23 | ||
![]() |
3d005c8316 | ||
![]() |
af31b5add3 | ||
![]() |
9d02a1d391 | ||
![]() |
98e6f32fe8 | ||
![]() |
2726c6a849 |
@@ -10,9 +10,10 @@ import { stripPrefixFromEntityName } from "./strip_prefix_from_entity_name";
|
||||
|
||||
export const computeEntityName = (
|
||||
stateObj: HassEntity,
|
||||
hass: HomeAssistant
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): string | undefined => {
|
||||
const entry = hass.entities[stateObj.entity_id] as
|
||||
const entry = entities[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
|
||||
@@ -20,12 +21,13 @@ export const computeEntityName = (
|
||||
// Fall back to state name if not in the entity registry (friendly name)
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
return computeEntityEntryName(entry, hass);
|
||||
return computeEntityEntryName(entry, devices);
|
||||
};
|
||||
|
||||
export const computeEntityEntryName = (
|
||||
entry: EntityRegistryDisplayEntry | EntityRegistryEntry,
|
||||
hass: HomeAssistant
|
||||
devices: HomeAssistant["devices"],
|
||||
fallbackStateObj?: HassEntity
|
||||
): string | undefined => {
|
||||
const name =
|
||||
entry.name ||
|
||||
@@ -33,15 +35,14 @@ export const computeEntityEntryName = (
|
||||
? String(entry.original_name)
|
||||
: undefined);
|
||||
|
||||
const device = entry.device_id ? hass.devices[entry.device_id] : undefined;
|
||||
const device = entry.device_id ? devices[entry.device_id] : undefined;
|
||||
|
||||
if (!device) {
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
const stateObj = hass.states[entry.entity_id] as HassEntity | undefined;
|
||||
if (stateObj) {
|
||||
return computeStateName(stateObj);
|
||||
if (fallbackStateObj) {
|
||||
return computeStateName(fallbackStateObj);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
@@ -18,9 +18,12 @@ interface EntityContext {
|
||||
|
||||
export const getEntityContext = (
|
||||
stateObj: HassEntity,
|
||||
hass: HomeAssistant
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"]
|
||||
): EntityContext => {
|
||||
const entry = hass.entities[stateObj.entity_id] as
|
||||
const entry = entities[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
|
||||
@@ -32,7 +35,7 @@ export const getEntityContext = (
|
||||
floor: null,
|
||||
};
|
||||
}
|
||||
return getEntityEntryContext(entry, hass);
|
||||
return getEntityEntryContext(entry, entities, devices, areas, floors);
|
||||
};
|
||||
|
||||
export const getEntityEntryContext = (
|
||||
@@ -40,15 +43,18 @@ export const getEntityEntryContext = (
|
||||
| EntityRegistryDisplayEntry
|
||||
| EntityRegistryEntry
|
||||
| ExtEntityRegistryEntry,
|
||||
hass: HomeAssistant
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"]
|
||||
): EntityContext => {
|
||||
const entity = hass.entities[entry.entity_id];
|
||||
const entity = entities[entry.entity_id];
|
||||
const deviceId = entry?.device_id;
|
||||
const device = deviceId ? hass.devices[deviceId] : undefined;
|
||||
const device = deviceId ? devices[deviceId] : undefined;
|
||||
const areaId = entry?.area_id || device?.area_id;
|
||||
const area = areaId ? hass.areas[areaId] : undefined;
|
||||
const area = areaId ? areas[areaId] : undefined;
|
||||
const floorId = area?.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : undefined;
|
||||
const floor = floorId ? floors[floorId] : undefined;
|
||||
|
||||
return {
|
||||
entity: entity,
|
||||
|
@@ -60,7 +60,13 @@ export const generateEntityFilter = (
|
||||
}
|
||||
}
|
||||
|
||||
const { area, floor, device, entity } = getEntityContext(stateObj, hass);
|
||||
const { area, floor, device, entity } = getEntityContext(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
if (entity && entity.hidden) {
|
||||
return false;
|
||||
|
@@ -2,6 +2,12 @@ import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LocalizeFunc } from "./localize";
|
||||
import { computeEntityName } from "../entity/compute_entity_name";
|
||||
import { computeDeviceName } from "../entity/compute_device_name";
|
||||
import { getEntityContext } from "../entity/context/get_entity_context";
|
||||
import { computeAreaName } from "../entity/compute_area_name";
|
||||
import { computeFloorName } from "../entity/compute_floor_name";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
|
||||
export type FormatEntityStateFunc = (
|
||||
stateObj: HassEntity,
|
||||
@@ -17,16 +23,28 @@ export type FormatEntityAttributeNameFunc = (
|
||||
attribute: string
|
||||
) => string;
|
||||
|
||||
export type EntityNameType = "entity" | "device" | "area" | "floor";
|
||||
|
||||
export type FormatEntityNameFunc = (
|
||||
stateObj: HassEntity,
|
||||
type: EntityNameType | EntityNameType[],
|
||||
separator?: string
|
||||
) => string;
|
||||
|
||||
export const computeFormatFunctions = async (
|
||||
localize: LocalizeFunc,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"],
|
||||
sensorNumericDeviceClasses: string[]
|
||||
): Promise<{
|
||||
formatEntityState: FormatEntityStateFunc;
|
||||
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
|
||||
formatEntityAttributeName: FormatEntityAttributeNameFunc;
|
||||
formatEntityName: FormatEntityNameFunc;
|
||||
}> => {
|
||||
const { computeStateDisplay } = await import(
|
||||
"../entity/compute_state_display"
|
||||
@@ -57,5 +75,45 @@ export const computeFormatFunctions = async (
|
||||
),
|
||||
formatEntityAttributeName: (stateObj, attribute) =>
|
||||
computeAttributeNameDisplay(localize, stateObj, entities, attribute),
|
||||
formatEntityName: (stateObj, type, separator = " ") => {
|
||||
const types = ensureArray(type);
|
||||
const namesList: (string | undefined)[] = [];
|
||||
|
||||
const { device, area, floor } = getEntityContext(
|
||||
stateObj,
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors
|
||||
);
|
||||
|
||||
for (const t of types) {
|
||||
switch (t) {
|
||||
case "entity": {
|
||||
namesList.push(computeEntityName(stateObj, entities, devices));
|
||||
break;
|
||||
}
|
||||
case "device": {
|
||||
if (device) {
|
||||
namesList.push(computeDeviceName(device));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "area": {
|
||||
if (area) {
|
||||
namesList.push(computeAreaName(area));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "floor": {
|
||||
if (floor) {
|
||||
namesList.push(computeFloorName(floor));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return namesList.filter((name) => name !== undefined).join(separator);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@@ -5,12 +5,8 @@ import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { domainToName } from "../../data/integration";
|
||||
@@ -148,11 +144,9 @@ export class HaEntityPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const { area, device } = getEntityContext(stateObj, this.hass);
|
||||
|
||||
const entityName = computeEntityName(stateObj, this.hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
const entityName = this.hass.formatEntityName(stateObj, "entity");
|
||||
const deviceName = this.hass.formatEntityName(stateObj, "device");
|
||||
const areaName = this.hass.formatEntityName(stateObj, "area");
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
@@ -311,12 +305,10 @@ export class HaEntityPicker extends LitElement {
|
||||
items = entityIds.map<EntityComboBoxItem>((entityId) => {
|
||||
const stateObj = hass!.states[entityId];
|
||||
|
||||
const { area, device } = getEntityContext(stateObj, hass);
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const entityName = computeEntityName(stateObj, hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
const entityName = this.hass.formatEntityName(stateObj, "entity");
|
||||
const deviceName = this.hass.formatEntityName(stateObj, "device");
|
||||
const areaName = this.hass.formatEntityName(stateObj, "area");
|
||||
|
||||
const domainName = domainToName(
|
||||
this.hass.localize,
|
||||
|
@@ -6,11 +6,7 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import {
|
||||
@@ -259,12 +255,10 @@ export class HaStatisticPicker extends LitElement {
|
||||
}
|
||||
const id = meta.statistic_id;
|
||||
|
||||
const { area, device } = getEntityContext(stateObj, hass);
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const entityName = computeEntityName(stateObj, hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
const entityName = hass.formatEntityName(stateObj, "entity");
|
||||
const deviceName = hass.formatEntityName(stateObj, "device");
|
||||
const areaName = hass.formatEntityName(stateObj, "area");
|
||||
|
||||
const primary = entityName || deviceName || id;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
@@ -337,11 +331,9 @@ export class HaStatisticPicker extends LitElement {
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
|
||||
if (stateObj) {
|
||||
const { area, device } = getEntityContext(stateObj, this.hass);
|
||||
|
||||
const entityName = computeEntityName(stateObj, this.hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
const entityName = this.hass.formatEntityName(stateObj, "entity");
|
||||
const deviceName = this.hass.formatEntityName(stateObj, "device");
|
||||
const areaName = this.hass.formatEntityName(stateObj, "area");
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
|
@@ -28,6 +28,9 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
|
||||
@property({ reflect: true }) size: "small" | "medium" = "medium";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "no-wrap" })
|
||||
public nowrap = false;
|
||||
|
||||
@property() public variant:
|
||||
| "brand"
|
||||
| "neutral"
|
||||
@@ -71,6 +74,10 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
:host {
|
||||
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
|
||||
}
|
||||
|
||||
:host([no-wrap]) wa-button-group::part(base) {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -330,7 +330,13 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
private _renderInfo = (completion: Completion): CompletionInfo => {
|
||||
const key = completion.label;
|
||||
const context = getEntityContext(this.hass!.states[key], this.hass!);
|
||||
const context = getEntityContext(
|
||||
this.hass!.states[key],
|
||||
this.hass!.entities,
|
||||
this.hass!.devices,
|
||||
this.hass!.areas,
|
||||
this.hass!.floors
|
||||
);
|
||||
|
||||
const completionInfo = document.createElement("div");
|
||||
completionInfo.classList.add("completion-info");
|
||||
|
@@ -246,6 +246,8 @@ export class HaMediaSelector extends LitElement {
|
||||
entityId: this._getActiveEntityId(),
|
||||
navigateIds: this.value?.metadata?.navigateIds,
|
||||
accept: this.selector.media?.accept,
|
||||
defaultId: this.value?.media_content_id,
|
||||
defaultType: this.value?.media_content_type,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
|
@@ -165,6 +165,8 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
.action=${this._action}
|
||||
.preferredLayout=${this._preferredLayout}
|
||||
.accept=${this._params.accept}
|
||||
.defaultId=${this._params.defaultId}
|
||||
.defaultType=${this._params.defaultType}
|
||||
@close-dialog=${this.closeDialog}
|
||||
@media-picked=${this._mediaPicked}
|
||||
@media-browsed=${this._mediaBrowsed}
|
||||
|
112
src/components/media-player/ha-browse-media-manual.ts
Normal file
112
src/components/media-player/ha-browse-media-manual.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import "../ha-card";
|
||||
import "../ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../ha-form/types";
|
||||
import type { MediaPlayerItemId } from "./ha-media-player-browse";
|
||||
|
||||
export interface ManualMediaPickedEvent {
|
||||
item: MediaPlayerItemId;
|
||||
}
|
||||
|
||||
@customElement("ha-browse-media-manual")
|
||||
class BrowseMediaManual extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public item!: MediaPlayerItemId;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
name: "media_content_id",
|
||||
required: true,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "media_content_type",
|
||||
required: false,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.schema=${this._schema()}
|
||||
.data=${this.item}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button @click=${this._mediaPicked}>
|
||||
${this.hass.localize("ui.common.submit")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
const value = { ...ev.detail.value };
|
||||
|
||||
this.item = value;
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
entry: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(`ui.components.selectors.media.${entry.name}`);
|
||||
|
||||
private _computeHelper = (
|
||||
entry: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(`ui.components.selectors.media.${entry.name}_detail`);
|
||||
|
||||
private _mediaPicked() {
|
||||
fireEvent(this, "manual-media-picked", {
|
||||
item: {
|
||||
media_content_id: this.item.media_content_id || "",
|
||||
media_content_type: this.item.media_content_type || "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
margin: 16px auto;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 448px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-browse-media-manual": BrowseMediaManual;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"manual-media-picked": ManualMediaPickedEvent;
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
import { grid } from "@lit-labs/virtualizer/layouts/grid";
|
||||
|
||||
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
|
||||
import { mdiArrowUpRight, mdiPlay, mdiPlus, mdiKeyboard } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
@@ -28,7 +28,11 @@ import {
|
||||
BROWSER_PLAYER,
|
||||
MediaClassBrowserSettings,
|
||||
} from "../../data/media-player";
|
||||
import { browseLocalMediaPlayer } from "../../data/media_source";
|
||||
import {
|
||||
browseLocalMediaPlayer,
|
||||
isManualMediaSourceContentId,
|
||||
MANUAL_MEDIA_SOURCE_PREFIX,
|
||||
} from "../../data/media_source";
|
||||
import { isTTSMediaSource } from "../../data/tts";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
@@ -53,7 +57,9 @@ import "../ha-spinner";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
import "./ha-browse-media-tts";
|
||||
import "./ha-browse-media-manual";
|
||||
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
|
||||
import type { ManualMediaPickedEvent } from "./ha-browse-media-manual";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -74,6 +80,18 @@ export interface MediaPlayerItemId {
|
||||
media_content_type: string | undefined;
|
||||
}
|
||||
|
||||
const MANUAL_ITEM: MediaPlayerItem = {
|
||||
can_expand: true,
|
||||
can_play: false,
|
||||
can_search: false,
|
||||
children_media_class: "",
|
||||
media_class: "app",
|
||||
media_content_id: MANUAL_MEDIA_SOURCE_PREFIX,
|
||||
media_content_type: "",
|
||||
iconPath: mdiKeyboard,
|
||||
title: "Manual entry",
|
||||
};
|
||||
|
||||
@customElement("ha-media-player-browse")
|
||||
export class HaMediaPlayerBrowse extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -91,6 +109,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public accept?: string[];
|
||||
|
||||
@property({ attribute: false }) public defaultId?: string;
|
||||
|
||||
@property({ attribute: false }) public defaultType?: string;
|
||||
|
||||
// @todo Consider reworking to eliminate need for attribute since it is manipulated internally
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@@ -216,56 +238,69 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
}
|
||||
// Fetch current
|
||||
if (!currentProm) {
|
||||
currentProm = this._fetchData(
|
||||
this.entityId,
|
||||
currentId.media_content_id,
|
||||
currentId.media_content_type
|
||||
if (
|
||||
currentId.media_content_id &&
|
||||
isManualMediaSourceContentId(currentId.media_content_id)
|
||||
) {
|
||||
this._currentItem = MANUAL_ITEM;
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: navigateIds,
|
||||
current: this._currentItem,
|
||||
});
|
||||
} else {
|
||||
if (!currentProm) {
|
||||
currentProm = this._fetchData(
|
||||
this.entityId,
|
||||
currentId.media_content_id,
|
||||
currentId.media_content_type
|
||||
);
|
||||
}
|
||||
currentProm.then(
|
||||
(item) => {
|
||||
this._currentItem = item;
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: navigateIds,
|
||||
current: item,
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
// When we change entity ID, we will first try to see if the new entity is
|
||||
// able to resolve the new path. If that results in an error, browse the root.
|
||||
const isNewEntityWithSamePath =
|
||||
oldNavigateIds &&
|
||||
changedProps.has("entityId") &&
|
||||
navigateIds.length === oldNavigateIds.length &&
|
||||
oldNavigateIds.every(
|
||||
(oldItem, idx) =>
|
||||
navigateIds[idx].media_content_id ===
|
||||
oldItem.media_content_id &&
|
||||
navigateIds[idx].media_content_type ===
|
||||
oldItem.media_content_type
|
||||
);
|
||||
if (isNewEntityWithSamePath) {
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: [
|
||||
{ media_content_id: undefined, media_content_type: undefined },
|
||||
],
|
||||
replace: true,
|
||||
});
|
||||
} else if (
|
||||
err.code === "entity_not_found" &&
|
||||
this.entityId &&
|
||||
isUnavailableState(this.hass.states[this.entityId]?.state)
|
||||
) {
|
||||
this._setError({
|
||||
message: this.hass.localize(
|
||||
`ui.components.media-browser.media_player_unavailable`
|
||||
),
|
||||
code: "entity_not_found",
|
||||
});
|
||||
} else {
|
||||
this._setError(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
currentProm.then(
|
||||
(item) => {
|
||||
this._currentItem = item;
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: navigateIds,
|
||||
current: item,
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
// When we change entity ID, we will first try to see if the new entity is
|
||||
// able to resolve the new path. If that results in an error, browse the root.
|
||||
const isNewEntityWithSamePath =
|
||||
oldNavigateIds &&
|
||||
changedProps.has("entityId") &&
|
||||
navigateIds.length === oldNavigateIds.length &&
|
||||
oldNavigateIds.every(
|
||||
(oldItem, idx) =>
|
||||
navigateIds[idx].media_content_id === oldItem.media_content_id &&
|
||||
navigateIds[idx].media_content_type === oldItem.media_content_type
|
||||
);
|
||||
if (isNewEntityWithSamePath) {
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: [
|
||||
{ media_content_id: undefined, media_content_type: undefined },
|
||||
],
|
||||
replace: true,
|
||||
});
|
||||
} else if (
|
||||
err.code === "entity_not_found" &&
|
||||
this.entityId &&
|
||||
isUnavailableState(this.hass.states[this.entityId]?.state)
|
||||
) {
|
||||
this._setError({
|
||||
message: this.hass.localize(
|
||||
`ui.components.media-browser.media_player_unavailable`
|
||||
),
|
||||
code: "entity_not_found",
|
||||
});
|
||||
} else {
|
||||
this._setError(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
// Fetch parent
|
||||
if (!parentProm && parentId !== undefined) {
|
||||
parentProm = this._fetchData(
|
||||
@@ -479,111 +514,120 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
</ha-alert>
|
||||
</div>
|
||||
`
|
||||
: isTTSMediaSource(currentItem.media_content_id)
|
||||
? html`
|
||||
<ha-browse-media-tts
|
||||
.item=${currentItem}
|
||||
.hass=${this.hass}
|
||||
.action=${this.action}
|
||||
@tts-picked=${this._ttsPicked}
|
||||
></ha-browse-media-tts>
|
||||
`
|
||||
: !children.length && !currentItem.not_shown
|
||||
: isManualMediaSourceContentId(currentItem.media_content_id)
|
||||
? html`<ha-browse-media-manual
|
||||
.item=${{
|
||||
media_content_id: this.defaultId || "",
|
||||
media_content_type: this.defaultType || "",
|
||||
}}
|
||||
.hass=${this.hass}
|
||||
@manual-media-picked=${this._manualPicked}
|
||||
></ha-browse-media-manual>`
|
||||
: isTTSMediaSource(currentItem.media_content_id)
|
||||
? html`
|
||||
<div class="container no-items">
|
||||
${currentItem.media_content_id ===
|
||||
"media-source://media_source/local/."
|
||||
? html`
|
||||
<div class="highlight-add-button">
|
||||
<span>
|
||||
<ha-svg-icon
|
||||
.path=${mdiArrowUpRight}
|
||||
></ha-svg-icon>
|
||||
</span>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.highlight_button"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.components.media-browser.no_items"
|
||||
)}
|
||||
</div>
|
||||
<ha-browse-media-tts
|
||||
.item=${currentItem}
|
||||
.hass=${this.hass}
|
||||
.action=${this.action}
|
||||
@tts-picked=${this._ttsPicked}
|
||||
></ha-browse-media-tts>
|
||||
`
|
||||
: this.preferredLayout === "grid" ||
|
||||
(this.preferredLayout === "auto" &&
|
||||
childrenMediaClass.layout === "grid")
|
||||
: !children.length && !currentItem.not_shown
|
||||
? html`
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
.layout=${grid({
|
||||
itemSize: {
|
||||
width: "175px",
|
||||
height:
|
||||
childrenMediaClass.thumbnail_ratio ===
|
||||
"portrait"
|
||||
? "312px"
|
||||
: "225px",
|
||||
},
|
||||
gap: "16px",
|
||||
flex: { preserve: "aspect-ratio" },
|
||||
justify: "space-evenly",
|
||||
direction: "vertical",
|
||||
})}
|
||||
.items=${children}
|
||||
.renderItem=${this._renderGridItem}
|
||||
class="children ${classMap({
|
||||
portrait:
|
||||
childrenMediaClass.thumbnail_ratio ===
|
||||
"portrait",
|
||||
not_shown: !!currentItem.not_shown,
|
||||
})}"
|
||||
></lit-virtualizer>
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<div class="grid not-shown">
|
||||
<div class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
<div class="container no-items">
|
||||
${currentItem.media_content_id ===
|
||||
"media-source://media_source/local/."
|
||||
? html`
|
||||
<div class="highlight-add-button">
|
||||
<span>
|
||||
<ha-svg-icon
|
||||
.path=${mdiArrowUpRight}
|
||||
></ha-svg-icon>
|
||||
</span>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.highlight_button"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.components.media-browser.no_items"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-list>
|
||||
: this.preferredLayout === "grid" ||
|
||||
(this.preferredLayout === "auto" &&
|
||||
childrenMediaClass.layout === "grid")
|
||||
? html`
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
.items=${children}
|
||||
style=${styleMap({
|
||||
height: `${children.length * 72 + 26}px`,
|
||||
.layout=${grid({
|
||||
itemSize: {
|
||||
width: "175px",
|
||||
height:
|
||||
childrenMediaClass.thumbnail_ratio ===
|
||||
"portrait"
|
||||
? "312px"
|
||||
: "225px",
|
||||
},
|
||||
gap: "16px",
|
||||
flex: { preserve: "aspect-ratio" },
|
||||
justify: "space-evenly",
|
||||
direction: "vertical",
|
||||
})}
|
||||
.renderItem=${this._renderListItem}
|
||||
.items=${children}
|
||||
.renderItem=${this._renderGridItem}
|
||||
class="children ${classMap({
|
||||
portrait:
|
||||
childrenMediaClass.thumbnail_ratio ===
|
||||
"portrait",
|
||||
not_shown: !!currentItem.not_shown,
|
||||
})}"
|
||||
></lit-virtualizer>
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<ha-list-item
|
||||
noninteractive
|
||||
class="not-shown"
|
||||
.graphic=${mediaClass.show_list_images
|
||||
? "medium"
|
||||
: "avatar"}
|
||||
>
|
||||
<span class="title">
|
||||
<div class="grid not-shown">
|
||||
<div class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-list>
|
||||
`
|
||||
`
|
||||
: html`
|
||||
<ha-list>
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
.items=${children}
|
||||
style=${styleMap({
|
||||
height: `${children.length * 72 + 26}px`,
|
||||
})}
|
||||
.renderItem=${this._renderListItem}
|
||||
></lit-virtualizer>
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<ha-list-item
|
||||
noninteractive
|
||||
class="not-shown"
|
||||
.graphic=${mediaClass.show_list_images
|
||||
? "medium"
|
||||
: "avatar"}
|
||||
>
|
||||
<span class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
: ""}
|
||||
</ha-list>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -617,8 +661,9 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${MediaClassBrowserSettings[
|
||||
class=${child.iconPath ? "icon" : "folder"}
|
||||
.path=${child.iconPath ||
|
||||
MediaClassBrowserSettings[
|
||||
child.media_class === "directory"
|
||||
? child.children_media_class || child.media_class
|
||||
: child.media_class
|
||||
@@ -768,6 +813,14 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _manualPicked(ev: CustomEvent<ManualMediaPickedEvent>) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "media-picked", {
|
||||
item: ev.detail.item as MediaPlayerItem,
|
||||
navigateIds: this.navigateIds,
|
||||
});
|
||||
}
|
||||
|
||||
private _childClicked = async (ev: MouseEvent): Promise<void> => {
|
||||
const target = ev.currentTarget as any;
|
||||
const item: MediaPlayerItem = target.item;
|
||||
@@ -791,9 +844,23 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
mediaContentId?: string,
|
||||
mediaContentType?: string
|
||||
): Promise<MediaPlayerItem> {
|
||||
return entityId && entityId !== BROWSER_PLAYER
|
||||
? browseMediaPlayer(this.hass, entityId, mediaContentId, mediaContentType)
|
||||
: browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||
const prom =
|
||||
entityId && entityId !== BROWSER_PLAYER
|
||||
? browseMediaPlayer(
|
||||
this.hass,
|
||||
entityId,
|
||||
mediaContentId,
|
||||
mediaContentType
|
||||
)
|
||||
: browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||
|
||||
return prom.then((item) => {
|
||||
if (!mediaContentId && this.action === "pick") {
|
||||
item.children = item.children || [];
|
||||
item.children.push(MANUAL_ITEM);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
private _measureCard(): void {
|
||||
@@ -1141,6 +1208,11 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
||||
}
|
||||
|
||||
.child .icon {
|
||||
color: #00a9f7; /* Match the png color from brands repo */
|
||||
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
||||
}
|
||||
|
||||
.child .play {
|
||||
position: absolute;
|
||||
transition: color 0.5s;
|
||||
|
@@ -12,6 +12,8 @@ export interface MediaPlayerBrowseDialogParams {
|
||||
navigateIds?: MediaPlayerItemId[];
|
||||
minimumNavigateLevel?: number;
|
||||
accept?: string[];
|
||||
defaultId?: string;
|
||||
defaultType?: string;
|
||||
}
|
||||
|
||||
export const showMediaBrowserDialog = (
|
||||
|
@@ -199,10 +199,12 @@ export interface MediaPlayerItem {
|
||||
media_content_type: string;
|
||||
media_content_id: string;
|
||||
media_class: keyof TranslationDict["ui"]["components"]["media-browser"]["class"];
|
||||
children_media_class?: string;
|
||||
children_media_class?: string | null;
|
||||
can_play: boolean;
|
||||
can_expand: boolean;
|
||||
can_search: boolean;
|
||||
thumbnail?: string;
|
||||
iconPath?: string;
|
||||
children?: MediaPlayerItem[];
|
||||
not_shown?: number;
|
||||
}
|
||||
|
@@ -24,6 +24,11 @@ export const browseLocalMediaPlayer = (
|
||||
media_content_id: mediaContentId,
|
||||
});
|
||||
|
||||
export const MANUAL_MEDIA_SOURCE_PREFIX = "__MANUAL_ENTRY__";
|
||||
|
||||
export const isManualMediaSourceContentId = (mediaContentId: string) =>
|
||||
mediaContentId.startsWith(MANUAL_MEDIA_SOURCE_PREFIX);
|
||||
|
||||
export const isMediaSourceContentId = (mediaId: string) =>
|
||||
mediaId.startsWith("media-source://");
|
||||
|
||||
|
@@ -1,8 +1,5 @@
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { Selector } from "../selector";
|
||||
@@ -79,10 +76,8 @@ export const formatSelectorValue = (
|
||||
if (!stateObj) {
|
||||
return entityId;
|
||||
}
|
||||
const { device } = getEntityContext(stateObj, hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const entityName = computeEntityName(stateObj, hass);
|
||||
return [deviceName, entityName].filter(Boolean).join(" ") || entityId;
|
||||
const name = hass.formatEntityName(stateObj, ["device", "entity"], " ");
|
||||
return name || entityId;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
@@ -23,14 +23,8 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import {
|
||||
computeEntityEntryName,
|
||||
computeEntityName,
|
||||
} from "../../common/entity/compute_entity_name";
|
||||
import {
|
||||
getEntityContext,
|
||||
getEntityEntryContext,
|
||||
} from "../../common/entity/context/get_entity_context";
|
||||
import { computeEntityEntryName } from "../../common/entity/compute_entity_name";
|
||||
import { getEntityEntryContext } from "../../common/entity/context/get_entity_context";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import "../../components/ha-button-menu";
|
||||
@@ -322,22 +316,28 @@ export class MoreInfoDialog extends LitElement {
|
||||
(isDefaultView && this._parentEntityIds.length === 0) ||
|
||||
isSpecificInitialView;
|
||||
|
||||
const context = stateObj
|
||||
? getEntityContext(stateObj, this.hass)
|
||||
: this._entry
|
||||
? getEntityEntryContext(this._entry, this.hass)
|
||||
: undefined;
|
||||
let entityName: string | undefined;
|
||||
let deviceName: string | undefined;
|
||||
let areaName: string | undefined;
|
||||
|
||||
const entityName = stateObj
|
||||
? computeEntityName(stateObj, this.hass)
|
||||
: this._entry
|
||||
? computeEntityEntryName(this._entry, this.hass)
|
||||
: entityId;
|
||||
|
||||
const deviceName = context?.device
|
||||
? computeDeviceName(context.device)
|
||||
: undefined;
|
||||
const areaName = context?.area ? computeAreaName(context.area) : undefined;
|
||||
if (stateObj) {
|
||||
entityName = this.hass.formatEntityName(stateObj, "entity");
|
||||
deviceName = this.hass.formatEntityName(stateObj, "device");
|
||||
areaName = this.hass.formatEntityName(stateObj, "area");
|
||||
} else if (this._entry) {
|
||||
const { device, area } = getEntityEntryContext(
|
||||
this._entry,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
entityName = computeEntityEntryName(this._entry, this.hass.devices);
|
||||
deviceName = device ? computeDeviceName(device) : undefined;
|
||||
areaName = area ? computeAreaName(area) : undefined;
|
||||
} else {
|
||||
entityName = entityId;
|
||||
}
|
||||
|
||||
const breadcrumb = [areaName, deviceName, entityName].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
|
@@ -21,15 +21,10 @@ import { componentsWithService } from "../../common/config/components_with_servi
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import {
|
||||
computeDeviceName,
|
||||
computeDeviceNameDisplay,
|
||||
} from "../../common/entity/compute_device_name";
|
||||
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
|
||||
@@ -635,12 +630,10 @@ export class QuickBar extends LitElement {
|
||||
.map((entityId) => {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
|
||||
const { area, device } = getEntityContext(stateObj, this.hass);
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const entityName = computeEntityName(stateObj, this.hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
const entityName = this.hass.formatEntityName(stateObj, "entity");
|
||||
const deviceName = this.hass.formatEntityName(stateObj, "device");
|
||||
const areaName = this.hass.formatEntityName(stateObj, "area");
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
|
@@ -114,17 +114,22 @@ export const provideHass = (
|
||||
formatEntityState,
|
||||
formatEntityAttributeName,
|
||||
formatEntityAttributeValue,
|
||||
formatEntityName,
|
||||
} = await computeFormatFunctions(
|
||||
hass().localize,
|
||||
hass().locale,
|
||||
hass().config,
|
||||
hass().entities,
|
||||
hass().devices,
|
||||
hass().areas,
|
||||
hass().floors,
|
||||
[] // numericDeviceClasses
|
||||
);
|
||||
hass().updateHass({
|
||||
formatEntityState,
|
||||
formatEntityAttributeName,
|
||||
formatEntityAttributeValue,
|
||||
formatEntityName,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -161,6 +161,8 @@ export class HAFullCalendar extends LitElement {
|
||||
<ha-button-toggle-group
|
||||
.buttons=${viewToggleButtons}
|
||||
.active=${this._activeView}
|
||||
size="small"
|
||||
no-wrap
|
||||
@value-changed=${this._handleView}
|
||||
></ha-button-toggle-group>
|
||||
`
|
||||
@@ -195,6 +197,8 @@ export class HAFullCalendar extends LitElement {
|
||||
<ha-button-toggle-group
|
||||
.buttons=${viewToggleButtons}
|
||||
.active=${this._activeView}
|
||||
size="small"
|
||||
no-wrap
|
||||
@value-changed=${this._handleView}
|
||||
></ha-button-toggle-group>
|
||||
</div>
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-resizable-bottom-sheet";
|
||||
import type { HaResizableBottomSheet } from "../../../components/ha-resizable-bottom-sheet";
|
||||
import {
|
||||
@@ -39,18 +37,9 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@state() private _resizing = false;
|
||||
|
||||
@query("ha-resizable-bottom-sheet")
|
||||
private _bottomSheetElement?: HaResizableBottomSheet;
|
||||
|
||||
private _resizeStartX = 0;
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unregisterResizeHandlers();
|
||||
}
|
||||
|
||||
private _renderContent() {
|
||||
// get config type
|
||||
const type = this._getType();
|
||||
@@ -165,16 +154,7 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="handle ${this._resizing ? "resizing" : ""}"
|
||||
@mousedown=${this._handleMouseDown}
|
||||
@touchstart=${this._handleMouseDown}
|
||||
>
|
||||
${this._resizing ? html`<div class="indicator"></div>` : nothing}
|
||||
</div>
|
||||
${this._renderContent()}
|
||||
`;
|
||||
return this._renderContent();
|
||||
}
|
||||
|
||||
private _getType() {
|
||||
@@ -227,67 +207,6 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
(this.config as ActionSidebarConfig)?.toggleYamlMode();
|
||||
};
|
||||
|
||||
private _handleMouseDown = (ev: MouseEvent | TouchEvent) => {
|
||||
// Prevent the browser from interpreting this as a scroll/PTR gesture.
|
||||
ev.preventDefault();
|
||||
this._startResizing(
|
||||
(ev as TouchEvent).touches?.length
|
||||
? (ev as TouchEvent).touches[0].clientX
|
||||
: (ev as MouseEvent).clientX
|
||||
);
|
||||
};
|
||||
|
||||
private _startResizing(clientX: number) {
|
||||
// register event listeners for drag handling
|
||||
document.addEventListener("mousemove", this._handleMouseMove);
|
||||
document.addEventListener("mouseup", this._endResizing);
|
||||
document.addEventListener("touchmove", this._handleMouseMove, {
|
||||
passive: false,
|
||||
});
|
||||
document.addEventListener("touchend", this._endResizing);
|
||||
document.addEventListener("touchcancel", this._endResizing);
|
||||
|
||||
this._resizing = true;
|
||||
this._resizeStartX = clientX;
|
||||
}
|
||||
|
||||
private _handleMouseMove = (ev: MouseEvent | TouchEvent) => {
|
||||
this._updateSize(
|
||||
(ev as TouchEvent).touches?.length
|
||||
? (ev as TouchEvent).touches[0].clientX
|
||||
: (ev as MouseEvent).clientX
|
||||
);
|
||||
};
|
||||
|
||||
private _updateSize(clientX: number) {
|
||||
let delta = this._resizeStartX - clientX;
|
||||
|
||||
if (computeRTL(this.hass)) {
|
||||
delta = -delta;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
fireEvent(this, "sidebar-resized", {
|
||||
deltaInPx: delta,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _endResizing = () => {
|
||||
this._unregisterResizeHandlers();
|
||||
this._resizing = false;
|
||||
document.body.style.removeProperty("cursor");
|
||||
fireEvent(this, "sidebar-resizing-stopped");
|
||||
};
|
||||
|
||||
private _unregisterResizeHandlers() {
|
||||
document.removeEventListener("mousemove", this._handleMouseMove);
|
||||
document.removeEventListener("mouseup", this._endResizing);
|
||||
document.removeEventListener("touchmove", this._handleMouseMove);
|
||||
document.removeEventListener("touchend", this._endResizing);
|
||||
document.removeEventListener("touchcancel", this._endResizing);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
z-index: 6;
|
||||
@@ -308,28 +227,6 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
position: absolute;
|
||||
margin-inline-start: -11px;
|
||||
height: calc(100% - (2 * var(--ha-card-border-radius)));
|
||||
width: 24px;
|
||||
z-index: 7;
|
||||
cursor: ew-resize;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--ha-card-border-radius) 0;
|
||||
}
|
||||
.handle.resizing {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.handle .indicator {
|
||||
background-color: var(--primary-color);
|
||||
height: 100%;
|
||||
width: 4px;
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -343,9 +240,5 @@ declare global {
|
||||
"yaml-changed": {
|
||||
value: unknown;
|
||||
};
|
||||
"sidebar-resized": {
|
||||
deltaInPx: number;
|
||||
};
|
||||
"sidebar-resizing-stopped": undefined;
|
||||
}
|
||||
}
|
||||
|
@@ -22,7 +22,6 @@ import {
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
@@ -102,13 +101,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
@state() private _sidebarKey?: string;
|
||||
|
||||
@storage({
|
||||
key: "automation-sidebar-width-percentage",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _sidebarWidth = 40;
|
||||
|
||||
@query("ha-automation-sidebar") private _sidebarElement?: HaAutomationSidebar;
|
||||
|
||||
@queryAll("ha-automation-action, ha-automation-condition")
|
||||
@@ -118,17 +110,13 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
private _previousConfig?: ManualAutomationConfig;
|
||||
|
||||
private _prevSidebarWidthPx?: number;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("paste", this._handlePaste);
|
||||
window.addEventListener("resize", this._resizeSidebarWidth);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
window.removeEventListener("paste", this._handlePaste);
|
||||
window.removeEventListener("resize", this._resizeSidebarWidth);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
@@ -315,11 +303,9 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.config=${this._sidebarConfig}
|
||||
@value-changed=${this._sidebarConfigChanged}
|
||||
.disabled=${this.disabled}
|
||||
.sidebarKey=${this._sidebarKey}
|
||||
@value-changed=${this._sidebarConfigChanged}
|
||||
@sidebar-resized=${this._resizeSidebar}
|
||||
@sidebar-resizing-stopped=${this._stopResizeSidebar}
|
||||
></ha-automation-sidebar>
|
||||
</div>
|
||||
</div>
|
||||
@@ -328,12 +314,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this.style.setProperty(
|
||||
"--sidebar-dynamic-width",
|
||||
`${this._widthPxToVw(this._widthPercentageToPx(this._sidebarWidth))}vw`
|
||||
);
|
||||
|
||||
const expanded = extractSearchParam("expanded");
|
||||
if (expanded === "1") {
|
||||
this._clearParam("expanded");
|
||||
@@ -662,47 +642,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _resizeSidebarWidth = () => {
|
||||
this.style.setProperty(
|
||||
"--sidebar-dynamic-width",
|
||||
`${this._widthPxToVw(this._widthPercentageToPx(this._sidebarWidth))}vw`
|
||||
);
|
||||
};
|
||||
|
||||
private _widthPxToVw(px: number) {
|
||||
return (px / window.innerWidth) * 100;
|
||||
}
|
||||
|
||||
private _widthPercentageToPx(percentage: number) {
|
||||
return (percentage / 100) * this.clientWidth;
|
||||
}
|
||||
|
||||
private _resizeSidebar(ev) {
|
||||
ev.stopPropagation();
|
||||
const delta = ev.detail.deltaInPx as number;
|
||||
|
||||
// set initial resize width to add / reduce delta from it
|
||||
if (!this._prevSidebarWidthPx) {
|
||||
this._prevSidebarWidthPx = (this._sidebarWidth / 100) * this.clientWidth;
|
||||
}
|
||||
|
||||
const widthPx = delta + this._prevSidebarWidthPx;
|
||||
|
||||
if (widthPx > this.clientWidth * 0.7 || widthPx < this.clientWidth * 0.3) {
|
||||
return;
|
||||
}
|
||||
|
||||
const widthVw = this._widthPxToVw(widthPx);
|
||||
|
||||
this.style.setProperty("--sidebar-dynamic-width", `${widthVw}vw`);
|
||||
this._sidebarWidth = (widthPx / this.clientWidth) * 100;
|
||||
}
|
||||
|
||||
private _stopResizeSidebar(ev) {
|
||||
ev.stopPropagation();
|
||||
this._prevSidebarWidthPx = undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
saveFabStyles,
|
||||
|
@@ -109,7 +109,7 @@ export const manualEditorStyles = css`
|
||||
}
|
||||
|
||||
.has-sidebar {
|
||||
--sidebar-width: max(var(--sidebar-dynamic-width), 375px);
|
||||
--sidebar-width: min(35vw, 500px);
|
||||
--sidebar-gap: 16px;
|
||||
}
|
||||
|
||||
|
@@ -172,7 +172,9 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
element.hass = this.hass;
|
||||
const stateObj = this.hass.states[entry.entity_id];
|
||||
|
||||
let name = computeEntityName(stateObj, this.hass) || this.deviceName;
|
||||
let name =
|
||||
computeEntityName(stateObj, this.hass.entities, this.hass.devices) ||
|
||||
this.deviceName;
|
||||
|
||||
if (entry.hidden_by) {
|
||||
name += ` (${this.hass.localize(
|
||||
|
@@ -1238,7 +1238,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
private _computeEntityName(entity: EntityRegistryEntry) {
|
||||
const device = this.hass.devices[this.deviceId];
|
||||
return (
|
||||
computeEntityEntryName(entity, this.hass) ||
|
||||
computeEntityEntryName(entity, this.hass.devices) ||
|
||||
computeDeviceNameDisplay(device, this.hass)
|
||||
);
|
||||
}
|
||||
|
@@ -682,7 +682,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
|
||||
const entityName = computeEntityEntryName(
|
||||
entry as EntityRegistryEntry,
|
||||
this.hass
|
||||
this.hass.devices,
|
||||
entity
|
||||
);
|
||||
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
|
@@ -18,6 +18,7 @@ import type { HomeAssistant, Route } from "../../../types";
|
||||
import "./error-log-card";
|
||||
import "./system-log-card";
|
||||
import type { SystemLogCard } from "./system-log-card";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
|
||||
const logProviders: LogProvider[] = [
|
||||
{
|
||||
@@ -208,15 +209,17 @@ export class HaConfigLogs extends LitElement {
|
||||
private async _getInstalledAddons() {
|
||||
try {
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
this._logProviders = [
|
||||
...this._logProviders,
|
||||
...addonsInfo.addons
|
||||
.filter((addon) => addon.version)
|
||||
.map((addon) => ({
|
||||
key: addon.slug,
|
||||
name: addon.name,
|
||||
})),
|
||||
];
|
||||
const sortedAddons = addonsInfo.addons
|
||||
.filter((addon) => addon.version)
|
||||
.map((addon) => ({
|
||||
key: addon.slug,
|
||||
name: addon.name,
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
|
||||
this._logProviders = [...this._logProviders, ...sortedAddons];
|
||||
} catch (_err) {
|
||||
// Ignore, nothing the user can do anyway
|
||||
}
|
||||
|
@@ -21,7 +21,6 @@ import {
|
||||
string,
|
||||
} from "superstruct";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
@@ -85,13 +84,6 @@ export class HaManualScriptEditor extends LitElement {
|
||||
|
||||
@state() private _sidebarKey?: string;
|
||||
|
||||
@storage({
|
||||
key: "automation-sidebar-width-percentage",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _sidebarWidth = 40;
|
||||
|
||||
@query("ha-script-fields")
|
||||
private _scriptFields?: HaScriptFields;
|
||||
|
||||
@@ -106,8 +98,6 @@ export class HaManualScriptEditor extends LitElement {
|
||||
|
||||
private _openFields = false;
|
||||
|
||||
private _prevSidebarWidthPx?: number;
|
||||
|
||||
public addFields() {
|
||||
this._openFields = true;
|
||||
fireEvent(this, "value-changed", {
|
||||
@@ -262,10 +252,8 @@ export class HaManualScriptEditor extends LitElement {
|
||||
.isWide=${this.isWide}
|
||||
.hass=${this.hass}
|
||||
.config=${this._sidebarConfig}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._sidebarConfigChanged}
|
||||
@sidebar-resized=${this._resizeSidebar}
|
||||
@sidebar-resizing-stopped=${this._stopResizeSidebar}
|
||||
.disabled=${this.disabled}
|
||||
></ha-automation-sidebar>
|
||||
</div>
|
||||
</div>
|
||||
@@ -274,12 +262,6 @@ export class HaManualScriptEditor extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this.style.setProperty(
|
||||
"--sidebar-dynamic-width",
|
||||
`${this._widthPxToVw(this._widthPercentageToPx(this._sidebarWidth))}vw`
|
||||
);
|
||||
|
||||
const expanded = extractSearchParam("expanded");
|
||||
if (expanded === "1") {
|
||||
this._clearParam("expanded");
|
||||
@@ -314,12 +296,10 @@ export class HaManualScriptEditor extends LitElement {
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("paste", this._handlePaste);
|
||||
window.addEventListener("resize", this._resizeSidebarWidth);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
window.removeEventListener("paste", this._handlePaste);
|
||||
window.removeEventListener("resize", this._resizeSidebarWidth);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
@@ -577,47 +557,6 @@ export class HaManualScriptEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _resizeSidebarWidth = () => {
|
||||
this.style.setProperty(
|
||||
"--sidebar-dynamic-width",
|
||||
`${this._widthPxToVw(this._widthPercentageToPx(this._sidebarWidth))}vw`
|
||||
);
|
||||
};
|
||||
|
||||
private _widthPxToVw(px: number) {
|
||||
return (px / window.innerWidth) * 100;
|
||||
}
|
||||
|
||||
private _widthPercentageToPx(percentage: number) {
|
||||
return (percentage / 100) * this.clientWidth;
|
||||
}
|
||||
|
||||
private _resizeSidebar(ev) {
|
||||
ev.stopPropagation();
|
||||
const delta = ev.detail.deltaInPx as number;
|
||||
|
||||
// set initial resize width to add / reduce delta from it
|
||||
if (!this._prevSidebarWidthPx) {
|
||||
this._prevSidebarWidthPx = (this._sidebarWidth / 100) * this.clientWidth;
|
||||
}
|
||||
|
||||
const widthPx = delta + this._prevSidebarWidthPx;
|
||||
|
||||
if (widthPx > this.clientWidth * 0.7 || widthPx < this.clientWidth * 0.3) {
|
||||
return;
|
||||
}
|
||||
|
||||
const widthVw = this._widthPxToVw(widthPx);
|
||||
|
||||
this.style.setProperty("--sidebar-dynamic-width", `${widthVw}vw`);
|
||||
this._sidebarWidth = (widthPx / this.clientWidth) * 100;
|
||||
}
|
||||
|
||||
private _stopResizeSidebar(ev) {
|
||||
ev.stopPropagation();
|
||||
this._prevSidebarWidthPx = undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
saveFabStyles,
|
||||
|
@@ -419,7 +419,13 @@ class HuiEnergySankeyCard
|
||||
};
|
||||
deviceNodes.forEach((deviceNode) => {
|
||||
const entity = this.hass.states[deviceNode.id];
|
||||
const { area, floor } = getEntityContext(entity, this.hass);
|
||||
const { area, floor } = getEntityContext(
|
||||
entity,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
if (area) {
|
||||
if (area.area_id in areas) {
|
||||
areas[area.area_id].value += deviceNode.value;
|
||||
@@ -457,6 +463,9 @@ class HuiEnergySankeyCard
|
||||
return { areas, floors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Organizes device nodes into hierarchical sections based on parent-child relationships.
|
||||
*/
|
||||
protected _getDeviceSections(
|
||||
parentLinks: Record<string, string>,
|
||||
deviceNodes: Node[]
|
||||
@@ -465,20 +474,34 @@ class HuiEnergySankeyCard
|
||||
const childSection: Node[] = [];
|
||||
const parentIds = Object.values(parentLinks);
|
||||
const remainingLinks: typeof parentLinks = {};
|
||||
|
||||
deviceNodes.forEach((deviceNode) => {
|
||||
if (parentIds.includes(deviceNode.id)) {
|
||||
const isChild = deviceNode.id in parentLinks;
|
||||
const isParent = parentIds.includes(deviceNode.id);
|
||||
if (isParent && !isChild) {
|
||||
// Top-level parents (have children but no parents themselves)
|
||||
parentSection.push(deviceNode);
|
||||
remainingLinks[deviceNode.id] = parentLinks[deviceNode.id];
|
||||
} else {
|
||||
childSection.push(deviceNode);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter out links where parent is already in current parent section
|
||||
Object.entries(parentLinks).forEach(([child, parent]) => {
|
||||
if (!parentSection.some((node) => node.id === parent)) {
|
||||
remainingLinks[child] = parent;
|
||||
}
|
||||
});
|
||||
|
||||
if (parentSection.length > 0) {
|
||||
// Recursively process child section with remaining links
|
||||
return [
|
||||
...this._getDeviceSections(remainingLinks, parentSection),
|
||||
childSection,
|
||||
parentSection,
|
||||
...this._getDeviceSections(remainingLinks, childSection),
|
||||
];
|
||||
}
|
||||
|
||||
// Base case: no more parent-child relationships to process
|
||||
return [deviceNodes];
|
||||
}
|
||||
|
||||
|
@@ -5,11 +5,7 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeEntityName } from "../../../../common/entity/compute_entity_name";
|
||||
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { computeRTL } from "../../../../common/util/compute_rtl";
|
||||
import "../../../../components/data-table/ha-data-table";
|
||||
@@ -66,11 +62,9 @@ export class HuiEntityPickerTable extends LitElement {
|
||||
(entity) => {
|
||||
const stateObj = this.hass.states[entity];
|
||||
|
||||
const { area, device } = getEntityContext(stateObj, this.hass);
|
||||
|
||||
const entityName = computeEntityName(stateObj, this.hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
const entityName = this.hass.formatEntityName(stateObj, "entity");
|
||||
const deviceName = this.hass.formatEntityName(stateObj, "device");
|
||||
const areaName = this.hass.formatEntityName(stateObj, "area");
|
||||
const name = [deviceName, entityName].filter(Boolean).join(" ");
|
||||
const domain = computeDomain(entity);
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import { computeEntityName } from "../../../../common/entity/compute_entity_name";
|
||||
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
|
||||
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
|
||||
import { clamp } from "../../../../common/number/clamp";
|
||||
@@ -177,7 +176,13 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
for (const entityId of otherEntities) {
|
||||
const stateObj = hass.states[entityId];
|
||||
if (!stateObj) continue;
|
||||
const { device } = getEntityContext(stateObj, hass);
|
||||
const { device } = getEntityContext(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
if (!device) {
|
||||
unassignedEntities.push(entityId);
|
||||
continue;
|
||||
@@ -268,8 +273,8 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
return {
|
||||
...computeTileCard(e),
|
||||
name:
|
||||
computeEntityName(stateObj, hass) ||
|
||||
(device ? computeDeviceName(device) : ""),
|
||||
hass.formatEntityName(stateObj, "entity") ||
|
||||
hass.formatEntityName(stateObj, "device"),
|
||||
};
|
||||
}),
|
||||
],
|
||||
|
@@ -33,6 +33,7 @@ import { fetchWithAuth } from "../util/fetch-with-auth";
|
||||
import { getState } from "../util/ha-pref-storage";
|
||||
import hassCallApi, { hassCallApiRaw } from "../util/hass-call-api";
|
||||
import type { HassBaseEl } from "./hass-base-mixin";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
|
||||
export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
superClass: T
|
||||
@@ -210,6 +211,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
value != null ? value : (stateObj.attributes[attribute] ?? ""),
|
||||
...getState(),
|
||||
...this._pendingHass,
|
||||
formatEntityName: (stateObj) => computeStateName(stateObj),
|
||||
};
|
||||
|
||||
this.hassConnected();
|
||||
|
@@ -8,7 +8,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) => {
|
||||
class StateDisplayMixin extends superClass {
|
||||
protected hassConnected() {
|
||||
super.hassConnected();
|
||||
this._updateStateDisplay();
|
||||
this._updateFormatFunctions();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps) {
|
||||
@@ -25,13 +25,16 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) => {
|
||||
this.hass.localize !== oldHass.localize ||
|
||||
this.hass.locale !== oldHass.locale ||
|
||||
this.hass.config !== oldHass.config ||
|
||||
this.hass.entities !== oldHass.entities)
|
||||
this.hass.entities !== oldHass.entities ||
|
||||
this.hass.devices !== oldHass.devices ||
|
||||
this.hass.areas !== oldHass.areas ||
|
||||
this.hass.floors !== oldHass.floors)
|
||||
) {
|
||||
this._updateStateDisplay();
|
||||
this._updateFormatFunctions();
|
||||
}
|
||||
}
|
||||
|
||||
private _updateStateDisplay = async () => {
|
||||
private _updateFormatFunctions = async () => {
|
||||
if (!this.hass || !this.hass.config) {
|
||||
return;
|
||||
}
|
||||
@@ -52,17 +55,22 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) => {
|
||||
formatEntityState,
|
||||
formatEntityAttributeName,
|
||||
formatEntityAttributeValue,
|
||||
formatEntityName,
|
||||
} = await computeFormatFunctions(
|
||||
this.hass.localize,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors,
|
||||
sensorNumericDeviceClasses
|
||||
);
|
||||
this._updateHass({
|
||||
formatEntityState,
|
||||
formatEntityAttributeName,
|
||||
formatEntityAttributeValue,
|
||||
formatEntityName,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import type {
|
||||
HassServiceTarget,
|
||||
MessageBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { EntityNameType } from "./common/translations/entity-state";
|
||||
import type { LocalizeFunc } from "./common/translations/localize";
|
||||
import type { AreaRegistryEntry } from "./data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "./data/device_registry";
|
||||
@@ -285,6 +286,11 @@ export interface HomeAssistant {
|
||||
value?: any
|
||||
): string;
|
||||
formatEntityAttributeName(stateObj: HassEntity, attribute: string): string;
|
||||
formatEntityName(
|
||||
stateObj: HassEntity,
|
||||
type: EntityNameType | EntityNameType[],
|
||||
separator?: string
|
||||
): string;
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
|
@@ -6,49 +6,53 @@ import {
|
||||
} from "../../../src/common/entity/compute_entity_name";
|
||||
import * as computeStateNameModule from "../../../src/common/entity/compute_state_name";
|
||||
import * as stripPrefixModule from "../../../src/common/entity/strip_prefix_from_entity_name";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import {
|
||||
mockEntity,
|
||||
mockEntityEntry,
|
||||
mockStateObj,
|
||||
} from "./context/context-mock";
|
||||
|
||||
describe("computeEntityName", () => {
|
||||
it("returns state name if entity not in registry", () => {
|
||||
vi.spyOn(computeStateNameModule, "computeStateName").mockReturnValue(
|
||||
"Kitchen Light"
|
||||
);
|
||||
const stateObj = {
|
||||
const stateObj = mockStateObj({
|
||||
entity_id: "light.kitchen",
|
||||
attributes: { friendly_name: "Kitchen Light" },
|
||||
state: "on",
|
||||
};
|
||||
});
|
||||
const hass = {
|
||||
entities: {},
|
||||
devices: {},
|
||||
states: {
|
||||
"light.kitchen": stateObj,
|
||||
},
|
||||
};
|
||||
expect(computeEntityName(stateObj as any, hass as any)).toBe(
|
||||
} as unknown as HomeAssistant;
|
||||
expect(computeEntityName(stateObj, hass.entities, hass.devices)).toBe(
|
||||
"Kitchen Light"
|
||||
);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("returns entity entry name if present", () => {
|
||||
const stateObj = {
|
||||
const stateObj = mockStateObj({
|
||||
entity_id: "light.kitchen",
|
||||
attributes: {},
|
||||
state: "on",
|
||||
};
|
||||
});
|
||||
const hass = {
|
||||
entities: {
|
||||
"light.kitchen": {
|
||||
entity_id: "light.kitchen",
|
||||
name: "Ceiling Light",
|
||||
labels: [],
|
||||
},
|
||||
},
|
||||
devices: {},
|
||||
states: {
|
||||
"light.kitchen": stateObj,
|
||||
},
|
||||
};
|
||||
expect(computeEntityName(stateObj as any, hass as any)).toBe(
|
||||
} as unknown as HomeAssistant;
|
||||
expect(computeEntityName(stateObj, hass.entities, hass.devices)).toBe(
|
||||
"Ceiling Light"
|
||||
);
|
||||
});
|
||||
@@ -56,11 +60,12 @@ describe("computeEntityName", () => {
|
||||
|
||||
describe("computeEntityEntryName", () => {
|
||||
it("returns entry.name if no device", () => {
|
||||
const entry = { entity_id: "light.kitchen", name: "Ceiling Light" };
|
||||
const entry = mockEntity({
|
||||
entity_id: "light.kitchen",
|
||||
name: "Ceiling Light",
|
||||
});
|
||||
const hass = { devices: {}, states: {} };
|
||||
expect(computeEntityEntryName(entry as any, hass as any)).toBe(
|
||||
"Ceiling Light"
|
||||
);
|
||||
expect(computeEntityEntryName(entry, hass.devices)).toBe("Ceiling Light");
|
||||
});
|
||||
|
||||
it("returns device-stripped name if device present", () => {
|
||||
@@ -70,16 +75,16 @@ describe("computeEntityEntryName", () => {
|
||||
vi.spyOn(stripPrefixModule, "stripPrefixFromEntityName").mockImplementation(
|
||||
(name, prefix) => name.replace(prefix + " ", "")
|
||||
);
|
||||
const entry = {
|
||||
const entry = mockEntity({
|
||||
entity_id: "light.kitchen",
|
||||
name: "Kitchen Light",
|
||||
device_id: "dev1",
|
||||
};
|
||||
});
|
||||
const hass = {
|
||||
devices: { dev1: {} },
|
||||
states: {},
|
||||
};
|
||||
expect(computeEntityEntryName(entry as any, hass as any)).toBe("Light");
|
||||
} as unknown as HomeAssistant;
|
||||
expect(computeEntityEntryName(entry, hass.devices)).toBe("Light");
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -87,16 +92,16 @@ describe("computeEntityEntryName", () => {
|
||||
vi.spyOn(computeDeviceNameModule, "computeDeviceName").mockReturnValue(
|
||||
"Kitchen Light"
|
||||
);
|
||||
const entry = {
|
||||
const entry = mockEntity({
|
||||
entity_id: "light.kitchen",
|
||||
name: "Kitchen Light",
|
||||
device_id: "dev1",
|
||||
};
|
||||
});
|
||||
const hass = {
|
||||
devices: { dev1: {} },
|
||||
states: {},
|
||||
};
|
||||
expect(computeEntityEntryName(entry as any, hass as any)).toBeUndefined();
|
||||
} as unknown as HomeAssistant;
|
||||
expect(computeEntityEntryName(entry, hass.devices)).toBeUndefined();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -104,35 +109,36 @@ describe("computeEntityEntryName", () => {
|
||||
vi.spyOn(computeStateNameModule, "computeStateName").mockReturnValue(
|
||||
"Fallback Name"
|
||||
);
|
||||
const entry = { entity_id: "light.kitchen" };
|
||||
const entry = mockEntity({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
devices: {},
|
||||
states: {
|
||||
"light.kitchen": { entity_id: "light.kitchen" },
|
||||
},
|
||||
};
|
||||
expect(computeEntityEntryName(entry as any, hass as any)).toBe(
|
||||
} as unknown as HomeAssistant;
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
expect(computeEntityEntryName(entry, hass.devices, stateObj)).toBe(
|
||||
"Fallback Name"
|
||||
);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("returns original_name if present", () => {
|
||||
const entry = { entity_id: "light.kitchen", original_name: "Old Name" };
|
||||
const entry = mockEntityEntry({
|
||||
entity_id: "light.kitchen",
|
||||
original_name: "Old Name",
|
||||
});
|
||||
const hass = {
|
||||
devices: {},
|
||||
states: {},
|
||||
};
|
||||
expect(computeEntityEntryName(entry as any, hass as any)).toBe("Old Name");
|
||||
} as unknown as HomeAssistant;
|
||||
expect(computeEntityEntryName(entry, hass.devices)).toBe("Old Name");
|
||||
});
|
||||
|
||||
it("returns undefined if no name, original_name, or device", () => {
|
||||
const entry = { entity_id: "light.kitchen" };
|
||||
const entry = mockEntity({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
devices: {},
|
||||
states: {},
|
||||
};
|
||||
expect(computeEntityEntryName(entry as any, hass as any)).toBeUndefined();
|
||||
} as unknown as HomeAssistant;
|
||||
expect(computeEntityEntryName(entry, hass.devices)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("handles entities with numeric original_name (real bug from issue #25363)", () => {
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { EntityRegistryDisplayEntry } from "../../../../src/data/entity_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "../../../../src/data/entity_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
|
||||
export const mockStateObj = (partial: Partial<HassEntity>): HassEntity => ({
|
||||
@@ -26,6 +29,31 @@ export const mockEntity = (
|
||||
...partial,
|
||||
});
|
||||
|
||||
export const mockEntityEntry = (
|
||||
partial: Partial<EntityRegistryEntry>
|
||||
): EntityRegistryEntry => ({
|
||||
entity_id: "",
|
||||
name: null,
|
||||
icon: null,
|
||||
platform: "",
|
||||
config_entry_id: null,
|
||||
config_subentry_id: null,
|
||||
device_id: null,
|
||||
area_id: null,
|
||||
labels: [],
|
||||
disabled_by: null,
|
||||
hidden_by: null,
|
||||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
unique_id: "",
|
||||
id: "",
|
||||
options: null,
|
||||
categories: {},
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
...partial,
|
||||
});
|
||||
|
||||
export const mockDevice = (
|
||||
partial: Partial<DeviceRegistryEntry>
|
||||
): DeviceRegistryEntry => ({
|
||||
|
@@ -26,7 +26,13 @@ describe("getEntityContext", () => {
|
||||
floors: {},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = getEntityContext(stateObj, hass);
|
||||
const result = getEntityContext(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
entity,
|
||||
@@ -71,7 +77,13 @@ describe("getEntityContext", () => {
|
||||
},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = getEntityContext(stateObj, hass);
|
||||
const result = getEntityContext(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
entity,
|
||||
@@ -105,7 +117,13 @@ describe("getEntityContext", () => {
|
||||
},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = getEntityContext(stateObj, hass);
|
||||
const result = getEntityContext(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
entity,
|
||||
@@ -138,7 +156,13 @@ describe("getEntityContext", () => {
|
||||
floors: {},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = getEntityContext(stateObj, hass);
|
||||
const result = getEntityContext(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
entity,
|
||||
|
Reference in New Issue
Block a user