Add a config option for column count

If cards are horizontally stacked, the number of columns can now be
configured in the Strategy options.

Stack counts under domains, affect the domain views as well as the Area
views.

```yaml
home_view:
  stack_count:
    _: 2
    persons: 3
    areas: [2,1] # [Strategy Card, HASS card]
domains:
  _:
    stack_count: 2
    light: 3
```

Closes #203
This commit is contained in:
DigiLive
2025-05-16 14:54:29 +02:00
parent b2e34e83a4
commit 2ff0782685
7 changed files with 112 additions and 37 deletions

View File

@@ -165,7 +165,7 @@ class Registry {
}));
// Process entries of the HASS area registry.
if (Registry.strategyOptions.areas._?.hidden) {
if (Registry.strategyOptions.areas._.hidden) {
Registry._areas = [];
} else {
// Create and add the undisclosed area if not hidden in the strategy options.
@@ -251,7 +251,7 @@ class Registry {
const states: string[] = [];
if (!Registry.initialized) {
logMessage(lvlWarn, 'Registry not initialized!');
logMessage(lvlWarn, 'Registry is not initialized!');
return '?';
}
@@ -264,6 +264,7 @@ class Registry {
.map((entity) => `states['${entity.entity_id}']`),
);
// noinspection SpellCheckingInspection
return `{% set entities = [${states}] %}
{{ entities
| selectattr('state','${operator}','${value}')

View File

@@ -6,6 +6,9 @@ import { localize } from './utilities/localize';
*/
export const ConfigurationDefaults: StrategyDefaults = {
areas: {
_: {
type: 'AreaCard',
},
undisclosed: {
// TODO: Refactor undisclosed to other.
aliases: [],
@@ -39,11 +42,13 @@ export const ConfigurationDefaults: StrategyDefaults = {
hide_config_entities: undefined,
hide_diagnostic_entities: undefined,
showControls: true,
stack_count: 1,
},
binary_sensor: {
title: `${localize('sensor.binary')} ` + localize('sensor.sensors'),
showControls: false,
hidden: false,
stack_count: 2, // TODO: Add to wiki. also for other configurations.
},
camera: {
title: localize('camera.cameras'),
@@ -141,6 +146,9 @@ export const ConfigurationDefaults: StrategyDefaults = {
extra_views: [],
home_view: {
hidden: [],
stack_count: {
_: 2,
},
},
views: {
camera: {

View File

@@ -121,7 +121,7 @@ class MushroomStrategy extends HTMLTemplateElement {
return null;
}
const titleCard = new HeaderCard(
const headerCard = new HeaderCard(
{ entity_id: entities.map((entity) => entity.entity_id) },
{
...Registry.strategyOptions.domains['_'],
@@ -133,7 +133,7 @@ class MushroomStrategy extends HTMLTemplateElement {
const DomainCard = (await import(`./cards/${moduleName}`)).default;
if (domain === 'sensor') {
const domainCards = entities
let domainCards = entities
.filter((entity) => Registry.hassStates[entity.entity_id]?.attributes.unit_of_measurement)
.map((entity) => {
const options = {
@@ -144,7 +144,17 @@ class MushroomStrategy extends HTMLTemplateElement {
};
return new SensorCard(entity, options).getCard();
});
return domainCards.length ? { type: 'vertical-stack', cards: [titleCard, ...domainCards] } : null;
if (domainCards.length) {
domainCards = stackHorizontal(
domainCards,
Registry.strategyOptions.domains[domain].stack_count ?? Registry.strategyOptions.domains['_'].stack_count,
);
return { type: 'vertical-stack', cards: [headerCard, ...domainCards] };
}
return null;
}
let domainCards = entities.map((entity) => {
@@ -155,11 +165,12 @@ class MushroomStrategy extends HTMLTemplateElement {
return new DomainCard(entity, cardOptions).getCard();
});
if (domain === 'binary_sensor') {
domainCards = stackHorizontal(domainCards);
}
domainCards = stackHorizontal(
domainCards,
Registry.strategyOptions.domains[domain].stack_count ?? Registry.strategyOptions.domains['_'].stack_count,
);
return domainCards.length ? { type: 'vertical-stack', cards: [titleCard, ...domainCards] } : null;
return domainCards.length ? { type: 'vertical-stack', cards: [headerCard, ...domainCards] } : null;
} catch (e) {
logMessage(lvlError, `Error creating card configurations for domain ${domain}`, e);
return null;
@@ -180,17 +191,27 @@ class MushroomStrategy extends HTMLTemplateElement {
if (miscellaneousEntities.length) {
try {
const MiscellaneousCard = (await import('./cards/MiscellaneousCard')).default;
const miscellaneousCards = [
new HeaderCard(target, Registry.strategyOptions.domains.default).createCard(),
...miscellaneousEntities.map((entity) =>
new MiscellaneousCard(entity, Registry.strategyOptions.card_options?.[entity.entity_id]).getCard(),
),
];
let miscellaneousCards = miscellaneousEntities.map((entity) =>
new MiscellaneousCard(entity, Registry.strategyOptions.card_options?.[entity.entity_id]).getCard(),
);
viewCards.push({
type: 'vertical-stack',
cards: miscellaneousCards,
});
const headerCard = new HeaderCard(target, {
...Registry.strategyOptions.domains['_'],
...Registry.strategyOptions.domains['default'],
}).createCard();
if (miscellaneousCards.length) {
miscellaneousCards = stackHorizontal(
miscellaneousCards,
Registry.strategyOptions.domains['default'].stack_count ??
Registry.strategyOptions.domains['_'].stack_count,
);
viewCards.push({
type: 'vertical-stack',
cards: [headerCard, ...miscellaneousCards],
});
}
} catch (e) {
logMessage(lvlError, 'Error creating card configurations for domain `miscellaneous`', e);
}

View File

@@ -1,4 +1,3 @@
import { AreaRegistryEntry } from '../homeassistant/data/area_registry';
import { DeviceRegistryEntry } from '../homeassistant/data/device_registry';
import { EntityRegistryEntry } from '../homeassistant/data/entity_registry';
import { ActionConfig, CallServiceActionConfig } from '../homeassistant/data/lovelace/config/action';
@@ -8,6 +7,7 @@ import { LovelaceViewConfig, LovelaceViewRawConfig } from '../homeassistant/data
import { HomeAssistant } from '../homeassistant/types';
import { LovelaceChipConfig } from '../lovelace-mushroom/utils/lovelace/chip/types';
import { StrategyHeaderCardConfig } from './strategy-cards';
import { AreaRegistryEntry } from '../homeassistant/data/area_registry';
/**
* List of supported domains.
@@ -69,6 +69,7 @@ const SUPPORTED_CHIPS = ['light', 'fan', 'cover', 'switch', 'climate', 'weather'
*
* This constant array defines the sections that are present in the home view.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const HOME_VIEW_SECTIONS = ['areas', 'areasTitle', 'chips', 'greeting', 'persons'] as const;
export type SupportedDomains = (typeof SUPPORTED_DOMAINS)[number];
@@ -175,11 +176,13 @@ export interface ViewInfo {
* @property {boolean} [hide_config_entities] - If True, all configuration entities are hidden from the dashboard.
* @property {boolean} [hide_diagnostic_entities] - If True, all diagnostic entities are hidden from the dashboard.
* @property {boolean} [showControls] - False to hide controls.
* @property {number} [stack_count] - Number of cards per row.
*/
export interface AllDomainsConfig {
hide_config_entities?: boolean;
hide_diagnostic_entities?: boolean;
showControls?: boolean;
stack_count?: number;
}
/**
@@ -187,23 +190,25 @@ export interface AllDomainsConfig {
*
* @property {boolean} hidden - If True, all entities of the domain are hidden from the dashboard.
* @property {number} [order] - Ordering position of the domains in a view.
* @property {number} [stack_count] - Number of cards per row.
*/
export interface SingleDomainConfig extends Partial<StrategyHeaderCardConfig> {
hidden: boolean;
order?: number;
stack_count?: number;
}
/**
* Strategy Configuration.
*
* @property {Object.<K in keyof StrategyArea, StrategyArea>} areas - List of areas.
* @property {Object.<K in keyof RegistryEntry, CustomCardConfig>} card_options - Card options for entities.
* @property {Object.<string, StrategyArea>} areas - The configuration of areas.
* @property {Object.<string, CustomCardConfig>} card_options - Card options for entities.
* @property {ChipConfiguration} chips - The configuration of chips in the Home view.
* @property {boolean} debug - If True, the strategy outputs more verbose debug information in the console.
* @property {Object.<string, AllDomainsConfig | SingleDomainConfig>} domains - List of domains.
* @property {LovelaceCardConfig[]} extra_cards - List of cards to show below room cards.
* @property {StrategyViewConfig[]} extra_views - List of custom-defined views to add to the dashboard.
* @property {{ hidden: HomeViewSections[] | [] }} home_view - List of views to add to the dashboard.
* @property {{ Object }} home_view - List of views to add to the dashboard.
* @property {Record<SupportedViews, StrategyViewConfig>} views - The configurations of views.
* @property {LovelaceCardConfig[]} quick_access_cards - List of custom-defined cards to show between the welcome card
* and rooms cards.
@@ -218,6 +223,7 @@ export interface StrategyConfig {
extra_views: StrategyViewConfig[];
home_view: {
hidden: HomeViewSections[];
stack_count: { _: number } & { [K in HomeViewSections]?: K extends 'areas' ? [number, number] : number };
};
views: Record<SupportedViews, StrategyViewConfig>;
quick_access_cards: LovelaceCardConfig[];
@@ -226,9 +232,12 @@ export interface StrategyConfig {
/**
* Represents the default configuration for a strategy.
*/
export interface StrategyDefaults extends StrategyConfig {
areas: { undisclosed: StrategyArea } & { [S: string]: StrategyArea };
}
export type StrategyDefaults = Omit<StrategyConfig, 'areas'> & {
areas: {
_: AllAreasConfig;
undisclosed: StrategyArea;
};
};
/**
* Strategy Area.
@@ -246,6 +255,15 @@ export interface StrategyArea extends AreaRegistryEntry {
type?: string;
}
/**
* Configuration for all areas.
*
* @property {string} [type] - The type of area card.
*/
export interface AllAreasConfig {
type?: string;
}
/**
* A list of chips to show in the Home view.
*

View File

@@ -1,6 +1,7 @@
import { LovelaceCardConfig } from '../types/homeassistant/data/lovelace/config/card';
import { StackCardConfig } from '../types/homeassistant/panels/lovelace/cards/types';
// noinspection GrazieInspection
/**
* Stacks an array of Lovelace card configurations into horizontal stacks based on their type.
*
@@ -9,6 +10,7 @@ import { StackCardConfig } from '../types/homeassistant/panels/lovelace/cards/ty
* It returns a new array of stacked card configurations, preserving the original order of the cards.
*
* @param cardConfigurations - An array of Lovelace card configurations to be stacked.
* @param defaultCount - The default number of cards to stack if the type or column count is not found in the mapping.
* @param [columnCounts] - An object mapping card types to their respective column counts.
* If a type is not found in the mapping, it defaults to 2.
* @returns An array of stacked card configurations, where each configuration is a horizontal stack
@@ -16,17 +18,26 @@ import { StackCardConfig } from '../types/homeassistant/panels/lovelace/cards/ty
*
* @example
* ```typescript
* stackedCards = stackHorizontal(card, {area: 1, "custom:card": 2});
* stackedCards = stackHorizontal(card, 2, {area: 1, 'custom:card': 2});
* ```
*/
export function stackHorizontal(
cardConfigurations: LovelaceCardConfig[],
defaultCount: number = 2,
columnCounts?: {
[key: string]: number;
[key: string]: number | undefined;
},
): LovelaceCardConfig[] {
if (cardConfigurations.length <= 1) {
return cardConfigurations;
}
// Function to process a sequence of cards
const doStack = (cards: LovelaceCardConfig[], columnCount: number) => {
if (cards.length <= 1) {
return cards;
}
const stackedCardConfigurations: StackCardConfig[] = [];
for (let i = 0; i < cards.length; i += columnCount) {
@@ -44,7 +55,7 @@ export function stackHorizontal(
for (let i = 0; i < cardConfigurations.length; ) {
const currentCard = cardConfigurations[i];
const currentType = currentCard.type; // Assuming each card has a 'type' property
const currentType = currentCard.type;
// Start a new sequence
const sequence: LovelaceCardConfig[] = [];
@@ -55,7 +66,7 @@ export function stackHorizontal(
i++; // Move to the next card
}
const columnCount = Math.max(columnCounts?.[currentType] || 2, 1);
const columnCount = Math.max(columnCounts?.[currentType] || defaultCount, 1);
// Process the sequence and add the result to the processedConfigurations array
processedConfigurations.push(...doStack(sequence, columnCount));

View File

@@ -10,6 +10,7 @@ import { ViewConfig, ViewConstructor } from '../types/strategy/strategy-views';
import { sanitizeClassName } from '../utilities/auxiliaries';
import { logMessage, lvlFatal } from '../utilities/debug';
import RegistryFilter from '../utilities/RegistryFilter';
import { stackHorizontal } from '../utilities/cardStacking';
/**
* Abstract View Class.
@@ -45,7 +46,7 @@ abstract class AbstractView {
*/
protected constructor() {
if (!Registry.initialized) {
logMessage(lvlFatal, 'Registry not initialized!');
logMessage(lvlFatal, 'Registry is not initialized!');
}
}
@@ -63,7 +64,7 @@ abstract class AbstractView {
// Create card configurations for each area.
for (const area of Registry.areas) {
const areaCards: AbstractCardConfig[] = [];
let areaCards: AbstractCardConfig[] = [];
// Set the target of the Header card to the current area.
let target: HassServiceTarget = {
@@ -85,8 +86,14 @@ abstract class AbstractView {
),
);
// Vertically stack the cards of the current area.
// Stack the cards of the current area.
if (areaCards.length) {
areaCards = stackHorizontal(
areaCards,
Registry.strategyOptions.domains[this.domain as SupportedDomains].stack_count ??
Registry.strategyOptions.domains['_'].stack_count,
);
// Create and insert a Header card.
const areaHeaderCardOptions = (
'headerCardConfiguration' in this.baseConfiguration ? this.baseConfiguration.headerCardConfiguration : {}

View File

@@ -210,7 +210,11 @@ class HomeView extends AbstractView {
return {
type: 'vertical-stack',
cards: stackHorizontal(cardConfigurations),
cards: stackHorizontal(
cardConfigurations,
Registry.strategyOptions.home_view.stack_count['persons'] ??
Registry.strategyOptions.home_view.stack_count['_'],
),
};
}
@@ -223,7 +227,6 @@ class HomeView extends AbstractView {
private async createAreasSection(): Promise<StackCardConfig | undefined> {
if (Registry.strategyOptions.home_view.hidden.includes('areas')) {
// Areas section is hidden.
return;
}
@@ -253,11 +256,17 @@ class HomeView extends AbstractView {
}).getCard(),
);
}
console.log(
Registry.strategyOptions.home_view.stack_count.areas?.[0],
Registry.strategyOptions.home_view.stack_count.areas?.[1],
);
return {
type: 'vertical-stack',
title: Registry.strategyOptions.home_view.hidden.includes('areasTitle') ? undefined : localize('generic.areas'),
cards: stackHorizontal(cardConfigurations, { area: 1, 'custom:mushroom-template-card': 2 }),
cards: stackHorizontal(cardConfigurations, Registry.strategyOptions.home_view.stack_count['_'], {
'custom:mushroom-template-card': Registry.strategyOptions.home_view.stack_count.areas?.[0],
area: Registry.strategyOptions.home_view.stack_count.areas?.[1],
}),
};
}
}