mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-24 12:49:19 +00:00
Display entities without area in summary dashboard (#27777)
* Add support for no area, no floor and no device in entity filter * Display entities without area in summary dashboard
This commit is contained in:
@@ -9,9 +9,9 @@ type EntityCategory = "none" | "config" | "diagnostic";
|
|||||||
export interface EntityFilter {
|
export interface EntityFilter {
|
||||||
domain?: string | string[];
|
domain?: string | string[];
|
||||||
device_class?: string | string[];
|
device_class?: string | string[];
|
||||||
device?: string | string[];
|
device?: string | null | (string | null)[];
|
||||||
area?: string | string[];
|
area?: string | null | (string | null)[];
|
||||||
floor?: string | string[];
|
floor?: string | null | (string | null)[];
|
||||||
label?: string | string[];
|
label?: string | string[];
|
||||||
entity_category?: EntityCategory | EntityCategory[];
|
entity_category?: EntityCategory | EntityCategory[];
|
||||||
hidden_platform?: string | string[];
|
hidden_platform?: string | string[];
|
||||||
@@ -19,6 +19,18 @@ export interface EntityFilter {
|
|||||||
|
|
||||||
export type EntityFilterFunc = (entityId: string) => boolean;
|
export type EntityFilterFunc = (entityId: string) => boolean;
|
||||||
|
|
||||||
|
const normalizeFilterArray = <T>(
|
||||||
|
value: T | null | T[] | (T | null)[] | undefined
|
||||||
|
): Set<T | null> | undefined => {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (value === null) {
|
||||||
|
return new Set([null]);
|
||||||
|
}
|
||||||
|
return new Set(ensureArray(value));
|
||||||
|
};
|
||||||
|
|
||||||
export const generateEntityFilter = (
|
export const generateEntityFilter = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
filter: EntityFilter
|
filter: EntityFilter
|
||||||
@@ -29,11 +41,9 @@ export const generateEntityFilter = (
|
|||||||
const deviceClasses = filter.device_class
|
const deviceClasses = filter.device_class
|
||||||
? new Set(ensureArray(filter.device_class))
|
? new Set(ensureArray(filter.device_class))
|
||||||
: undefined;
|
: undefined;
|
||||||
const floors = filter.floor ? new Set(ensureArray(filter.floor)) : undefined;
|
const floors = normalizeFilterArray(filter.floor);
|
||||||
const areas = filter.area ? new Set(ensureArray(filter.area)) : undefined;
|
const areas = normalizeFilterArray(filter.area);
|
||||||
const devices = filter.device
|
const devices = normalizeFilterArray(filter.device);
|
||||||
? new Set(ensureArray(filter.device))
|
|
||||||
: undefined;
|
|
||||||
const entityCategories = filter.entity_category
|
const entityCategories = filter.entity_category
|
||||||
? new Set(ensureArray(filter.entity_category))
|
? new Set(ensureArray(filter.entity_category))
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -73,23 +83,20 @@ export const generateEntityFilter = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (floors) {
|
if (floors) {
|
||||||
if (!floor || !floors.has(floor.floor_id)) {
|
const floorId = floor?.floor_id ?? null;
|
||||||
|
if (!floors.has(floorId)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (areas) {
|
if (areas) {
|
||||||
if (!area) {
|
const areaId = area?.area_id ?? null;
|
||||||
return false;
|
if (!areas.has(areaId)) {
|
||||||
}
|
|
||||||
if (!areas.has(area.area_id)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (devices) {
|
if (devices) {
|
||||||
if (!device) {
|
const deviceId = device?.id ?? null;
|
||||||
return false;
|
if (!devices.has(deviceId)) {
|
||||||
}
|
|
||||||
if (!devices.has(device.id)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,24 @@ const processAreasForClimate = (
|
|||||||
return cards;
|
return cards;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const processUnassignedEntities = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entities: string[]
|
||||||
|
): LovelaceCardConfig[] => {
|
||||||
|
const unassignedFilter = generateEntityFilter(hass, {
|
||||||
|
area: null,
|
||||||
|
});
|
||||||
|
const unassignedEntities = entities.filter(unassignedFilter);
|
||||||
|
const areaCards: LovelaceCardConfig[] = [];
|
||||||
|
const computeTileCard = computeAreaTileCardConfig(hass, "", true);
|
||||||
|
|
||||||
|
for (const entityId of unassignedEntities) {
|
||||||
|
areaCards.push(computeTileCard(entityId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return areaCards;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("climate-view-strategy")
|
@customElement("climate-view-strategy")
|
||||||
export class ClimateViewStrategy extends ReactiveElement {
|
export class ClimateViewStrategy extends ReactiveElement {
|
||||||
static async generate(
|
static async generate(
|
||||||
@@ -190,10 +208,33 @@ export class ClimateViewStrategy extends ReactiveElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process unassigned entities
|
||||||
|
const unassignedCards = processUnassignedEntities(hass, entities);
|
||||||
|
|
||||||
|
if (unassignedCards.length > 0) {
|
||||||
|
const section: LovelaceSectionRawConfig = {
|
||||||
|
type: "grid",
|
||||||
|
column_span: 2,
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
type: "heading",
|
||||||
|
heading:
|
||||||
|
sections.length > 0
|
||||||
|
? hass.localize(
|
||||||
|
"ui.panel.lovelace.strategy.climate.other_devices"
|
||||||
|
)
|
||||||
|
: hass.localize("ui.panel.lovelace.strategy.climate.devices"),
|
||||||
|
},
|
||||||
|
...unassignedCards,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
sections.push(section);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "sections",
|
type: "sections",
|
||||||
max_columns: 2,
|
max_columns: 2,
|
||||||
sections: sections || [],
|
sections: sections,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,24 @@ const processAreasForLight = (
|
|||||||
return cards;
|
return cards;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const processUnassignedLights = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entities: string[]
|
||||||
|
): LovelaceCardConfig[] => {
|
||||||
|
const unassignedFilter = generateEntityFilter(hass, {
|
||||||
|
area: null,
|
||||||
|
});
|
||||||
|
const unassignedLights = entities.filter(unassignedFilter);
|
||||||
|
const areaCards: LovelaceCardConfig[] = [];
|
||||||
|
const computeTileCard = computeAreaTileCardConfig(hass, "", false);
|
||||||
|
|
||||||
|
for (const entityId of unassignedLights) {
|
||||||
|
areaCards.push(computeTileCard(entityId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return areaCards;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("light-view-strategy")
|
@customElement("light-view-strategy")
|
||||||
export class LightViewStrategy extends ReactiveElement {
|
export class LightViewStrategy extends ReactiveElement {
|
||||||
static async generate(
|
static async generate(
|
||||||
@@ -136,10 +154,30 @@ export class LightViewStrategy extends ReactiveElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process unassigned lights
|
||||||
|
const unassignedCards = processUnassignedLights(hass, entities);
|
||||||
|
if (unassignedCards.length > 0) {
|
||||||
|
const section: LovelaceSectionRawConfig = {
|
||||||
|
type: "grid",
|
||||||
|
column_span: 2,
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
type: "heading",
|
||||||
|
heading:
|
||||||
|
sections.length > 0
|
||||||
|
? hass.localize("ui.panel.lovelace.strategy.light.other_lights")
|
||||||
|
: hass.localize("ui.panel.lovelace.strategy.light.lights"),
|
||||||
|
},
|
||||||
|
...unassignedCards,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
sections.push(section);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "sections",
|
type: "sections",
|
||||||
max_columns: 2,
|
max_columns: 2,
|
||||||
sections: sections || [],
|
sections: sections,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,11 +87,6 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
|
|||||||
const allEntities = Object.keys(this.hass!.states);
|
const allEntities = Object.keys(this.hass!.states);
|
||||||
|
|
||||||
const areas = Object.values(this.hass.areas);
|
const areas = Object.values(this.hass.areas);
|
||||||
const areasFilter = generateEntityFilter(this.hass, {
|
|
||||||
area: areas.map((area) => area.area_id),
|
|
||||||
});
|
|
||||||
|
|
||||||
const entitiesInsideArea = allEntities.filter(areasFilter);
|
|
||||||
|
|
||||||
switch (this._config.summary) {
|
switch (this._config.summary) {
|
||||||
case "light": {
|
case "light": {
|
||||||
@@ -100,7 +95,7 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
|
|||||||
generateEntityFilter(this.hass!, filter)
|
generateEntityFilter(this.hass!, filter)
|
||||||
);
|
);
|
||||||
|
|
||||||
const lightEntities = findEntities(entitiesInsideArea, lightsFilters);
|
const lightEntities = findEntities(allEntities, lightsFilters);
|
||||||
|
|
||||||
const onLights = lightEntities.filter((entityId) => {
|
const onLights = lightEntities.filter((entityId) => {
|
||||||
const s = this.hass!.states[entityId]?.state;
|
const s = this.hass!.states[entityId]?.state;
|
||||||
@@ -153,7 +148,7 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
|
|||||||
generateEntityFilter(this.hass!, filter)
|
generateEntityFilter(this.hass!, filter)
|
||||||
);
|
);
|
||||||
|
|
||||||
const safetyEntities = findEntities(entitiesInsideArea, safetyFilters);
|
const safetyEntities = findEntities(allEntities, safetyFilters);
|
||||||
|
|
||||||
const locks = safetyEntities.filter((entityId) => {
|
const locks = safetyEntities.filter((entityId) => {
|
||||||
const domain = computeDomain(entityId);
|
const domain = computeDomain(entityId);
|
||||||
@@ -204,7 +199,7 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const mediaPlayerEntities = findEntities(
|
const mediaPlayerEntities = findEntities(
|
||||||
entitiesInsideArea,
|
allEntities,
|
||||||
mediaPlayerFilters
|
mediaPlayerFilters
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,26 @@ const processAreasForMediaPlayers = (
|
|||||||
return cards;
|
return cards;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const processUnassignedEntities = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entities: string[]
|
||||||
|
): LovelaceCardConfig[] => {
|
||||||
|
const unassignedFilter = generateEntityFilter(hass, {
|
||||||
|
area: null,
|
||||||
|
});
|
||||||
|
const unassignedEntities = entities.filter(unassignedFilter);
|
||||||
|
const areaCards: LovelaceCardConfig[] = [];
|
||||||
|
|
||||||
|
for (const entityId of unassignedEntities) {
|
||||||
|
areaCards.push({
|
||||||
|
type: "media-control",
|
||||||
|
entity: entityId,
|
||||||
|
} satisfies MediaControlCardConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return areaCards;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("home-media-players-view-strategy")
|
@customElement("home-media-players-view-strategy")
|
||||||
export class HomeMMediaPlayersViewStrategy extends ReactiveElement {
|
export class HomeMMediaPlayersViewStrategy extends ReactiveElement {
|
||||||
static async generate(
|
static async generate(
|
||||||
@@ -134,10 +154,35 @@ export class HomeMMediaPlayersViewStrategy extends ReactiveElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process unassigned entities
|
||||||
|
const unassignedCards = processUnassignedEntities(hass, entities);
|
||||||
|
|
||||||
|
if (unassignedCards.length > 0) {
|
||||||
|
const section: LovelaceSectionRawConfig = {
|
||||||
|
type: "grid",
|
||||||
|
column_span: 2,
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
type: "heading",
|
||||||
|
heading:
|
||||||
|
sections.length > 0
|
||||||
|
? hass.localize(
|
||||||
|
"ui.panel.lovelace.strategy.home_media_players.other_media_players"
|
||||||
|
)
|
||||||
|
: hass.localize(
|
||||||
|
"ui.panel.lovelace.strategy.home_media_players.media_players"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
...unassignedCards,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
sections.push(section);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "sections",
|
type: "sections",
|
||||||
max_columns: 2,
|
max_columns: 2,
|
||||||
sections: sections || [],
|
sections: sections,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,24 @@ const processAreasForSafety = (
|
|||||||
return cards;
|
return cards;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const processUnassignedEntities = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entities: string[]
|
||||||
|
): LovelaceCardConfig[] => {
|
||||||
|
const unassignedFilter = generateEntityFilter(hass, {
|
||||||
|
area: null,
|
||||||
|
});
|
||||||
|
const unassignedLights = entities.filter(unassignedFilter);
|
||||||
|
const areaCards: LovelaceCardConfig[] = [];
|
||||||
|
const computeTileCard = computeAreaTileCardConfig(hass, "", false);
|
||||||
|
|
||||||
|
for (const entityId of unassignedLights) {
|
||||||
|
areaCards.push(computeTileCard(entityId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return areaCards;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("safety-view-strategy")
|
@customElement("safety-view-strategy")
|
||||||
export class SafetyViewStrategy extends ReactiveElement {
|
export class SafetyViewStrategy extends ReactiveElement {
|
||||||
static async generate(
|
static async generate(
|
||||||
@@ -178,10 +196,33 @@ export class SafetyViewStrategy extends ReactiveElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process unassigned entities
|
||||||
|
const unassignedCards = processUnassignedEntities(hass, entities);
|
||||||
|
|
||||||
|
if (unassignedCards.length > 0) {
|
||||||
|
const section: LovelaceSectionRawConfig = {
|
||||||
|
type: "grid",
|
||||||
|
column_span: 2,
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
type: "heading",
|
||||||
|
heading:
|
||||||
|
sections.length > 0
|
||||||
|
? hass.localize(
|
||||||
|
"ui.panel.lovelace.strategy.safety.other_devices"
|
||||||
|
)
|
||||||
|
: hass.localize("ui.panel.lovelace.strategy.safety.devices"),
|
||||||
|
},
|
||||||
|
...unassignedCards,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
sections.push(section);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "sections",
|
type: "sections",
|
||||||
max_columns: 2,
|
max_columns: 2,
|
||||||
sections: sections || [],
|
sections: sections,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6975,6 +6975,22 @@
|
|||||||
"common_controls": {
|
"common_controls": {
|
||||||
"not_loaded": "Usage Prediction integration is not loaded.",
|
"not_loaded": "Usage Prediction integration is not loaded.",
|
||||||
"no_data": "This place will soon fill up with the entities you use most often, based on your activity."
|
"no_data": "This place will soon fill up with the entities you use most often, based on your activity."
|
||||||
|
},
|
||||||
|
"light": {
|
||||||
|
"lights": "Lights",
|
||||||
|
"other_lights": "Other lights"
|
||||||
|
},
|
||||||
|
"safety": {
|
||||||
|
"devices": "Devices",
|
||||||
|
"other_devices": "Other devices"
|
||||||
|
},
|
||||||
|
"climate": {
|
||||||
|
"devices": "Devices",
|
||||||
|
"other_devices": "Other devices"
|
||||||
|
},
|
||||||
|
"home_media_players": {
|
||||||
|
"media_players": "Media players",
|
||||||
|
"other_media_players": "Other media players"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cards": {
|
"cards": {
|
||||||
|
|||||||
@@ -388,4 +388,70 @@ describe("generateEntityFilter", () => {
|
|||||||
expect(filter("light.no_area")).toBe(false);
|
expect(filter("light.no_area")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("null filtering", () => {
|
||||||
|
it("should filter entities with no area when null is used", () => {
|
||||||
|
const filter = generateEntityFilter(mockHass, { area: null });
|
||||||
|
|
||||||
|
expect(filter("light.no_area")).toBe(true);
|
||||||
|
expect(filter("light.living_room")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter entities with specific area OR no area when null is in array", () => {
|
||||||
|
const filter = generateEntityFilter(mockHass, {
|
||||||
|
area: ["living_room", null],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(filter("light.living_room")).toBe(true);
|
||||||
|
expect(filter("sensor.temperature")).toBe(true);
|
||||||
|
expect(filter("light.no_area")).toBe(true);
|
||||||
|
expect(filter("switch.kitchen")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter entities with no floor when null is used", () => {
|
||||||
|
const filter = generateEntityFilter(mockHass, { floor: null });
|
||||||
|
|
||||||
|
expect(filter("light.no_area")).toBe(true);
|
||||||
|
expect(filter("light.living_room")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter entities with specific floor OR no floor", () => {
|
||||||
|
const filter = generateEntityFilter(mockHass, {
|
||||||
|
floor: ["main_floor", null],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(filter("light.living_room")).toBe(true);
|
||||||
|
expect(filter("switch.kitchen")).toBe(true);
|
||||||
|
expect(filter("light.no_area")).toBe(true);
|
||||||
|
expect(filter("light.bedroom")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter entities with no device when null is used", () => {
|
||||||
|
const filter = generateEntityFilter(mockHass, { device: null });
|
||||||
|
|
||||||
|
expect(filter("light.living_room")).toBe(false);
|
||||||
|
expect(filter("light.no_area")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter entities with specific device OR no device", () => {
|
||||||
|
const filter = generateEntityFilter(mockHass, {
|
||||||
|
device: ["device1", null],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(filter("light.living_room")).toBe(true);
|
||||||
|
expect(filter("switch.kitchen")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should combine null filtering with other criteria", () => {
|
||||||
|
const filter = generateEntityFilter(mockHass, {
|
||||||
|
domain: "light",
|
||||||
|
area: ["living_room", null],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(filter("light.living_room")).toBe(true);
|
||||||
|
expect(filter("light.no_area")).toBe(true);
|
||||||
|
expect(filter("light.bedroom")).toBe(false);
|
||||||
|
expect(filter("sensor.temperature")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user