Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/panels/lovelace/cards/hui-home-summary-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const COLORS: Record<HomeSummary, string> = {
climate: "deep-orange",
security: "blue-grey",
media_players: "blue",
unassigned_devices: "grey",
};

@customElement("hui-home-summary-card")
Expand Down
2 changes: 2 additions & 0 deletions src/panels/lovelace/strategies/get-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
"home-media-players": () =>
import("./home/home-media-players-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"),
security: () => import("../../security/strategies/security-view-strategy"),
climate: () => import("../../climate/strategies/climate-view-strategy"),
Expand Down
15 changes: 15 additions & 0 deletions src/panels/lovelace/strategies/home/helpers/home-summaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const HOME_SUMMARIES = [
"climate",
"security",
"media_players",
"unassigned_devices",
] as const;

export type HomeSummary = (typeof HOME_SUMMARIES)[number];
Expand All @@ -18,13 +19,27 @@ export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
climate: "mdi:home-thermometer",
security: "mdi:security",
media_players: "mdi:multimedia",
unassigned_devices: "mdi:shape",
};

export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
light: lightEntityFilters,
climate: climateEntityFilters,
security: securityEntityFilters,
media_players: [{ domain: "media_player", entity_category: "none" }],
unassigned_devices: [
{
area: null,
hidden_platform: [
"automation",
"script",
"hassio",
"backup",
"zone",
"person",
],
},
],
};

export const getSummaryLabel = (
Expand Down
11 changes: 11 additions & 0 deletions src/panels/lovelace/strategies/home/home-dashboard-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ export class HomeDashboardStrategy extends ReactiveElement {
icon: HOME_SUMMARIES_ICONS.media_players,
} 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 {
views: [
{
Expand All @@ -83,6 +93,7 @@ export class HomeDashboardStrategy extends ReactiveElement {
},
...areaViews,
mediaPlayersView,
unassignedDevicesView,
],
};
}
Expand Down
36 changes: 36 additions & 0 deletions src/panels/lovelace/strategies/home/home-main-view-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,19 @@ export class HomeMainViewStrategy extends ReactiveElement {
columns: 4,
},
} 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[];

const summarySection: LovelaceSectionConfig = {
Expand Down Expand Up @@ -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 = (
[
favoriteSection.cards && favoriteSection,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
7 changes: 6 additions & 1 deletion src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -7063,7 +7063,8 @@
},
"home": {
"summary_list": {
"media_players": "Media players"
"media_players": "Media players",
"unassigned_devices": "Unassigned devices"
},
"welcome_user": "Welcome {user}",
"summaries": "Summaries",
Expand Down Expand Up @@ -7093,6 +7094,10 @@
"home_media_players": {
"media_players": "Media players",
"other_media_players": "Other media players"
},
"unassigned_devices": {
"unassigned_helpers": "Unassigned helpers",
"unassigned_entities": "Unassigned entities"
}
},
"cards": {
Expand Down
Loading