mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-28 12:17:23 +00:00
Compare commits
2 Commits
fix-undo-r
...
unassigned
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ce6419cd8 | ||
|
|
ff28ca11c9 |
@@ -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")
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -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,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": {
|
||||||
|
|||||||
Reference in New Issue
Block a user