mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-05 20:54:15 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b25a220df | ||
|
|
74c44e7c71 | ||
|
|
7b6561491a | ||
|
|
17e0352ecf | ||
|
|
7b379d587c | ||
|
|
6b32c87702 |
+1
-1
@@ -107,7 +107,7 @@
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.5.0",
|
||||
"hls.js": "1.7.3",
|
||||
"home-assistant-js-websocket": "9.7.0",
|
||||
"home-assistant-js-websocket": "9.7.1",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "12.1.2",
|
||||
"js-yaml": "5.4.2",
|
||||
|
||||
Generated
+5
-5
@@ -384,8 +384,8 @@ importers:
|
||||
specifier: 1.7.3
|
||||
version: 1.7.3
|
||||
home-assistant-js-websocket:
|
||||
specifier: 9.7.0
|
||||
version: 9.7.0
|
||||
specifier: 9.7.1
|
||||
version: 9.7.1
|
||||
idb-keyval:
|
||||
specifier: 6.3.0
|
||||
version: 6.3.0
|
||||
@@ -5267,8 +5267,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-MsPlx6yVW4Qv4C7mEVou4/gk/5cN2dVxOTnrNjPSmyH2f0Ln+/UkG10Ax1PE6OvrmTVPKgQYozTtBaPOuCzq7A==}
|
||||
|
||||
[email protected].0:
|
||||
resolution: {integrity: sha512-AE/VtwY+sKi7B3934KuIgvj2g06QY9aFVdd2zh1r5ygbpMo2TZbvN9Zoma8L2wHNYFsngTRiZxYOnLlCysIOzA==}
|
||||
[email protected].1:
|
||||
resolution: {integrity: sha512-zOAiyF+gUc/IsiswYdWKns6SCBbvWb8iJtMX9+Dk0wlbdfcp+I38T2Vx36krWeWMk/GPwoSiYwCs5hk/+Hsn7w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
|
||||
@@ -13073,7 +13073,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected].0: {}
|
||||
[email protected].1: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
|
||||
@@ -3,6 +3,9 @@ autoInstallPeers: false
|
||||
|
||||
# Supply-chain protection
|
||||
minimumReleaseAge: 4320 # 3 days, in minutes
|
||||
minimumReleaseAgeExclude:
|
||||
# Our own packages, published from our own CI
|
||||
- home-assistant-js-websocket
|
||||
trustPolicy: no-downgrade
|
||||
trustPolicyExclude:
|
||||
# Backports published after provenance-signed 7.x releases
|
||||
|
||||
@@ -6,7 +6,10 @@ import type {
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
import { stripPrefixFromEntityName } from "./strip_prefix_from_entity_name";
|
||||
import {
|
||||
isDeviceName,
|
||||
stripPrefixFromEntityName,
|
||||
} from "./strip_prefix_from_entity_name";
|
||||
|
||||
export const computeEntityName = (
|
||||
stateObj: HassEntity,
|
||||
@@ -48,8 +51,14 @@ export const computeEntityEntryName = (
|
||||
|
||||
const deviceName = computeDeviceName(device);
|
||||
|
||||
// An empty entity name or one equal to the device name means the entity uses the device name
|
||||
if (!name || deviceName === name) {
|
||||
// An original_name never contains the device name, only a user-set name needs the loose match
|
||||
const isOnlyDeviceName =
|
||||
entry.name != null && deviceName
|
||||
? isDeviceName(entry.name, deviceName)
|
||||
: deviceName === name;
|
||||
|
||||
// An empty entity name or one that is only the device name means the entity uses the device name
|
||||
if (!name || isOnlyDeviceName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,4 +30,19 @@ export const stripPrefixFromEntityName = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether an entity name is only the device name, which core treats as no name of its own.
|
||||
*/
|
||||
export const isDeviceName = (entityName: string, deviceName: string) => {
|
||||
if (!deviceName) {
|
||||
return false;
|
||||
}
|
||||
const lowerCasedEntityName = entityName.toLowerCase();
|
||||
const lowerCasedDeviceName = deviceName.toLowerCase();
|
||||
return (
|
||||
lowerCasedEntityName.startsWith(lowerCasedDeviceName) &&
|
||||
/^[ :-]*$/.test(lowerCasedEntityName.slice(lowerCasedDeviceName.length))
|
||||
);
|
||||
};
|
||||
|
||||
const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str;
|
||||
|
||||
@@ -13,16 +13,17 @@ import { DecoratedMarker } from "../decorated_marker";
|
||||
import { isTouch } from "../../../util/is_touch";
|
||||
import type {
|
||||
MapClusterOptions,
|
||||
MapCircleHandle,
|
||||
MapCircleOptions,
|
||||
MapControlPosition,
|
||||
MapEngine,
|
||||
MapEngineOptions,
|
||||
MapFitOptions,
|
||||
MapItemHandle,
|
||||
MapLatLng,
|
||||
MapMarkerHandle,
|
||||
MapMarkerOptions,
|
||||
MapPath,
|
||||
MapPathHandle,
|
||||
} from "../map-engine";
|
||||
import type { ResolvedMapStyle } from "../map-styles";
|
||||
import { setMarkerAccessibility } from "../marker-accessibility";
|
||||
@@ -264,16 +265,35 @@ export class LeafletMapEngine implements MapEngine {
|
||||
public addCircle(
|
||||
center: MapLatLng,
|
||||
options: MapCircleOptions
|
||||
): MapItemHandle {
|
||||
): MapCircleHandle {
|
||||
const circle = this.Leaflet!.circle(center, {
|
||||
interactive: false,
|
||||
color: options.color,
|
||||
radius: options.radius,
|
||||
}).addTo(this.leafletMap!);
|
||||
return { remove: () => circle.remove() };
|
||||
return {
|
||||
update: (newCenter, newOptions) => {
|
||||
circle
|
||||
.setLatLng(newCenter)
|
||||
.setRadius(newOptions.radius)
|
||||
.setStyle({ color: newOptions.color });
|
||||
},
|
||||
remove: () => circle.remove(),
|
||||
};
|
||||
}
|
||||
|
||||
public addPath(path: MapPath): MapItemHandle {
|
||||
public addPath(path: MapPath): MapPathHandle {
|
||||
let items = this._drawPath(path);
|
||||
return {
|
||||
update: (next) => {
|
||||
items.forEach((item) => item.remove());
|
||||
items = this._drawPath(next);
|
||||
},
|
||||
remove: () => items.forEach((item) => item.remove()),
|
||||
};
|
||||
}
|
||||
|
||||
private _drawPath(path: MapPath): (Polyline | CircleMarker)[] {
|
||||
const items: (Polyline | CircleMarker)[] = [];
|
||||
for (const segment of path.segments) {
|
||||
items.push(
|
||||
@@ -296,7 +316,7 @@ export class LeafletMapEngine implements MapEngine {
|
||||
);
|
||||
}
|
||||
items.forEach((item) => item.addTo(this.leafletMap!));
|
||||
return { remove: () => items.forEach((item) => item.remove()) };
|
||||
return items;
|
||||
}
|
||||
|
||||
public setClustering(options: MapClusterOptions | null): void {
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { deepEqual } from "../../util/deep-equal";
|
||||
import { isTouch } from "../../../util/is_touch";
|
||||
import type {
|
||||
MapCircleHandle,
|
||||
MapCircleOptions,
|
||||
MapClusterIcon,
|
||||
MapClusterOptions,
|
||||
@@ -49,6 +50,7 @@ import type {
|
||||
MapLatLng,
|
||||
MapMarkerOptions,
|
||||
MapPath,
|
||||
MapPathHandle,
|
||||
} from "../map-engine";
|
||||
import { destinationPoint, distanceMeters, pointEastOf } from "../map-engine";
|
||||
import type { ResolvedMapStyle } from "../map-styles";
|
||||
@@ -121,6 +123,42 @@ const circlePolygon = (
|
||||
};
|
||||
};
|
||||
|
||||
const coloredCircle = (
|
||||
center: MapLatLng,
|
||||
options: MapCircleOptions
|
||||
): Feature<Polygon> => ({
|
||||
...circlePolygon(center, options.radius),
|
||||
properties: { color: options.color },
|
||||
});
|
||||
|
||||
const pathLines = (path: MapPath): FeatureCollection => ({
|
||||
type: "FeatureCollection",
|
||||
features: path.segments.map((segment) => ({
|
||||
type: "Feature",
|
||||
properties: { color: path.color, opacity: segment.opacity ?? 1 },
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: segment.points.map((point) => [point[1], point[0]]),
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
const pathPoints = (path: MapPath): FeatureCollection => ({
|
||||
type: "FeatureCollection",
|
||||
features: path.markers.map((pathMarker) => ({
|
||||
type: "Feature",
|
||||
properties: {
|
||||
color: path.color,
|
||||
opacity: pathMarker.opacity ?? 1,
|
||||
tooltip: pathMarker.tooltipHtml,
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [pathMarker.location[1], pathMarker.location[0]],
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
interface ManagedMarker {
|
||||
element: HTMLElement;
|
||||
location: MapLatLng;
|
||||
@@ -777,25 +815,28 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
public addCircle(
|
||||
center: MapLatLng,
|
||||
options: MapCircleOptions
|
||||
): MapItemHandle {
|
||||
): MapCircleHandle {
|
||||
if (!this._map) {
|
||||
return { remove: () => undefined };
|
||||
return { update: () => undefined, remove: () => undefined };
|
||||
}
|
||||
const id = `${CUSTOM_PREFIX}circle-${this._idCounter++}`;
|
||||
this._addCustomSource(id, circlePolygon(center, options.radius));
|
||||
this._addCustomSource(id, coloredCircle(center, options));
|
||||
this._addCustomLayer({
|
||||
id: `${id}-fill`,
|
||||
type: "fill",
|
||||
source: id,
|
||||
paint: { "fill-color": options.color, "fill-opacity": 0.2 },
|
||||
paint: { "fill-color": ["get", "color"], "fill-opacity": 0.2 },
|
||||
});
|
||||
this._addCustomLayer({
|
||||
id: `${id}-line`,
|
||||
type: "line",
|
||||
source: id,
|
||||
paint: { "line-color": options.color, "line-width": 3 },
|
||||
paint: { "line-color": ["get", "color"], "line-width": 3 },
|
||||
});
|
||||
return {
|
||||
update: (newCenter, newOptions) => {
|
||||
this._setCustomSourceData(id, coloredCircle(newCenter, newOptions));
|
||||
},
|
||||
remove: () => {
|
||||
this._removeCustomLayer(`${id}-fill`);
|
||||
this._removeCustomLayer(`${id}-line`);
|
||||
@@ -1043,46 +1084,20 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
};
|
||||
}
|
||||
|
||||
public addPath(path: MapPath): MapItemHandle {
|
||||
public addPath(path: MapPath): MapPathHandle {
|
||||
if (!this._map || !this._maplibre) {
|
||||
return { remove: () => undefined };
|
||||
return { update: () => undefined, remove: () => undefined };
|
||||
}
|
||||
const id = `${CUSTOM_PREFIX}path-${this._idCounter++}`;
|
||||
|
||||
const lines: FeatureCollection = {
|
||||
type: "FeatureCollection",
|
||||
features: path.segments.map((segment) => ({
|
||||
type: "Feature",
|
||||
properties: { opacity: segment.opacity ?? 1 },
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: segment.points.map((point) => [point[1], point[0]]),
|
||||
},
|
||||
})),
|
||||
};
|
||||
const points: FeatureCollection = {
|
||||
type: "FeatureCollection",
|
||||
features: path.markers.map((pathMarker) => ({
|
||||
type: "Feature",
|
||||
properties: {
|
||||
opacity: pathMarker.opacity ?? 1,
|
||||
tooltip: pathMarker.tooltipHtml,
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [pathMarker.location[1], pathMarker.location[0]],
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
this._addCustomSource(`${id}-lines`, lines);
|
||||
this._addCustomSource(`${id}-points`, points);
|
||||
this._addCustomSource(`${id}-lines`, pathLines(path));
|
||||
this._addCustomSource(`${id}-points`, pathPoints(path));
|
||||
this._addCustomLayer({
|
||||
id: `${id}-lines`,
|
||||
type: "line",
|
||||
source: `${id}-lines`,
|
||||
paint: {
|
||||
"line-color": path.color,
|
||||
"line-color": ["get", "color"],
|
||||
"line-width": 3,
|
||||
"line-opacity": ["get", "opacity"],
|
||||
},
|
||||
@@ -1093,10 +1108,10 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
source: `${id}-points`,
|
||||
paint: {
|
||||
"circle-radius": isTouch ? 8 : 3,
|
||||
"circle-color": path.color,
|
||||
"circle-color": ["get", "color"],
|
||||
"circle-opacity": ["get", "opacity"],
|
||||
"circle-stroke-width": 2,
|
||||
"circle-stroke-color": path.color,
|
||||
"circle-stroke-color": ["get", "color"],
|
||||
"circle-stroke-opacity": ["get", "opacity"],
|
||||
},
|
||||
});
|
||||
@@ -1139,6 +1154,10 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
this._pathPointLayers.add(layerId);
|
||||
|
||||
return {
|
||||
update: (next) => {
|
||||
this._setCustomSourceData(`${id}-lines`, pathLines(next));
|
||||
this._setCustomSourceData(`${id}-points`, pathPoints(next));
|
||||
},
|
||||
remove: () => {
|
||||
map.off("mouseenter", layerId, onEnter);
|
||||
map.off("mouseleave", layerId, onLeave);
|
||||
|
||||
@@ -115,6 +115,16 @@ export interface MapMarkerHandle extends MapItemHandle {
|
||||
readonly clusterData?: unknown;
|
||||
}
|
||||
|
||||
export interface MapCircleHandle extends MapItemHandle {
|
||||
/** Move, resize or recolor without removing it first */
|
||||
update(center: MapLatLng, options: MapCircleOptions): void;
|
||||
}
|
||||
|
||||
export interface MapPathHandle extends MapItemHandle {
|
||||
/** Replace the drawn trail without removing it first */
|
||||
update(path: MapPath): void;
|
||||
}
|
||||
|
||||
export interface MapDraggableMarkerOptions extends MapMarkerOptions {
|
||||
onDragEnd?(location: MapLatLng): void;
|
||||
}
|
||||
@@ -239,13 +249,13 @@ export interface MapEngine {
|
||||
): MapMarkerHandle;
|
||||
|
||||
/** Draw a meter-radius circle (zone radius) */
|
||||
addCircle(center: MapLatLng, options: MapCircleOptions): MapItemHandle;
|
||||
addCircle(center: MapLatLng, options: MapCircleOptions): MapCircleHandle;
|
||||
|
||||
/** Editing support, MapLibre only; undefined on the Leaflet fallback */
|
||||
editing?: MapEditingSupport;
|
||||
|
||||
/** Draw one history trail (points with tooltips, connecting segments) */
|
||||
addPath(path: MapPath): MapItemHandle;
|
||||
addPath(path: MapPath): MapPathHandle;
|
||||
|
||||
/** Cluster the markers added with cluster: true; call after each batch of addMarker calls */
|
||||
setClustering(options: MapClusterOptions | null): void;
|
||||
|
||||
@@ -334,6 +334,22 @@ export class HaControlSelect extends LitElement {
|
||||
width: 100%;
|
||||
hyphens: auto;
|
||||
}
|
||||
:host(:not([vertical])) .option {
|
||||
min-width: 0;
|
||||
}
|
||||
:host(:not([vertical])) .option .content {
|
||||
padding-inline: var(--ha-space-1);
|
||||
}
|
||||
:host(:not([vertical])) .option .content span {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
hyphens: manual;
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
:host([vertical]) {
|
||||
width: var(--control-select-thickness);
|
||||
height: auto;
|
||||
|
||||
@@ -21,6 +21,7 @@ import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { getEntityLocation } from "../../common/entity/get_entity_location";
|
||||
import { supportsVectorMaps } from "../../common/map/base-layer";
|
||||
import type {
|
||||
MapCircleHandle,
|
||||
MapClusterIcon,
|
||||
MapControlPosition,
|
||||
MapEngine,
|
||||
@@ -29,6 +30,7 @@ import type {
|
||||
MapLatLng,
|
||||
MapMarkerHandle,
|
||||
MapPath,
|
||||
MapPathHandle,
|
||||
MapPathMarker,
|
||||
MapPathSegment,
|
||||
MapEditableCircleHandle,
|
||||
@@ -419,7 +421,9 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
private _zoneHandles: MapItemHandle[] = [];
|
||||
|
||||
private _pathHandles: MapItemHandle[] = [];
|
||||
private _zoneCircleHandles: MapCircleHandle[] = [];
|
||||
|
||||
private _pathHandles: MapPathHandle[] = [];
|
||||
|
||||
private _focusPoints: MapLatLng[] = [];
|
||||
|
||||
@@ -483,6 +487,7 @@ export class HaMap extends ReactiveElement {
|
||||
this._entityMarkers.clear();
|
||||
this._clusterAvatars.clear();
|
||||
this._zoneHandles = [];
|
||||
this._zoneCircleHandles = [];
|
||||
this._pathHandles = [];
|
||||
this._removeEditableLocations();
|
||||
this._focusPoints = [];
|
||||
@@ -827,6 +832,7 @@ export class HaMap extends ReactiveElement {
|
||||
this._engine = undefined;
|
||||
this._entityHandles = [];
|
||||
this._zoneHandles = [];
|
||||
this._zoneCircleHandles = [];
|
||||
this._pathHandles = [];
|
||||
this._removeEditableLocations();
|
||||
this._focusPoints = [];
|
||||
@@ -1153,11 +1159,9 @@ export class HaMap extends ReactiveElement {
|
||||
if (!this._i18n || !this._config || !this._engine) {
|
||||
return;
|
||||
}
|
||||
if (this._pathHandles.length) {
|
||||
this._pathHandles.forEach((handle) => handle.remove());
|
||||
this._pathHandles = [];
|
||||
}
|
||||
if (!this.paths) {
|
||||
const paths = this.paths ?? [];
|
||||
this._pathHandles.splice(paths.length).forEach((handle) => handle.remove());
|
||||
if (!paths.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1165,7 +1169,7 @@ export class HaMap extends ReactiveElement {
|
||||
"--dark-primary-color"
|
||||
);
|
||||
|
||||
this.paths.forEach((path) => {
|
||||
paths.forEach((path, index) => {
|
||||
let opacityStep: number;
|
||||
let baseOpacity: number;
|
||||
if (path.gradualOpacity) {
|
||||
@@ -1257,7 +1261,12 @@ export class HaMap extends ReactiveElement {
|
||||
segments,
|
||||
markers,
|
||||
};
|
||||
this._pathHandles.push(this._engine!.addPath(enginePath));
|
||||
const handle = this._pathHandles[index];
|
||||
if (handle) {
|
||||
handle.update(enginePath);
|
||||
} else {
|
||||
this._pathHandles.push(this._engine!.addPath(enginePath));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1276,8 +1285,10 @@ export class HaMap extends ReactiveElement {
|
||||
this._zoneHandles.forEach((handle) => handle.remove());
|
||||
this._zoneHandles = [];
|
||||
this._focusZonePoints = [];
|
||||
let zoneCircleCount = 0;
|
||||
|
||||
if (!this.entities) {
|
||||
this._zoneCircleHandles.splice(0).forEach((handle) => handle.remove());
|
||||
this._entityMarkers.clear();
|
||||
this._clusterAvatars.clear();
|
||||
engine.setClustering(null);
|
||||
@@ -1359,9 +1370,16 @@ export class HaMap extends ReactiveElement {
|
||||
);
|
||||
|
||||
if (!hideRadius && radius) {
|
||||
this._zoneHandles.push(
|
||||
engine.addCircle(position, { radius, color: markerColor })
|
||||
);
|
||||
const circleOptions = { radius, color: markerColor };
|
||||
const circle = this._zoneCircleHandles[zoneCircleCount];
|
||||
if (circle) {
|
||||
circle.update(position, circleOptions);
|
||||
} else {
|
||||
this._zoneCircleHandles.push(
|
||||
engine.addCircle(position, circleOptions)
|
||||
);
|
||||
}
|
||||
zoneCircleCount++;
|
||||
}
|
||||
|
||||
const circleEl = createZoneMarkerElement({
|
||||
@@ -1490,6 +1508,10 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
this._zoneCircleHandles
|
||||
.splice(zoneCircleCount)
|
||||
.forEach((handle) => handle.remove());
|
||||
|
||||
const shownIds = new Set(this.entities.map(getEntityId));
|
||||
for (const cache of [this._entityMarkers, this._clusterAvatars]) {
|
||||
for (const entityId of cache.keys()) {
|
||||
|
||||
@@ -276,6 +276,21 @@ export class HatScriptGraph extends LitElement {
|
||||
nofocus
|
||||
aria-hidden="true"
|
||||
></hat-graph-node>
|
||||
<div
|
||||
class="graph-container"
|
||||
?track=${thenBranch.hasTrace}
|
||||
?unfinished=${thenBranch.unfinished}
|
||||
>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiCallReceived}
|
||||
?track=${thenBranch.hasTrace}
|
||||
?active=${this.selected === path}
|
||||
.notEnabled=${thenBranch.disabled}
|
||||
nofocus
|
||||
aria-hidden="true"
|
||||
></hat-graph-node>
|
||||
${this._renderBranchActions(thenBranch.children, graphEnd)}
|
||||
</div>
|
||||
${
|
||||
config.else
|
||||
? html`<div
|
||||
@@ -300,21 +315,6 @@ export class HatScriptGraph extends LitElement {
|
||||
?track=${elseBranch.hasTrace}
|
||||
></hat-graph-spacer>`
|
||||
}
|
||||
<div
|
||||
class="graph-container"
|
||||
?track=${thenBranch.hasTrace}
|
||||
?unfinished=${thenBranch.unfinished}
|
||||
>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiCallReceived}
|
||||
?track=${thenBranch.hasTrace}
|
||||
?active=${this.selected === path}
|
||||
.notEnabled=${thenBranch.disabled}
|
||||
nofocus
|
||||
aria-hidden="true"
|
||||
></hat-graph-node>
|
||||
${this._renderBranchActions(thenBranch.children, graphEnd)}
|
||||
</div>
|
||||
</hat-graph-branch>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,13 @@ import memoizeOne from "memoize-one";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import {
|
||||
computeDeviceName,
|
||||
computeDeviceNameDisplay,
|
||||
} from "../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||
import { isDeviceName } from "../../../common/entity/strip_prefix_from_entity_name";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
@@ -274,8 +278,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
this._name = this.entry.name || this._originalName;
|
||||
this._useDeviceName =
|
||||
!!this._device && !(this.entry.name ?? this._originalName);
|
||||
this._useDeviceName = !!this._device && !this._isOwnName(this.entry.name);
|
||||
this._icon = this.entry.icon || "";
|
||||
this._deviceClass =
|
||||
this.entry.device_class || this.entry.original_device_class;
|
||||
@@ -1757,7 +1760,19 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private get _hasOwnName(): boolean {
|
||||
return !!(this._computeName() ?? this._originalName);
|
||||
return this._isOwnName(this._computeName());
|
||||
}
|
||||
|
||||
// Only a user-set name needs the check, original_name is already unprefixed
|
||||
private _isOwnName(name: string | null): boolean {
|
||||
if (name === null) {
|
||||
return !!this._originalName;
|
||||
}
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
const deviceName = this._device && computeDeviceName(this._device);
|
||||
return !deviceName || !isDeviceName(name, deviceName);
|
||||
}
|
||||
|
||||
private get _useDeviceArea(): boolean {
|
||||
@@ -1787,6 +1802,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
return this.entry.name;
|
||||
}
|
||||
if (this._device && this._useDeviceName) {
|
||||
if (!this._isOwnName(this.entry.name)) {
|
||||
return this.entry.name;
|
||||
}
|
||||
return this._originalName ? "" : null;
|
||||
}
|
||||
const name = this._name.trim();
|
||||
|
||||
@@ -423,6 +423,7 @@ export class HuiViewHeader extends LitElement {
|
||||
.container:not(.edit-mode) .layout.badges-scroll hui-view-badges {
|
||||
--badges-wrap: nowrap;
|
||||
width: max-content;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
||||
@@ -147,6 +147,19 @@ describe("computeEntityEntryName", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("matches only a user-set name loosely against the device name", () => {
|
||||
const devices = { dev1: mockDevice({ id: "dev1", name: "Kitchen" }) };
|
||||
const entry = mockEntityEntry({ device_id: "dev1", name: "KITCHEN" });
|
||||
|
||||
expect(computeEntityEntryName(entry, devices)).toBeUndefined();
|
||||
expect(
|
||||
computeEntityEntryName(
|
||||
{ ...entry, name: null, original_name: "KITCHEN" },
|
||||
devices
|
||||
)
|
||||
).toBe("KITCHEN");
|
||||
});
|
||||
|
||||
it("returns undefined if no name, original_name, or device", () => {
|
||||
const entry = mockEntity({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isDeviceName } from "../../../src/common/entity/strip_prefix_from_entity_name";
|
||||
|
||||
describe("isDeviceName", () => {
|
||||
it.each([
|
||||
["Kitchen", "Kitchen", true],
|
||||
["KITCHEN", "Kitchen", true],
|
||||
["Kitchen -", "Kitchen", true],
|
||||
["Kitchen:", "Kitchen", true],
|
||||
["Kitchen Light", "Kitchen", false],
|
||||
["Kitchenette", "Kitchen", false],
|
||||
["Kitchen", "", false],
|
||||
])("%s with device %s is %s", (entityName, deviceName, expected) => {
|
||||
expect(isDeviceName(entityName, deviceName)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -89,9 +89,9 @@ const fakeEngine = vi.hoisted(() => {
|
||||
remove: vi.fn(),
|
||||
}));
|
||||
|
||||
addCircle = vi.fn(() => ({ remove: vi.fn() }));
|
||||
addCircle = vi.fn(() => ({ update: vi.fn(), remove: vi.fn() }));
|
||||
|
||||
addPath = vi.fn(() => ({ remove: vi.fn() }));
|
||||
addPath = vi.fn(() => ({ update: vi.fn(), remove: vi.fn() }));
|
||||
|
||||
setClustering = vi.fn();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user