diff --git a/src/Registry.ts b/src/Registry.ts index 938d9cb..b869836 100644 --- a/src/Registry.ts +++ b/src/Registry.ts @@ -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}') diff --git a/src/configurationDefaults.ts b/src/configurationDefaults.ts index 7a35b8e..1fb8cc7 100644 --- a/src/configurationDefaults.ts +++ b/src/configurationDefaults.ts @@ -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: { diff --git a/src/mushroom-strategy.ts b/src/mushroom-strategy.ts index ebd7502..6c75ccd 100644 --- a/src/mushroom-strategy.ts +++ b/src/mushroom-strategy.ts @@ -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); } diff --git a/src/types/strategy/strategy-generics.ts b/src/types/strategy/strategy-generics.ts index 06316f9..3778db8 100644 --- a/src/types/strategy/strategy-generics.ts +++ b/src/types/strategy/strategy-generics.ts @@ -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 { hidden: boolean; order?: number; + stack_count?: number; } /** * Strategy Configuration. * - * @property {Object.} areas - List of areas. - * @property {Object.} card_options - Card options for entities. + * @property {Object.} areas - The configuration of areas. + * @property {Object.} 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.} 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} 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; 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 & { + 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. * diff --git a/src/utilities/cardStacking.ts b/src/utilities/cardStacking.ts index 4b0d19b..61b462f 100644 --- a/src/utilities/cardStacking.ts +++ b/src/utilities/cardStacking.ts @@ -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)); diff --git a/src/views/AbstractView.ts b/src/views/AbstractView.ts index 8624889..902f9e5 100644 --- a/src/views/AbstractView.ts +++ b/src/views/AbstractView.ts @@ -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 : {} diff --git a/src/views/HomeView.ts b/src/views/HomeView.ts index 9989b24..026e3de 100644 --- a/src/views/HomeView.ts +++ b/src/views/HomeView.ts @@ -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 { 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], + }), }; } }