Refactor chips to badges

This commit is contained in:
DigiLive
2025-06-13 09:28:51 +02:00
parent e6e52a1389
commit 43173aa3af
22 changed files with 527 additions and 374 deletions

View File

@@ -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<SupportedViews, StrategyViewConfig>;
};
@@ -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]);
}

View File

@@ -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;

View File

@@ -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;

42
src/badges/CoverBadge.ts Normal file
View File

@@ -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;

49
src/badges/FanBadge.ts Normal file
View File

@@ -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;

49
src/badges/LightBadge.ts Normal file
View File

@@ -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;

49
src/badges/SwitchBadge.ts Normal file
View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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: {

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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<StrategyHeaderCardConfig> {
*
* @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 {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.<string, AllDomainsConfig | SingleDomainConfig>} 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<StrategyHeaderCardConfig> {
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);
}

View File

@@ -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<void> {
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<void> {
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);
}
}
}

View File

@@ -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<LovelaceViewConfig> {
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<LovelaceCardConfig[]> {
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<ChipsCardConfig | undefined> {
if (Registry.strategyOptions.home_view.hidden.includes('chips')) {
private async createBadgeSection(): Promise<LovelaceBadgeConfig[] | undefined> {
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()
);
}