Compare commits

...

2 Commits

Author SHA1 Message Date
Paul Bottein
5ce6419cd8 Add sections for entities and helpers 2025-11-25 14:53:49 +01:00
Paul Bottein
ff28ca11c9 Add unassigned devices view to home dashboard 2025-11-25 11:50:30 +01:00
7 changed files with 276 additions and 1 deletions

View File

@@ -35,6 +35,7 @@ const COLORS: Record<HomeSummary, string> = {
climate: "deep-orange", climate: "deep-orange",
security: "blue-grey", security: "blue-grey",
media_players: "blue", media_players: "blue",
unassigned_devices: "grey",
}; };
@customElement("hui-home-summary-card") @customElement("hui-home-summary-card")

View File

@@ -50,6 +50,8 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
"home-media-players": () => "home-media-players": () =>
import("./home/home-media-players-view-strategy"), import("./home/home-media-players-view-strategy"),
"home-area": () => import("./home/home-area-view-strategy"), "home-area": () => import("./home/home-area-view-strategy"),
"home-unassigned-devices": () =>
import("./home/home-unassigned-devices-view-strategy"),
light: () => import("../../light/strategies/light-view-strategy"), light: () => import("../../light/strategies/light-view-strategy"),
security: () => import("../../security/strategies/security-view-strategy"), security: () => import("../../security/strategies/security-view-strategy"),
climate: () => import("../../climate/strategies/climate-view-strategy"), climate: () => import("../../climate/strategies/climate-view-strategy"),

View File

@@ -9,6 +9,7 @@ export const HOME_SUMMARIES = [
"climate", "climate",
"security", "security",
"media_players", "media_players",
"unassigned_devices",
] as const; ] as const;
export type HomeSummary = (typeof HOME_SUMMARIES)[number]; export type HomeSummary = (typeof HOME_SUMMARIES)[number];
@@ -18,6 +19,7 @@ export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
climate: "mdi:home-thermometer", climate: "mdi:home-thermometer",
security: "mdi:security", security: "mdi:security",
media_players: "mdi:multimedia", media_players: "mdi:multimedia",
unassigned_devices: "mdi:shape",
}; };
export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = { export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
@@ -25,6 +27,19 @@ export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
climate: climateEntityFilters, climate: climateEntityFilters,
security: securityEntityFilters, security: securityEntityFilters,
media_players: [{ domain: "media_player", entity_category: "none" }], media_players: [{ domain: "media_player", entity_category: "none" }],
unassigned_devices: [
{
area: null,
hidden_platform: [
"automation",
"script",
"hassio",
"backup",
"zone",
"person",
],
},
],
}; };
export const getSummaryLabel = ( export const getSummaryLabel = (

View File

@@ -71,6 +71,16 @@ export class HomeDashboardStrategy extends ReactiveElement {
icon: HOME_SUMMARIES_ICONS.media_players, icon: HOME_SUMMARIES_ICONS.media_players,
} satisfies LovelaceViewRawConfig; } satisfies LovelaceViewRawConfig;
const unassignedDevicesView = {
title: getSummaryLabel(hass.localize, "unassigned_devices"),
path: "unassigned-devices",
subview: true,
strategy: {
type: "home-unassigned-devices",
},
icon: HOME_SUMMARIES_ICONS.unassigned_devices,
} satisfies LovelaceViewRawConfig;
return { return {
views: [ views: [
{ {
@@ -83,6 +93,7 @@ export class HomeDashboardStrategy extends ReactiveElement {
}, },
...areaViews, ...areaViews,
mediaPlayersView, mediaPlayersView,
unassignedDevicesView,
], ],
}; };
} }

View File

