diff --git a/src/Registry.ts b/src/Registry.ts index 1024112..d2471ce 100644 --- a/src/Registry.ts +++ b/src/Registry.ts @@ -116,7 +116,7 @@ class Registry { const { ConfigurationDefaults } = await import('./configurationDefaults'); try { - Registry._strategyOptions = deepmerge(ConfigurationDefaults, info.config?.strategy?.options ?? {}); + Registry._strategyOptions = deepmerge(ConfigurationDefaults, info.config.strategy.options ?? {}); } catch (e) { logMessage(lvlFatal, 'Error importing strategy options!', e); } @@ -189,7 +189,6 @@ class Registry { Registry.strategyOptions.areas.undisclosed.type = 'default'; // Remove hidden areas if configured as so and sort them by name. - Registry._areas = new RegistryFilter(Registry.areas).isNotHidden().orderBy(['order', 'name'], 'asc').toList(); } @@ -200,7 +199,7 @@ class Registry { Registry.strategyOptions.views = Object.fromEntries( entries.sort(([_, a], [__, b]) => { return (a.order ?? Infinity) - (b.order ?? Infinity) || (a.title ?? '').localeCompare(b.title ?? ''); - }), + }) ) as Record; }; @@ -216,7 +215,7 @@ class Registry { } return 0; // Maintain the original order when none or only one item is sortable. - }), + }) ) as { [K in SupportedDomains]: K extends '_' ? AllDomainsConfig : SingleDomainConfig }; }; @@ -256,7 +255,7 @@ class Registry { .whereDomain(domain) .where((entity) => !entity.entity_id.endsWith('_stateful_scene') && entity.platform !== 'group') .toList() - .map((entity) => `states['${entity.entity_id}']`), + .map((entity) => `states['${entity.entity_id}']`) ); // noinspection SpellCheckingInspection @@ -273,15 +272,15 @@ class Registry { /** * Get the names of the specified type which aren't set to hidden in the strategy options. * - * @param {string} type The type of options to filter ("domain", "view", "chip"). + * @param {string} type The type of options to filter ("domain", "view", "badge"). * * @returns {string[]} For domains and views: names of items that aren't hidden. - * For chips: names of items that are explicitly set to true. + * For badges: names of items that are explicitly set to true. */ - static getExposedNames(type: 'domain' | 'view' | 'chip'): string[] { - // TODO: Align chip with other types. - if (type === 'chip') { - return Object.entries(Registry.strategyOptions.chips) + static getExposedNames(type: 'domain' | 'view' | 'badge'): string[] { + // TODO: Align badge with other types. + if (type === 'badge') { + return Object.entries(Registry.strategyOptions.badges) .filter(([_, value]) => value === true) .map(([key]) => key.split('_')[0]); } diff --git a/src/chips/AbstractChip.ts b/src/badges/AbstractBadge.ts similarity index 53% rename from src/chips/AbstractChip.ts rename to src/badges/AbstractBadge.ts index d330508..e395f8f 100644 --- a/src/chips/AbstractChip.ts +++ b/src/badges/AbstractBadge.ts @@ -1,25 +1,24 @@ import { Registry } from '../Registry'; -import { LovelaceChipConfig } from '../types/lovelace-mushroom/utils/lovelace/chip/types'; import { logMessage, lvlFatal } from '../utilities/debug'; +import { LovelaceBadgeConfig } from '../types/homeassistant/data/lovelace/config/badge'; -abstract class AbstractChip { +abstract class AbstractBadge { /** - * Abstract Chip class. + * Abstract Badge class. * - * To create a chip configuration, this class should be extended by a child class. - * Child classes should override the default configuration so the chip correctly reflects the entity. + * To create a badge configuration, this class should be extended by a child class. + * Child classes should override the default configuration so the badge correctly reflects the entity. * * @remarks * Before using this class, the Registry module must be initialized by calling {@link Registry.initialize}. */ /** - * Configuration of the chip. + * Configuration of the badge. * - * Child classes should override this property to reflect their own card type and options. + * Child classes should override this property to reflect their own badge type and options. */ - protected configuration: LovelaceChipConfig = { - // TODO: Check if this is correct vs custom:mushroom-template-badge. Also in child classes. + protected configuration: LovelaceBadgeConfig = { type: 'template', }; @@ -31,18 +30,18 @@ abstract class AbstractChip { */ protected constructor() { if (!Registry.initialized) { - logMessage(lvlFatal, 'Registry not initialized!'); + logMessage(lvlFatal, 'Registry is not initialized!'); } } /** - * Get a chip configuration. + * Get a badge configuration. * - * The configuration should be set by any of the child classes so the chip correctly reflects an entity. + * The configuration should be set by any of the child classes so the badge correctly reflects an entity. */ - getChipConfiguration(): LovelaceChipConfig { + getConfiguration(): LovelaceBadgeConfig { return this.configuration; } } -export default AbstractChip; +export default AbstractBadge; diff --git a/src/badges/ClimateBadge.ts b/src/badges/ClimateBadge.ts new file mode 100644 index 0000000..421b729 --- /dev/null +++ b/src/badges/ClimateBadge.ts @@ -0,0 +1,51 @@ +// noinspection JSUnusedGlobalSymbols Class is dynamically imported. + +import AbstractBadge from './AbstractBadge'; +import { Registry } from '../Registry'; +import { LovelaceBadgeConfig } from '../types/homeassistant/data/lovelace/config/badge'; + +/** + * Climate Badge class. + * + * Used to create a badge configuration to indicate how many climates are operating. + */ +class ClimateBadge extends AbstractBadge { + /** + * Class Constructor. + * + * @param {LovelaceBadgeConfig} [customConfiguration] Custom badge configuration. + */ + constructor(customConfiguration?: LovelaceBadgeConfig) { + super(); + + this.configuration = { ...this.configuration, ...ClimateBadge.getDefaultConfig(), ...customConfiguration }; + } + + /** Returns the default configuration object for the badge. */ + static getDefaultConfig(): LovelaceBadgeConfig { + return { + type: 'custom:mushroom-template-badge', + icon: 'mdi:thermostat', + color: 'orange', + content: Registry.getCountTemplate('climate', 'ne', 'off'), + /* ` + 🔄${Registry.getCountTemplate('climate', 'eq', 'auto')} + ↕️❄️${Registry.getCountTemplate('climate', 'eq', 'heat_cool')} + 🔥${Registry.getCountTemplate('climate', 'eq', 'heat')} + ❄️${Registry.getCountTemplate('climate', 'eq', 'cool')} + 💧${Registry.getCountTemplate('climate', 'eq', 'dry')} + 💨${Registry.getCountTemplate('climate', 'eq', 'fan_only')} + ⭕${Registry.getCountTemplate('climate', 'eq', 'off')} + `,*/ + tap_action: { + action: 'none', + }, + hold_action: { + action: 'navigate', + navigation_path: 'climates', + }, + }; + } +} + +export default ClimateBadge; diff --git a/src/badges/CoverBadge.ts b/src/badges/CoverBadge.ts new file mode 100644 index 0000000..6bcb8f4 --- /dev/null +++ b/src/badges/CoverBadge.ts @@ -0,0 +1,42 @@ +// noinspection JSUnusedGlobalSymbols Class is dynamically imported. + +import { Registry } from '../Registry'; +import AbstractBadge from './AbstractBadge'; +import { LovelaceBadgeConfig } from '../types/homeassistant/data/lovelace/config/badge'; + +/** + * Cover Badge class. + * + * Used to create a badge configuration to indicate how many covers aren't closed. + */ +class CoverBadge extends AbstractBadge { + /** + * Class Constructor. + * + * @param {LovelaceBadgeConfig} [customConfiguration] Custom badge configuration. + */ + constructor(customConfiguration?: LovelaceBadgeConfig) { + super(); + + this.configuration = { ...this.configuration, ...CoverBadge.getDefaultConfig(), ...customConfiguration }; + } + + /** Returns the default configuration object for the badge. */ + static getDefaultConfig(): LovelaceBadgeConfig { + return { + type: 'custom:mushroom-template-badge', + icon: 'mdi:window-open', + color: 'cyan', + content: Registry.getCountTemplate('cover', 'search', '(open|opening|closing)'), + tap_action: { + action: 'none', + }, + hold_action: { + action: 'navigate', + navigation_path: 'covers', + }, + }; + } +} + +export default CoverBadge; diff --git a/src/badges/FanBadge.ts b/src/badges/FanBadge.ts new file mode 100644 index 0000000..b767872 --- /dev/null +++ b/src/badges/FanBadge.ts @@ -0,0 +1,49 @@ +// noinspection JSUnusedGlobalSymbols Class is dynamically imported. + +import { Registry } from '../Registry'; +import RegistryFilter from '../utilities/RegistryFilter'; +import AbstractBadge from './AbstractBadge'; +import { LovelaceBadgeConfig } from '../types/homeassistant/data/lovelace/config/badge'; + +/** + * Fan Badge class. + * + * Used to create a badge to indicate how many fans are on and to switch them all off. + */ +class FanBadge extends AbstractBadge { + /** + * Class Constructor. + * + * @param {LovelaceBadgeConfig} [customConfiguration] Custom badge configuration. + */ + constructor(customConfiguration?: LovelaceBadgeConfig) { + super(); + + this.configuration = { ...this.configuration, ...FanBadge.getDefaultConfig(), ...customConfiguration }; + } + + /** Returns the default configuration object for the badge. */ + static getDefaultConfig(): LovelaceBadgeConfig { + return { + type: 'custom:mushroom-template-badge', + icon: 'mdi:fan', + color: 'green', + content: Registry.getCountTemplate('fan', 'eq', 'on'), + tap_action: { + action: 'perform-action', + perform_action: 'fan.turn_off', + target: { + entity_id: new RegistryFilter(Registry.entities) + .whereDomain('fan') + .getValuesByProperty('entity_id') as string[], + }, + }, + hold_action: { + action: 'navigate', + navigation_path: 'fans', + }, + }; + } +} + +export default FanBadge; diff --git a/src/badges/LightBadge.ts b/src/badges/LightBadge.ts new file mode 100644 index 0000000..8788d71 --- /dev/null +++ b/src/badges/LightBadge.ts @@ -0,0 +1,49 @@ +// noinspection JSUnusedGlobalSymbols Class is dynamically imported. + +import { Registry } from '../Registry'; +import RegistryFilter from '../utilities/RegistryFilter'; +import AbstractBadge from './AbstractBadge'; +import { LovelaceBadgeConfig } from '../types/homeassistant/data/lovelace/config/badge'; + +/** + * Light Badge class. + * + * Used to create a badge configuration to indicate how many lights are on and to switch them all off. + */ +class LightBadge extends AbstractBadge { + /** + * Class Constructor. + * + * @param {LovelaceBadgeConfig} [customConfiguration] Custom badge configuration. + */ + constructor(customConfiguration?: LovelaceBadgeConfig) { + super(); + + this.configuration = { ...this.configuration, ...LightBadge.getDefaultConfig(), ...customConfiguration }; + } + + /** Returns the default configuration object for the badge. */ + static getDefaultConfig(): LovelaceBadgeConfig { + return { + type: 'custom:mushroom-template-badge', + icon: 'mdi:lightbulb-group', + color: 'amber', + content: Registry.getCountTemplate('light', 'eq', 'on'), + tap_action: { + action: 'perform-action', + perform_action: 'light.turn_off', + target: { + entity_id: new RegistryFilter(Registry.entities) + .whereDomain('light') + .getValuesByProperty('entity_id') as string[], + }, + }, + hold_action: { + action: 'navigate', + navigation_path: 'lights', + }, + }; + } +} + +export default LightBadge; diff --git a/src/badges/SwitchBadge.ts b/src/badges/SwitchBadge.ts new file mode 100644 index 0000000..4febf3c --- /dev/null +++ b/src/badges/SwitchBadge.ts @@ -0,0 +1,49 @@ +// noinspection JSUnusedGlobalSymbols Class is dynamically imported. + +import { Registry } from '../Registry'; +import RegistryFilter from '../utilities/RegistryFilter'; +import AbstractBadge from './AbstractBadge'; +import { LovelaceBadgeConfig } from '../types/homeassistant/data/lovelace/config/badge'; + +/** + * Switch Badge class. + * + * Used to create a badge configuration to indicate how many switches are on and to switch them all off. + */ +class SwitchBadge extends AbstractBadge { + /** + * Class Constructor. + * + * @param {LovelaceBadgeConfig} [customConfiguration] Custom badge configuration. + */ + constructor(customConfiguration?: LovelaceBadgeConfig) { + super(); + + this.configuration = { ...this.configuration, ...SwitchBadge.getDefaultConfig(), ...customConfiguration }; + } + + /** Returns the default configuration object for the badge. */ + static getDefaultConfig(): LovelaceBadgeConfig { + return { + type: 'custom:mushroom-template-badge', + icon: 'mdi:dip-switch', + color: 'blue', + content: Registry.getCountTemplate('switch', 'eq', 'on'), + tap_action: { + action: 'perform-action', + perform_action: 'switch.turn_off', + target: { + entity_id: new RegistryFilter(Registry.entities) + .whereDomain('switch') + .getValuesByProperty('entity_id') as string[], + }, + }, + hold_action: { + action: 'navigate', + navigation_path: 'switches', + }, + }; + } +} + +export default SwitchBadge; diff --git a/src/badges/WeatherBadge.ts b/src/badges/WeatherBadge.ts new file mode 100644 index 0000000..99f5786 --- /dev/null +++ b/src/badges/WeatherBadge.ts @@ -0,0 +1,35 @@ +// noinspection JSUnusedGlobalSymbols False positive. + +import { LovelaceBadgeConfig } from '../types/homeassistant/data/lovelace/config/badge'; +import { EntityBadgeConfig } from '../types/homeassistant/panels/lovelace/badges/types'; +import AbstractBadge from './AbstractBadge'; + +/** + * Weather Badge class. + * + * Used to create a badge configuration to indicate the current weather. + */ +class WeatherBadge extends AbstractBadge { + /** + * Class Constructor. + * + * @param {string} entityId Id of a weather entity. + * @param {LovelaceBadgeConfig} [customConfiguration] Custom badge configuration. + */ + constructor(entityId: string, customConfiguration?: LovelaceBadgeConfig) { + super(); + + this.configuration = { ...this.configuration, ...WeatherBadge.getDefaultConfig(entityId), ...customConfiguration }; + } + + /** Returns the default configuration object for the badge. */ + static getDefaultConfig(entityId: string): EntityBadgeConfig { + return { + type: 'entity', + entity: entityId, + state_content: ['state', 'temperature'], + }; + } +} + +export default WeatherBadge; diff --git a/src/cards/AbstractCard.ts b/src/cards/AbstractCard.ts index 606d512..88927d3 100644 --- a/src/cards/AbstractCard.ts +++ b/src/cards/AbstractCard.ts @@ -37,7 +37,7 @@ abstract class AbstractCard { */ protected constructor(entity: RegistryEntry) { if (!Registry.initialized) { - logMessage(lvlFatal, 'Registry not initialized!'); + logMessage(lvlFatal, 'Registry is not initialized!'); } this.entity = entity; diff --git a/src/chips/ClimateChip.ts b/src/chips/ClimateChip.ts deleted file mode 100644 index fb1b4df..0000000 --- a/src/chips/ClimateChip.ts +++ /dev/null @@ -1,42 +0,0 @@ -// noinspection JSUnusedGlobalSymbols Class is dynamically imported. - -import { Registry } from '../Registry'; -import { TemplateChipConfig } from '../types/lovelace-mushroom/utils/lovelace/chip/types'; -import AbstractChip from './AbstractChip'; - -/** - * Climate Chip class. - * - * Used to create a chip configuration to indicate how many climates are operating. - */ -class ClimateChip extends AbstractChip { - /** Returns the default configuration object for the chip. */ - static getDefaultConfig(): TemplateChipConfig { - return { - type: 'template', - icon: 'mdi:thermostat', - icon_color: 'orange', - content: Registry.getCountTemplate('climate', 'ne', 'off'), - tap_action: { - action: 'none', - }, - hold_action: { - action: 'navigate', - navigation_path: 'climates', - }, - }; - } - - /** - * Class Constructor. - * - * @param {TemplateChipConfig} [customConfiguration] Custom chip configuration. - */ - constructor(customConfiguration?: TemplateChipConfig) { - super(); - - this.configuration = { ...this.configuration, ...ClimateChip.getDefaultConfig(), ...customConfiguration }; - } -} - -export default ClimateChip; diff --git a/src/chips/CoverChip.ts b/src/chips/CoverChip.ts deleted file mode 100644 index 96b1f4c..0000000 --- a/src/chips/CoverChip.ts +++ /dev/null @@ -1,42 +0,0 @@ -// noinspection JSUnusedGlobalSymbols Class is dynamically imported. - -import { Registry } from '../Registry'; -import { TemplateChipConfig } from '../types/lovelace-mushroom/utils/lovelace/chip/types'; -import AbstractChip from './AbstractChip'; - -/** - * Cover Chip class. - * - * Used to create a chip configuration to indicate how many covers aren't closed. - */ -class CoverChip extends AbstractChip { - /** Returns the default configuration object for the chip. */ - static getDefaultConfig(): TemplateChipConfig { - return { - type: 'template', - icon: 'mdi:window-open', - icon_color: 'cyan', - content: Registry.getCountTemplate('cover', 'search', '(open|opening|closing)'), - tap_action: { - action: 'none', - }, - hold_action: { - action: 'navigate', - navigation_path: 'covers', - }, - }; - } - - /** - * Class Constructor. - * - * @param {TemplateChipConfig} [customConfiguration] Custom chip configuration. - */ - constructor(customConfiguration?: TemplateChipConfig) { - super(); - - this.configuration = { ...this.configuration, ...CoverChip.getDefaultConfig(), ...customConfiguration }; - } -} - -export default CoverChip; diff --git a/src/chips/FanChip.ts b/src/chips/FanChip.ts deleted file mode 100644 index bfcc616..0000000 --- a/src/chips/FanChip.ts +++ /dev/null @@ -1,49 +0,0 @@ -// noinspection JSUnusedGlobalSymbols Class is dynamically imported. - -import { Registry } from '../Registry'; -import { TemplateChipConfig } from '../types/lovelace-mushroom/utils/lovelace/chip/types'; -import AbstractChip from './AbstractChip'; -import RegistryFilter from '../utilities/RegistryFilter'; - -/** - * Fan Chip class. - * - * Used to create a chip to indicate how many fans are on and to switch them all off. - */ -class FanChip extends AbstractChip { - /** Returns the default configuration object for the chip. */ - static getDefaultConfig(): TemplateChipConfig { - return { - type: 'template', - icon: 'mdi:fan', - icon_color: 'green', - content: Registry.getCountTemplate('fan', 'eq', 'on'), - tap_action: { - action: 'perform-action', - perform_action: 'fan.turn_off', - target: { - entity_id: new RegistryFilter(Registry.entities) - .whereDomain('fan') - .getValuesByProperty('entity_id') as string[], - }, - }, - hold_action: { - action: 'navigate', - navigation_path: 'fans', - }, - }; - } - - /** - * Class Constructor. - * - * @param {TemplateChipConfig} [customConfiguration] Custom chip configuration. - */ - constructor(customConfiguration?: TemplateChipConfig) { - super(); - - this.configuration = { ...this.configuration, ...FanChip.getDefaultConfig(), ...customConfiguration }; - } -} - -export default FanChip; diff --git a/src/chips/LightChip.ts b/src/chips/LightChip.ts deleted file mode 100644 index b70e0e1..0000000 --- a/src/chips/LightChip.ts +++ /dev/null @@ -1,49 +0,0 @@ -// noinspection JSUnusedGlobalSymbols Class is dynamically imported. - -import { Registry } from '../Registry'; -import { TemplateChipConfig } from '../types/lovelace-mushroom/utils/lovelace/chip/types'; -import AbstractChip from './AbstractChip'; -import RegistryFilter from '../utilities/RegistryFilter'; - -/** - * Light Chip class. - * - * Used to create a chip configuration to indicate how many lights are on and to switch them all off. - */ -class LightChip extends AbstractChip { - /** Returns the default configuration object for the chip. */ - static getDefaultConfig(): TemplateChipConfig { - return { - type: 'template', - icon: 'mdi:lightbulb-group', - icon_color: 'amber', - content: Registry.getCountTemplate('light', 'eq', 'on'), - tap_action: { - action: 'perform-action', - perform_action: 'light.turn_off', - target: { - entity_id: new RegistryFilter(Registry.entities) - .whereDomain('light') - .getValuesByProperty('entity_id') as string[], - }, - }, - hold_action: { - action: 'navigate', - navigation_path: 'lights', - }, - }; - } - - /** - * Class Constructor. - * - * @param {TemplateChipConfig} [customConfiguration] Custom chip configuration. - */ - constructor(customConfiguration?: TemplateChipConfig) { - super(); - - this.configuration = { ...this.configuration, ...LightChip.getDefaultConfig(), ...customConfiguration }; - } -} - -export default LightChip; diff --git a/src/chips/SwitchChip.ts b/src/chips/SwitchChip.ts deleted file mode 100644 index 7e341d1..0000000 --- a/src/chips/SwitchChip.ts +++ /dev/null @@ -1,49 +0,0 @@ -// noinspection JSUnusedGlobalSymbols Class is dynamically imported. - -import { Registry } from '../Registry'; -import { TemplateChipConfig } from '../types/lovelace-mushroom/utils/lovelace/chip/types'; -import AbstractChip from './AbstractChip'; -import RegistryFilter from '../utilities/RegistryFilter'; - -/** - * Switch Chip class. - * - * Used to create a chip configuration to indicate how many switches are on and to switch them all off. - */ -class SwitchChip extends AbstractChip { - /** Returns the default configuration object for the chip. */ - static getDefaultConfig(): TemplateChipConfig { - return { - type: 'template', - icon: 'mdi:dip-switch', - icon_color: 'blue', - content: Registry.getCountTemplate('switch', 'eq', 'on'), - tap_action: { - action: 'perform-action', - perform_action: 'switch.turn_off', - target: { - entity_id: new RegistryFilter(Registry.entities) - .whereDomain('switch') - .getValuesByProperty('entity_id') as string[], - }, - }, - hold_action: { - action: 'navigate', - navigation_path: 'switches', - }, - }; - } - - /** - * Class Constructor. - * - * @param {TemplateChipConfig} [customConfiguration] Custom chip configuration. - */ - constructor(customConfiguration?: TemplateChipConfig) { - super(); - - this.configuration = { ...this.configuration, ...SwitchChip.getDefaultConfig(), ...customConfiguration }; - } -} - -export default SwitchChip; diff --git a/src/chips/WeatherChip.ts b/src/chips/WeatherChip.ts deleted file mode 100644 index 1cb5490..0000000 --- a/src/chips/WeatherChip.ts +++ /dev/null @@ -1,35 +0,0 @@ -// noinspection JSUnusedGlobalSymbols False positive. - -import { WeatherChipConfig } from '../types/lovelace-mushroom/utils/lovelace/chip/types'; -import AbstractChip from './AbstractChip'; - -/** - * Weather Chip class. - * - * Used to create a chip configuration to indicate the current weather. - */ -class WeatherChip extends AbstractChip { - /** Returns the default configuration object for the chip. */ - static getDefaultConfig(entityId: string): WeatherChipConfig { - return { - type: 'weather', - entity: entityId, - show_temperature: true, - show_conditions: true, - }; - } - - /** - * Class Constructor. - * - * @param {string} entityId Id of a weather entity. - * @param {WeatherChipConfig} [customConfiguration] Custom chip configuration. - */ - constructor(entityId: string, customConfiguration?: WeatherChipConfig) { - super(); - - this.configuration = { ...this.configuration, ...WeatherChip.getDefaultConfig(entityId), ...customConfiguration }; - } -} - -export default WeatherChip; diff --git a/src/configurationDefaults.ts b/src/configurationDefaults.ts index 2cafeb3..174115d 100644 --- a/src/configurationDefaults.ts +++ b/src/configurationDefaults.ts @@ -27,15 +27,15 @@ export const ConfigurationDefaults: StrategyDefaults = { }, }, card_options: {}, - chips: { - // TODO: Make chips sortable. + badges: { + // TODO: Make badges sortable. weather_entity: 'auto', light_count: true, fan_count: true, cover_count: true, switch_count: true, climate_count: true, - extra_chips: [], + extra_badges: [], }, debug: false, domains: { diff --git a/src/types/homeassistant/panels/lovelace/badges/types.ts b/src/types/homeassistant/panels/lovelace/badges/types.ts new file mode 100644 index 0000000..3844434 --- /dev/null +++ b/src/types/homeassistant/panels/lovelace/badges/types.ts @@ -0,0 +1,22 @@ +import type { ActionConfig } from '../../../data/lovelace/config/action'; +import type { LovelaceBadgeConfig } from '../../../data/lovelace/config/badge'; + +export interface EntityBadgeConfig extends LovelaceBadgeConfig { + type: 'entity'; + entity?: string; + name?: string; + icon?: string; + color?: string; + show_name?: boolean; + show_state?: boolean; + show_icon?: boolean; + show_entity_picture?: boolean; + state_content?: string | string[]; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; + /** + * @deprecated use `show_state`, `show_name`, `icon_type` + */ + display_type?: string; +} diff --git a/src/types/lovelace-mushroom/cards/chips-card.ts b/src/types/lovelace-mushroom/cards/chips-card.ts index 5e42c67..14c0d26 100644 --- a/src/types/lovelace-mushroom/cards/chips-card.ts +++ b/src/types/lovelace-mushroom/cards/chips-card.ts @@ -1,15 +1,15 @@ import { LovelaceCardConfig } from '../../homeassistant/data/lovelace/config/card'; -import { LovelaceChipConfig } from '../utils/lovelace/chip/types'; +import { MushroomChipConfig } from '../utils/lovelace/chip/types'; /** * Chips Card Configuration * - * @property {LovelaceChipConfig[]} chips - Array of chips to display. + * @property {MushroomChipConfig[]} chips - Array of chips to display. * @property {string} [alignment] - Chips alignment (start, end, center, justify). Defaults to 'start'. * * @see https://github.com/piitaya/lovelace-mushroom/blob/main/docs/cards/chips.md */ export interface ChipsCardConfig extends LovelaceCardConfig { - chips: LovelaceChipConfig[]; + chips: MushroomChipConfig[]; alignment?: string; } diff --git a/src/types/lovelace-mushroom/utils/lovelace/chip/types.ts b/src/types/lovelace-mushroom/utils/lovelace/chip/types.ts index 4dc3a0f..460bc7e 100644 --- a/src/types/lovelace-mushroom/utils/lovelace/chip/types.ts +++ b/src/types/lovelace-mushroom/utils/lovelace/chip/types.ts @@ -146,12 +146,12 @@ export type TemplateChipConfig = { * Conditional Chip Config * * @property {"conditional"} type - Type of the chip. - * @property {LovelaceChipConfig} [chip] - A chip configuration. + * @property {MushroomChipConfig} [chip] - A chip configuration. * @property {[]} conditions - Conditions for the chip. */ export type ConditionalChipConfig = { type: 'conditional'; - chip?: LovelaceChipConfig; + chip?: MushroomChipConfig; conditions: any[]; }; @@ -189,8 +189,8 @@ export type SpacerChipConfig = { type: 'spacer'; }; -/** Lovelace Chip Config */ -export type LovelaceChipConfig = +/** Mushroom Chip Config */ +export type MushroomChipConfig = | ActionChipConfig | AlarmControlPanelChipConfig | BackChipConfig diff --git a/src/types/strategy/strategy-generics.ts b/src/types/strategy/strategy-generics.ts index e5ebf56..7f4a782 100644 --- a/src/types/strategy/strategy-generics.ts +++ b/src/types/strategy/strategy-generics.ts @@ -5,9 +5,9 @@ import { LovelaceCardConfig } from '../homeassistant/data/lovelace/config/card'; import { LovelaceConfig } from '../homeassistant/data/lovelace/config/types'; import { LovelaceViewConfig, LovelaceViewRawConfig } from '../homeassistant/data/lovelace/config/view'; 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'; +import { LovelaceBadgeConfig } from '../homeassistant/data/lovelace/config/badge'; /** * List of supported domains. @@ -60,11 +60,11 @@ const SUPPORTED_VIEWS = [ ] as const; /** - * List of supported chips. + * List of supported badges. * - * This constant array defines the chips that are supported by the strategy. + * This constant array defines the badges that are supported by the strategy. */ -const SUPPORTED_CHIPS = ['light', 'fan', 'cover', 'switch', 'climate', 'weather'] as const; +const SUPPORTED_BADGES = ['light', 'fan', 'cover', 'switch', 'climate', 'weather'] as const; /** * List of home view sections. @@ -72,11 +72,11 @@ 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; +const HOME_VIEW_SECTIONS = ['areas', 'areasTitle', 'badges', 'greeting', 'persons'] as const; export type SupportedDomains = (typeof SUPPORTED_DOMAINS)[number]; export type SupportedViews = (typeof SUPPORTED_VIEWS)[number]; -export type SupportedChips = (typeof SUPPORTED_CHIPS)[number]; +export type SupportedBadges = (typeof SUPPORTED_BADGES)[number]; export type HomeViewSections = (typeof HOME_VIEW_SECTIONS)[number]; /** @@ -205,7 +205,7 @@ export interface SingleDomainConfig extends Partial { * * @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 {BadgeConfiguration} badges - The configuration of badges 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. @@ -218,7 +218,7 @@ export interface SingleDomainConfig extends Partial { export interface StrategyConfig { areas: { [S: string]: StrategyArea }; card_options: { [S: string]: CustomCardConfig }; - chips: ChipConfiguration; + badges: BadgeConfiguration; debug: boolean; domains: { [K in SupportedDomains]: K extends '_' ? AllDomainsConfig : SingleDomainConfig }; extra_cards: LovelaceCardConfig[]; @@ -267,21 +267,21 @@ export interface AllAreasConfig { } /** - * A list of chips to show in the Home view. + * A list of badges to show in the Home view. * - * @property {boolean} climate_count - Chip to display the number of climates which are not off. - * @property {boolean} cover_count - Chip to display the number of unclosed covers. - * @property {LovelaceChipConfig[] | []} extra_chips - List of extra chips. - * @property {boolean} fan_count - Chip to display the number of fans on. - * @property {boolean} light_count - Chip to display the number of lights on. - * @property {boolean} switch_count - Chip to display the number of switches on. - * @property {'auto' | `weather.${string}`} weather_entity - Entity id for the weather chip to use. + * @property {boolean} climate_count - Badge to display the number of climates which are not off. + * @property {boolean} cover_count - Badge to display the number of unclosed covers. + * @property {LovelaceBadgeConfig[] | []} extra_badges - List of extra badges. + * @property {boolean} fan_count - Badge to display the number of fans on. + * @property {boolean} light_count - Badge to display the number of lights on. + * @property {boolean} switch_count - Badge to display the number of switches on. + * @property {'auto' | `weather.${string}`} weather_entity - Entity id for the weather badges to use. * Accepts `weather.` ids or `auto` only. */ -export interface ChipConfiguration { +export interface BadgeConfiguration { climate_count: boolean; cover_count: boolean; - extra_chips: LovelaceChipConfig[] | []; + extra_badges: LovelaceBadgeConfig[] | []; fan_count: boolean; light_count: boolean; switch_count: boolean; @@ -361,11 +361,11 @@ export function isSupportedDomain(id: string): id is SupportedDomains { } /** - * Type guard to check if the strategy supports a given chip identifier. + * Type guard to check if the strategy supports a given badge identifier. * - * @param {string} id - The chip identifier to check (e.g., "light", "climate", "weather"). - * @returns {boolean} - True if the identifier represents a supported chip type. + * @param {string} id - The badge identifier to check (e.g., "light", "climate", "weather"). + * @returns {boolean} - True if the identifier represents a supported badge type. */ -export function isSupportedChip(id: string): id is SupportedChips { - return isInSupportedList(id, SUPPORTED_CHIPS); +export function isSupportedBadge(id: string): id is SupportedBadges { + return isInSupportedList(id, SUPPORTED_BADGES); } diff --git a/src/utilities/NoticeManager.ts b/src/utilities/NoticeManager.ts new file mode 100644 index 0000000..9e88498 --- /dev/null +++ b/src/utilities/NoticeManager.ts @@ -0,0 +1,117 @@ +import { HomeAssistant } from '../types/homeassistant/types'; + +interface NoticeOptions { + title?: string; + storageKey?: string; + version?: string; + notificationId?: string; +} + +interface StoredNotice { + shown: boolean; + timestamp: string; + version: string; +} + +export class NoticeManager { + private static readonly DEFAULT_NAMESPACE = 'mushroom_strategy_notice'; + private static readonly DEFAULT_TITLE = 'Deprecation Notice'; + + constructor( + private readonly hass: HomeAssistant, + private readonly options: { namespace?: string } = {} + ) {} + + /** + * Shows a deprecation notice if it hasn't been shown before. + */ + public async showDeprecationNotice(id: string, message: string, options: NoticeOptions = {}): Promise { + const storageKey = this.getStorageKey(id, options.storageKey); + const notificationId = options.notificationId || `mushroom_strategy_${id}`; + const title = options.title || NoticeManager.DEFAULT_TITLE; + + try { + // Check if notice was already shown + if (this.hasBeenShownSync(storageKey)) { + return; // Notice was already shown + } + + // Show persistent notification + await this.hass.callService('persistent_notification', 'create', { + title: title, + message: message, + notification_id: notificationId, + }); + + // Mark as shown + this.markAsShownSync(storageKey, options.version || '1.0.0'); + } catch (error) { + console.error(`[NoticeManager] Failed to show deprecation notice '${id}':`, error); + // Fallback to console if service call fails + console.warn(`[${title}] ${message}`); + } + } + + /** + * Clears a previously shown notice. + */ + public async clearNotice(id: string, customKey?: string, notificationId?: string): Promise { + const storageKey = this.getStorageKey(id, customKey); + try { + // Clear from storage + localStorage.removeItem(storageKey); + + // Clear the notification if notificationId is provided + if (notificationId) { + await this.hass.callService('persistent_notification', 'dismiss', { + notification_id: notificationId, + }); + } + } catch (error) { + console.error(`[NoticeManager] Failed to clear notice '${id}':`, error); + } + } + + /** + * Checks if a notice has been shown before. + */ + public hasBeenShown(id: string, customKey?: string): boolean { + const storageKey = this.getStorageKey(id, customKey); + return this.hasBeenShownSync(storageKey); + } + + private hasBeenShownSync(storageKey: string): boolean { + try { + const stored = localStorage.getItem(storageKey); + if (!stored) { + return false; + } + + const notice = JSON.parse(stored) as StoredNotice; + return notice.shown; + } catch { + return false; + } + } + + private getStorageKey(id: string, customKey?: string): string { + if (customKey) { + return customKey; + } + const namespace = this.options.namespace || NoticeManager.DEFAULT_NAMESPACE; + return `${namespace}_${id}`; + } + + private markAsShownSync(storageKey: string, version: string): void { + const notice: StoredNotice = { + shown: true, + timestamp: new Date().toISOString(), + version, + }; + try { + localStorage.setItem(storageKey, JSON.stringify(notice)); + } catch (error) { + console.error('[NoticeManager] Failed to save notice state:', error); + } + } +} diff --git a/src/views/HomeView.ts b/src/views/HomeView.ts index ca8a8f3..5e4f20e 100644 --- a/src/views/HomeView.ts +++ b/src/views/HomeView.ts @@ -4,11 +4,9 @@ import { Registry } from '../Registry'; import { ActionConfig } from '../types/homeassistant/data/lovelace/config/action'; import { LovelaceCardConfig } from '../types/homeassistant/data/lovelace/config/card'; import { AreaCardConfig, StackCardConfig } from '../types/homeassistant/panels/lovelace/cards/types'; -import { ChipsCardConfig } from '../types/lovelace-mushroom/cards/chips-card'; import { PersonCardConfig } from '../types/lovelace-mushroom/cards/person-card-config'; import { TemplateCardConfig } from '../types/lovelace-mushroom/cards/template-card-config'; -import { LovelaceChipConfig } from '../types/lovelace-mushroom/utils/lovelace/chip/types'; -import { isSupportedChip } from '../types/strategy/strategy-generics'; +import { isSupportedBadge } from '../types/strategy/strategy-generics'; import { ViewConfig } from '../types/strategy/strategy-views'; import { sanitizeClassName } from '../utilities/auxiliaries'; import { logMessage, lvlError, lvlInfo } from '../utilities/debug'; @@ -16,6 +14,8 @@ import { localize } from '../utilities/localize'; import AbstractView from './AbstractView'; import registryFilter from '../utilities/RegistryFilter'; import { stackHorizontal } from '../utilities/cardStacking'; +import { LovelaceViewConfig } from '../types/homeassistant/data/lovelace/config/view'; +import { LovelaceBadgeConfig } from '../types/homeassistant/data/lovelace/config/badge'; /** * Home View Class. @@ -38,8 +38,15 @@ class HomeView extends AbstractView { } /** Returns the default configuration object for the view. */ + // TODO: Move type and max_columns to the abstract class. static getDefaultConfig(): ViewConfig { return { + //type: 'sections', + //max_columns: 4, + header: { + badges_position: 'top', + layout: 'center', + }, title: localize('generic.home'), icon: 'mdi:home-assistant', path: 'home', @@ -47,6 +54,19 @@ class HomeView extends AbstractView { }; } + /** + * Get a view configuration. + * + * The configuration includes the card configurations which are created by createCardConfigurations(). + */ + async getView(): Promise { + return { + ...this.baseConfiguration, + badges: await this.createBadgeSection(), + cards: await this.createCardConfigurations(), + }; + } + /** * Create the configuration of the cards to include in the view. * @@ -55,24 +75,16 @@ class HomeView extends AbstractView { async createCardConfigurations(): Promise { const homeViewCards: LovelaceCardConfig[] = []; - let chipsSection, personsSection, areasSection; + let personsSection, areasSection; try { - [chipsSection, personsSection, areasSection] = await Promise.all([ - this.createChipsSection(), - this.createPersonsSection(), - this.createAreasSection(), - ]); + [personsSection, areasSection] = await Promise.all([this.createPersonsSection(), this.createAreasSection()]); } catch (e) { logMessage(lvlError, 'Error importing created sections!', e); return homeViewCards; } - if (chipsSection) { - homeViewCards.push(chipsSection); - } - if (personsSection) { homeViewCards.push(personsSection); } @@ -125,71 +137,67 @@ class HomeView extends AbstractView { } /** - * Create a chip section to include in the view + * Create a badge section to include in the view. * * If the section is marked as hidden in the strategy option, then the section is not created. */ - private async createChipsSection(): Promise { - if (Registry.strategyOptions.home_view.hidden.includes('chips')) { + private async createBadgeSection(): Promise { + if (Registry.strategyOptions.home_view.hidden.includes('badges')) { // The section is hidden. return; } - const chipConfigurations: LovelaceChipConfig[] = []; - const exposedChips = Registry.getExposedNames('chip'); + const configurations: LovelaceBadgeConfig[] = []; + const exposedBadges = Registry.getExposedNames('badge'); - let Chip; + let Badge; - // Weather chip. - // FIXME: It's not possible to hide the weather chip in the configuration. + // Weather badge. + // FIXME: It's not possible to hide the weather badge in the configuration. const weatherEntityId = - Registry.strategyOptions.chips.weather_entity === 'auto' + Registry.strategyOptions.badges.weather_entity === 'auto' ? Registry.entities.find((entity) => entity.entity_id.startsWith('weather.'))?.entity_id - : Registry.strategyOptions.chips.weather_entity; + : Registry.strategyOptions.badges.weather_entity; if (weatherEntityId) { try { - Chip = (await import('../chips/WeatherChip')).default; - const weatherChip = new Chip(weatherEntityId); + Badge = (await import('../badges/WeatherBadge')).default; + const weatherBadge = new Badge(weatherEntityId); - chipConfigurations.push(weatherChip.getChipConfiguration()); + configurations.push(weatherBadge.getConfiguration()); } catch (e) { - logMessage(lvlError, 'Error importing chip weather!', e); + logMessage(lvlError, 'Error importing badge weather!', e); } } else { - logMessage(lvlInfo, 'Weather chip has no entities available.'); + logMessage(lvlInfo, 'Weather badge has no entities available.'); } - // Numeric chips. - for (const chipName of exposedChips) { - if (!isSupportedChip(chipName) || !new registryFilter(Registry.entities).whereDomain(chipName).count()) { - logMessage(lvlInfo, `Chip for domain ${chipName} is unsupported or has no entities available.`); + // Numeric badges. + for (const badgeName of exposedBadges) { + if (!isSupportedBadge(badgeName) || !new registryFilter(Registry.entities).whereDomain(badgeName).count()) { + logMessage(lvlInfo, `Badge for domain ${badgeName} is unsupported or has no entities available.`); continue; } - const moduleName = sanitizeClassName(chipName + 'Chip'); + const moduleName = sanitizeClassName(badgeName + 'Badge'); try { - Chip = (await import(`../chips/${moduleName}`)).default; - const currentChip = new Chip(); + Badge = (await import(`../badges/${moduleName}`)).default; + const currentBadge = new Badge(); - chipConfigurations.push(currentChip.getChipConfiguration()); + configurations.push(currentBadge.getConfiguration()); } catch (e) { - logMessage(lvlError, `Error importing chip ${chipName}!`, e); + logMessage(lvlError, `Error importing badge ${badgeName}!`, e); } } - // Add extra chips. - if (Registry.strategyOptions.chips?.extra_chips) { - chipConfigurations.push(...Registry.strategyOptions.chips.extra_chips); + // Add extra badges. + if (Registry.strategyOptions.badges?.extra_badges) { + configurations.push(...Registry.strategyOptions.badges.extra_badges); } - return { - type: 'custom:mushroom-chips-card', - alignment: 'center', - chips: chipConfigurations, - }; + return configurations; } /** @@ -210,15 +218,14 @@ class HomeView extends AbstractView { cardConfigurations.push( ...Registry.entities .filter((entity) => entity.entity_id.startsWith('person.')) - .map((person) => new PersonCard(person).getCard()), + .map((person) => new PersonCard(person).getCard()) ); return { type: 'vertical-stack', cards: stackHorizontal( cardConfigurations, - Registry.strategyOptions.home_view.stack_count['persons'] ?? - Registry.strategyOptions.home_view.stack_count['_'], + Registry.strategyOptions.home_view.stack_count['persons'] ?? Registry.strategyOptions.home_view.stack_count['_'] ), }; } @@ -258,7 +265,7 @@ class HomeView extends AbstractView { new AreaCard(area, { ...Registry.strategyOptions.areas['_'], ...Registry.strategyOptions.areas[area.area_id], - }).getCard(), + }).getCard() ); }