@@ -228,6 +228,19 @@ export class HomeMainViewStrategy extends ReactiveElement {
columns: 4, columns: 4,
}, },
} satisfies HomeSummaryCard), } satisfies HomeSummaryCard),
{
type: "home-summary",
summary: "unassigned_devices",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "unassigned-devices",
},
grid_options: {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard,
].filter(Boolean) as LovelaceCardConfig[]; ].filter(Boolean) as LovelaceCardConfig[];
const summarySection: LovelaceSectionConfig = { const summarySection: LovelaceSectionConfig = {
@@ -297,6 +310,29 @@ export class HomeMainViewStrategy extends ReactiveElement {
} }
} }
const noAreaFilter = generateEntityFilter(hass, {
area: null,
});
const otherEntities = allEntities.filter(noAreaFilter);
if (otherEntities.length > 0) {
widgetSection.cards!.push({
type: "tile",
entity: otherEntities[0],
icon: "mdi:shape",
name: "Unassigned devices",
hide_state: true,
tap_action: {
action: "navigate",
navigation_path: "unassigned-devices",
},
icon_tap_action: {
action: "none",
},
});
}
const sections = ( const sections = (
[ [
favoriteSection.cards && favoriteSection, favoriteSection.cards && favoriteSection,

View File

@@ -0,0 +1,205 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
import {
findEntities,
generateEntityFilter,
} from "../../../../common/entity/entity_filter";
import { clamp } from "../../../../common/number/clamp";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import { isHelperDomain } from "../../../config/helpers/const";
import type { HeadingCardConfig } from "../../cards/types";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
export interface HomeUnassignedDevicesViewStrategyConfig {
type: "home-unassigned-devices";
}
@customElement("home-unassigned-devices-view-strategy")
export class HomeUnassignedDevicesViewStrategy extends ReactiveElement {
static async generate(
_config: HomeUnassignedDevicesViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const allEntities = Object.keys(hass.states);
const unassignedFilters = HOME_SUMMARIES_FILTERS.unassigned_devices.map(
(filter) => generateEntityFilter(hass, filter)
);
const unassignedEntities = findEntities(allEntities, unassignedFilters);
const sections: LovelaceSectionRawConfig[] = [];
const entitiesByDevice: Record<string, string[]> = {};
const entitiesWithoutDevices: string[] = [];
for (const entityId of unassignedEntities) {
const stateObj = hass.states[entityId];
if (!stateObj) continue;
const { device } = getEntityContext(
stateObj,
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
if (!device) {
entitiesWithoutDevices.push(entityId);
continue;
}
if (!(device.id in entitiesByDevice)) {
entitiesByDevice[device.id] = [];
}
entitiesByDevice[device.id].push(entityId);
}
const devicesEntities = Object.entries(entitiesByDevice).map(
([deviceId, entities]) => ({
device_id: deviceId,
entities: entities,
})
);
const helpersEntities = entitiesWithoutDevices.filter((entityId) => {
const domain = entityId.split(".")[0];
return isHelperDomain(domain);
});
const otherEntities = entitiesWithoutDevices.filter((entityId) => {
const domain = entityId.split(".")[0];
return !isHelperDomain(domain);
});
const batteryFilter = generateEntityFilter(hass, {
domain: "sensor",
device_class: "battery",
});
const energyFilter = generateEntityFilter(hass, {
domain: "sensor",
device_class: ["energy", "power"],
});
const primaryFilter = generateEntityFilter(hass, {
entity_category: "none",
});
for (const deviceEntities of devicesEntities) {
if (deviceEntities.entities.length === 0) continue;
const batteryEntities = deviceEntities.entities.filter((e) =>
batteryFilter(e)
);
const entities = deviceEntities.entities.filter(
(e) => !batteryFilter(e) && !energyFilter(e) && primaryFilter(e)
);
if (entities.length === 0) {
continue;
}
const deviceId = deviceEntities.device_id;
const device = hass.devices[deviceId];
let heading = "";
if (device) {
heading =
computeDeviceName(device) ||
hass.localize("ui.panel.lovelace.strategy.home.unamed_device");
}
sections.push({
type: "grid",
cards: [
{
type: "heading",
heading: heading,
tap_action: device
? {
action: "navigate",
navigation_path: `/config/devices/device/${device.id}`,
}
: undefined,
badges: [
...batteryEntities.slice(0, 1).map((e) => ({
entity: e,
type: "entity",
tap_action: {
action: "more-info",
},
})),
],
} satisfies HeadingCardConfig,
...entities.map((e) => ({
type: "tile",
entity: e,
name: {
type: "entity",
},
})),
],
});
}
if (helpersEntities.length) {
sections.push({
type: "grid",
cards: [
{
type: "heading",
heading: hass.localize(
"ui.panel.lovelace.strategy.unassigned_devices.unassigned_helpers"
),
} satisfies HeadingCardConfig,
...helpersEntities.map((e) => ({
type: "tile",
entity: e,
})),
],
});
}
if (otherEntities.length) {
sections.push({
type: "grid",
cards: [
{
type: "heading",
heading: hass.localize(
"ui.panel.lovelace.strategy.unassigned_devices.unassigned_entities"
),
} satisfies HeadingCardConfig,
...otherEntities.map((e) => ({
type: "tile",
entity: e,
})),
],
});
}
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
const maxColumns = clamp(sections.length, 2, 3);
// Take the full width if there is only one section to avoid narrow header on desktop
if (sections.length === 1) {
sections[0].column_span = 2;
}
return {
type: "sections",
header: {
badges_position: "bottom",
},
max_columns: maxColumns,
sections: sections,
};
}
}
declare global {
interface HTMLElementTagNameMap {
"home-unassigned-devices-view-strategy": HomeUnassignedDevicesViewStrategy;
}
}

View File

@@ -7063,7 +7063,8 @@
}, },
"home": { "home": {
"summary_list": { "summary_list": {
"media_players": "Media players" "media_players": "Media players",
"unassigned_devices": "Unassigned devices"
}, },
"welcome_user": "Welcome {user}", "welcome_user": "Welcome {user}",
"summaries": "Summaries", "summaries": "Summaries",
@@ -7093,6 +7094,10 @@
"home_media_players": { "home_media_players": {
"media_players": "Media players", "media_players": "Media players",
"other_media_players": "Other media players" "other_media_players": "Other media players"
},
"unassigned_devices": {
"unassigned_helpers": "Unassigned helpers",
"unassigned_entities": "Unassigned entities"
} }
}, },
"cards": { "cards": {