From f2ecdfe56274707c6a64258046d669542a99ae8f Mon Sep 17 00:00:00 2001 From: DigiLive Date: Sat, 10 May 2025 09:05:24 +0200 Subject: [PATCH] Add Device View Entities of the same device can now be grouped under a separate view instead of showing them all on a domain or area view. If grouped, by a device id, a device card is shown instead which can be tapped to view its entities. --- src/Registry.ts | 95 +++++-- src/cards/AbstractCard.ts | 3 +- src/cards/DeviceCard.ts | 69 +++++ src/cards/FanCard.ts | 6 +- src/cards/HeaderCard.ts | 52 ++-- src/cards/LightCard.ts | 2 +- src/cards/SceneCard.ts | 2 +- src/configurationDefaults.ts | 62 +++-- src/generators/AreaCardsGenerator.ts | 61 +++++ src/generators/AreaView.ts | 39 --- src/generators/AreaViewGenerator.ts | 40 +++ src/generators/DeviceCardsGenerator.ts | 56 ++++ src/generators/DeviceViewGenerator.ts | 40 +++ src/generators/domainCardGenerator.ts | 118 --------- src/generators/domainCardsGenerator.ts | 244 ++++++++++++++++++ src/mushroom-strategy.ts | 76 ++++-- src/translations/de.json | 3 + src/translations/en.json | 3 + src/translations/es.json | 3 + src/translations/nl.json | 3 + .../homeassistant/data/config_entries.ts | 26 ++ src/types/strategy/strategy-cards.ts | 74 ++++-- src/types/strategy/strategy-generics.ts | 164 +++++------- src/types/strategy/strategy-views.ts | 8 +- src/types/strategy/type-guards.ts | 27 +- src/utilities/RegistryFilter.ts | 26 +- src/utilities/auxiliaries.ts | 36 +++ src/utilities/localize.ts | 29 ++- src/views/CameraView.ts | 14 +- src/views/ClimateView.ts | 14 +- src/views/CoverView.ts | 19 +- src/views/FanView.ts | 17 +- src/views/LightView.ts | 16 +- src/views/LockView.ts | 16 +- src/views/SceneView.ts | 15 +- src/views/SwitchView.ts | 16 +- src/views/VacuumView.ts | 16 +- 37 files changed, 1077 insertions(+), 433 deletions(-) create mode 100644 src/cards/DeviceCard.ts create mode 100644 src/generators/AreaCardsGenerator.ts delete mode 100644 src/generators/AreaView.ts create mode 100644 src/generators/AreaViewGenerator.ts create mode 100644 src/generators/DeviceCardsGenerator.ts create mode 100644 src/generators/DeviceViewGenerator.ts delete mode 100644 src/generators/domainCardGenerator.ts create mode 100644 src/generators/domainCardsGenerator.ts create mode 100644 src/types/homeassistant/data/config_entries.ts diff --git a/src/Registry.ts b/src/Registry.ts index a40df04..97927d4 100644 --- a/src/Registry.ts +++ b/src/Registry.ts @@ -6,7 +6,6 @@ import { EntityRegistryEntry } from './types/homeassistant/data/entity_registry' import { AllDomainsConfig, DashboardInfo, - isSortable, SingleDomainConfig, StrategyArea, StrategyConfig, @@ -17,6 +16,9 @@ import { import { logMessage, lvlFatal, lvlOff, lvlWarn, setDebugLevel } from './utilities/debug'; import setupCustomLocalize from './utilities/localize'; import RegistryFilter from './utilities/RegistryFilter'; +import { getObjectKeysByPropertyValue } from './utilities/auxiliaries'; +import { ConfigEntry } from './types/homeassistant/data/config_entries'; +import { isSortable } from './types/strategy/type-guards'; /** * Registry Class @@ -36,6 +38,18 @@ class Registry { private static _initialized: boolean = false; /** The Custom strategy configuration. */ private static _strategyOptions: StrategyConfig; + static darkMode: boolean; + + /** The entities which are grouped into a device view */ + // TODO: Create type or interface? + private static _configEntries: ConfigEntry[] = []; + + /** + * Home Assistant's Config Entries. + */ + static get configEntries() { + return Registry._configEntries; + } /** * Class constructor. @@ -48,8 +62,15 @@ class Registry { // eslint-disable-next-line @typescript-eslint/no-empty-function private constructor() {} + private static _groupingDeviceIds: Set; + + /** Get the initialization status of the Registry class. */ + static get groupingDeviceIds() { + return Registry._groupingDeviceIds; + } + /** The configuration of the strategy. */ - static get strategyOptions(): StrategyConfig { + static get strategyOptions() { return Registry._strategyOptions; } @@ -59,7 +80,7 @@ class Registry { * @remarks * This module makes changes to the registry at {@link Registry.initialize}. */ - static get areas(): StrategyArea[] { + static get areas() { return Registry._areas; } @@ -69,7 +90,7 @@ class Registry { * @remarks * This module makes changes to the registry at {@link Registry.initialize}. */ - static get devices(): DeviceRegistryEntry[] { + static get devices() { return Registry._devices; } @@ -79,17 +100,17 @@ class Registry { * @remarks * This module makes changes to the registry at {@link Registry.initialize}. */ - static get entities(): EntityRegistryEntry[] { + static get entities() { return Registry._entities; } /** Home Assistant's State registry. */ - static get hassStates(): HassEntities { + static get hassStates() { return Registry._hassStates; } /** Get the initialization status of the Registry class. */ - static get initialized(): boolean { + static get initialized() { return Registry._initialized; } @@ -105,6 +126,7 @@ class Registry { */ static async initialize(info: DashboardInfo): Promise { setupCustomLocalize(info.hass); + Registry.darkMode = info.hass.themes.darkMode; // Import the Hass States and strategy options. Registry._hassStates = info.hass.states; @@ -121,10 +143,11 @@ class Registry { // Import the registries of Home Assistant. try { // noinspection ES6MissingAwait False positive? https://youtrack.jetbrains.com/issue/WEB-63746 - [Registry._entities, Registry._devices, Registry._areas] = await Promise.all([ + [Registry._entities, Registry._devices, Registry._areas, Registry._configEntries] = await Promise.all([ info.hass.callWS({ type: 'config/entity_registry/list' }) as Promise, info.hass.callWS({ type: 'config/device_registry/list' }) as Promise, info.hass.callWS({ type: 'config/area_registry/list' }) as Promise, + info.hass.callWS({ type: 'config_entries/get' }) as Promise, ]); } catch (e) { logMessage(lvlFatal, 'Error importing Home Assistant registries!', e); @@ -144,7 +167,7 @@ class Registry { .whereEntityCategory('diagnostic') .isNotHidden() .whereDisabledBy(null) - .orderBy(['name', 'original_name'], 'asc') + .orderBy(['name', 'original_name']) .toList(); Registry._entities = Registry.entities.map((entity) => ({ @@ -156,7 +179,7 @@ class Registry { Registry._devices = new RegistryFilter(Registry.devices) .isNotHidden() .whereDisabledBy(null) - .orderBy(['name_by_user', 'name'], 'asc') + .orderBy(['name_by_user', 'name']) .toList(); Registry._devices = Registry.devices.map((device) => ({ @@ -170,7 +193,7 @@ class Registry { } else { // Create and add the undisclosed area if not hidden in the strategy options. if (!Registry.strategyOptions.areas.undisclosed?.hidden) { - Registry.areas.push(ConfigurationDefaults.areas.undisclosed); + Registry._areas.push(ConfigurationDefaults.areas.undisclosed); } // Merge area configurations of the Strategy options into the entries of the area registry. @@ -180,18 +203,18 @@ class Registry { }); // Ensure the custom configuration of the undisclosed area doesn't overwrite the area_id. - Registry.strategyOptions.areas.undisclosed.area_id = 'undisclosed'; + Registry._strategyOptions.areas.undisclosed.area_id = 'undisclosed'; // Remove hidden areas if configured as so and sort them by name. - Registry._areas = new RegistryFilter(Registry.areas).isNotHidden().orderBy(['order', 'name'], 'asc').toList(); + Registry._areas = new RegistryFilter(Registry.areas).isNotHidden().orderBy(['order', 'name']).toList(); } // Sort views by order first and then by title. const sortViews = () => { const entries = Object.entries(Registry.strategyOptions.views); - Registry.strategyOptions.views = Object.fromEntries( + Registry._strategyOptions.views = Object.fromEntries( entries.sort(([_, a], [__, b]) => { return (a.order ?? Infinity) - (b.order ?? Infinity) || (a.title ?? '').localeCompare(b.title ?? ''); }), @@ -203,7 +226,8 @@ class Registry { // Sort domains by order first and then by title. const sortDomains = () => { const entries = Object.entries(Registry.strategyOptions.domains); - Registry.strategyOptions.domains = Object.fromEntries( + + Registry._strategyOptions.domains = Object.fromEntries( entries.sort(([, a], [, b]) => { if (isSortable(a) && isSortable(b)) { return (a.order ?? Infinity) - (b.order ?? Infinity) || (a.title ?? '').localeCompare(b.title ?? ''); @@ -219,13 +243,26 @@ class Registry { // Sort extra views by order first and then by title. // TODO: Add sorting to the wiki. const sortExtraViews = () => { - Registry.strategyOptions.extra_views.sort((a, b) => { + Registry._strategyOptions.extra_views.sort((a, b) => { return (a.order ?? Infinity) - (b.order ?? Infinity) || (a.title ?? '').localeCompare(b.title ?? ''); }); }; sortExtraViews(); + // Process grouping by device. + this._groupingDeviceIds = new Set( + Registry.devices + .filter( + (device) => + Registry.strategyOptions.device_options['_'].group_entities || + getObjectKeysByPropertyValue(Registry.strategyOptions.device_options, 'group_entities', true).includes( + device.id, + ), + ) + .map((device) => device.id), + ); + Registry._initialized = true; } @@ -278,20 +315,34 @@ class Registry { * * @param {string} type The type of options to filter ("domain", "view", "chip"). * - * @returns {string[]} For domains and views: names of items that aren't hidden. + * @returns {Set} For domains and views: names of items that aren't hidden. * For chips: names of items that are explicitly set to true. */ - static getExposedNames(type: 'domain' | 'view' | 'chip'): string[] { + static getExposedNames(type: 'domain' | 'view' | 'chip'): Set { // TODO: Align chip with other types. if (type === 'chip') { - return Object.entries(Registry.strategyOptions.chips) + const keySet = new Set(); + + Object.entries(Registry.strategyOptions.chips) .filter(([_, value]) => value === true) - .map(([key]) => key.split('_')[0]); + .forEach(([key]) => { + keySet.add(key.split('_')[0]); + }); + + return keySet; } - const group = Registry.strategyOptions[`${type}s`] as Record; + const typeOptions = Registry.strategyOptions[`${type}s`] as Record; - return Object.keys(group).filter((key) => key !== '_' && key !== 'default' && !group[key].hidden); + const keySet = new Set(); + + Object.keys(typeOptions).forEach((key) => { + if (key !== '_' && key !== 'default' && !typeOptions[key].hidden) { + keySet.add(key); + } + }); + + return keySet; } } diff --git a/src/cards/AbstractCard.ts b/src/cards/AbstractCard.ts index 606d512..a76a5f6 100644 --- a/src/cards/AbstractCard.ts +++ b/src/cards/AbstractCard.ts @@ -1,6 +1,5 @@ import { Registry } from '../Registry'; import { LovelaceCardConfig } from '../types/homeassistant/data/lovelace/config/card'; -import { AbstractCardConfig } from '../types/strategy/strategy-cards'; import { RegistryEntry } from '../types/strategy/strategy-generics'; import { logMessage, lvlFatal } from '../utilities/debug'; @@ -48,7 +47,7 @@ abstract class AbstractCard { * * The configuration should be set by any of the child classes so the card correctly reflects an entity. */ - getCard(): AbstractCardConfig { + getCard(): LovelaceCardConfig { return { ...this.configuration, entity: 'entity_id' in this.entity ? this.entity.entity_id : undefined, diff --git a/src/cards/DeviceCard.ts b/src/cards/DeviceCard.ts new file mode 100644 index 0000000..40e5104 --- /dev/null +++ b/src/cards/DeviceCard.ts @@ -0,0 +1,69 @@ +// noinspection JSUnusedGlobalSymbols Class is dynamically imported. +import AbstractCard from './AbstractCard'; +import { TemplateCardConfig } from '../types/lovelace-mushroom/cards/template-card-config'; +import { localize } from '../utilities/localize'; +import { DeviceRegistryEntry } from '../types/homeassistant/data/device_registry'; +import RegistryFilter from '../utilities/RegistryFilter'; +import { Registry } from '../Registry'; + +/** + * Device Card Class + * + * Used to create a card for a device. + * + * @class + * @extends AbstractCard + */ +class DeviceCard extends AbstractCard { + /** + * Class constructor. + * + * @param {DeviceRegistryEntry} device The device entity to create a card for. + * @param {TemplateCardConfig} [customConfiguration] Options for the card. + * + * @throws {Error} If the Helper module isn't initialized. + */ + constructor(device: DeviceRegistryEntry, customConfiguration?: TemplateCardConfig) { + super(device); + + const configuration = DeviceCard.getDefaultConfig(); + const deviceName = device.name_by_user ?? device.name ?? localize('generic.unnamed', 'title'); + const integration = new RegistryFilter(Registry.configEntries) + .where((entry) => entry.entry_id === device.config_entries[0]) + .single(); + + // Initialize the default configuration. + configuration.primary = `${localize('generic.device', 'title')}: ${deviceName}`; + + if (integration) { + const iconBaseUrl = `https://brands.home-assistant.io/_/${integration.domain}/`; + configuration.picture = Registry.darkMode ? `${iconBaseUrl}dark_icon.png` : `${iconBaseUrl}icon.png`; + } + + if (configuration.tap_action && 'navigation_path' in configuration.tap_action) { + configuration.tap_action.navigation_path = device.id; + } + + this.configuration = { ...this.configuration, ...configuration, ...customConfiguration }; + } + + /** Returns the default configuration object for the card. */ + static getDefaultConfig(): TemplateCardConfig { + return { + type: 'custom:mushroom-template-card', + primary: undefined, + secondary: localize('generic.tap_here', 'title') + '…', + icon: 'mdi:view-dashboard-outline', + icon_color: 'blue', + tap_action: { + action: 'navigate', + navigation_path: '', + }, + hold_action: { + action: 'none', + }, + }; + } +} + +export { DeviceCard }; diff --git a/src/cards/FanCard.ts b/src/cards/FanCard.ts index 7706ed7..90fee9d 100644 --- a/src/cards/FanCard.ts +++ b/src/cards/FanCard.ts @@ -15,9 +15,11 @@ class FanCard extends AbstractCard { return { type: 'custom:mushroom-fan-card', icon: undefined, - show_percentage_control: true, - show_oscillate_control: true, icon_animation: true, + collapsible_controls: true, + show_direction_control: true, + show_oscillate_control: true, + show_percentage_control: true, }; } diff --git a/src/cards/HeaderCard.ts b/src/cards/HeaderCard.ts index 597eee6..e537d18 100644 --- a/src/cards/HeaderCard.ts +++ b/src/cards/HeaderCard.ts @@ -1,7 +1,8 @@ import { HassServiceTarget } from 'home-assistant-js-websocket'; import { LovelaceCardConfig } from '../types/homeassistant/data/lovelace/config/card'; import { StackCardConfig } from '../types/homeassistant/panels/lovelace/cards/types'; -import { CustomHeaderCardConfig, StrategyHeaderCardConfig } from '../types/strategy/strategy-cards'; +import { HeaderCardConfig } from '../types/strategy/strategy-cards'; +import { localize } from '../utilities/localize'; /** * Header Card class. @@ -13,34 +14,41 @@ class HeaderCard { /** The target to control the entities of. */ private readonly target: HassServiceTarget; /** The current configuration of the card after instantiating this class. */ - private readonly configuration: StrategyHeaderCardConfig; - - /** Returns the default configuration object for the card. */ - static getDefaultConfig(): StrategyHeaderCardConfig { - return { - type: 'custom:mushroom-title-card', - showControls: true, - iconOn: 'mdi:power-on', - iconOff: 'mdi:power-off', - onService: 'none', - offService: 'none', - }; - } + private readonly configuration: HeaderCardConfig; /** * Class constructor. * * @param {HassServiceTarget} target The target which is optionally controlled by the card. - * @param {CustomHeaderCardConfig} [customConfiguration] Custom card configuration. + * @param {HeaderCardConfig} [customConfiguration] Custom card configuration. * * @remarks * The target object can contain one or multiple ids of different entry types. */ - constructor(target: HassServiceTarget, customConfiguration?: CustomHeaderCardConfig) { + constructor(target: HassServiceTarget, customConfiguration?: HeaderCardConfig) { this.target = target; this.configuration = { ...HeaderCard.getDefaultConfig(), ...customConfiguration }; } + /** Returns the default configuration object for the card. */ + static getDefaultConfig(): HeaderCardConfig { + return { + type: 'custom:mushroom-title-card', + showControls: false, + on: { + icon: 'mdi:power-on', + icon_color: 'disabled', + service: 'none', + }, + off: { + icon: 'mdi:power-off', + icon_color: 'disabled', + service: 'none', + }, + title: localize('generic.unknown', 'title'), + }; + } + /** * Create a Header card configuration. * @@ -65,24 +73,24 @@ class HeaderCard { cards: [ { type: 'custom:mushroom-template-card', - icon: this.configuration.iconOff, + icon: this.configuration.on?.icon, layout: 'vertical', - icon_color: 'red', + icon_color: 'green', tap_action: { action: 'call-service', - service: this.configuration.offService, + service: this.configuration.on?.service, target: this.target, data: {}, }, }, { type: 'custom:mushroom-template-card', - icon: this.configuration.iconOn, + icon: this.configuration.off?.icon, layout: 'vertical', - icon_color: 'amber', + icon_color: 'deep-orange', tap_action: { action: 'call-service', - service: this.configuration.onService, + service: this.configuration.off?.service, target: this.target, data: {}, }, diff --git a/src/cards/LightCard.ts b/src/cards/LightCard.ts index c6e4415..c3075bc 100644 --- a/src/cards/LightCard.ts +++ b/src/cards/LightCard.ts @@ -2,8 +2,8 @@ import { EntityRegistryEntry } from '../types/homeassistant/data/entity_registry'; import { LightCardConfig } from '../types/lovelace-mushroom/cards/light-card-config'; -import { isCallServiceActionConfig } from '../types/strategy/strategy-generics'; import AbstractCard from './AbstractCard'; +import { isCallServiceActionConfig } from '../types/strategy/type-guards'; /** * Light Card Class diff --git a/src/cards/SceneCard.ts b/src/cards/SceneCard.ts index 5bd9729..78827b3 100644 --- a/src/cards/SceneCard.ts +++ b/src/cards/SceneCard.ts @@ -5,7 +5,7 @@ import { EntityRegistryEntry } from '../types/homeassistant/data/entity_registry import { EntityCardConfig } from '../types/lovelace-mushroom/cards/entity-card-config'; import AbstractCard from './AbstractCard'; import SwitchCard from './SwitchCard'; -import { isCallServiceActionConfig } from '../types/strategy/strategy-generics'; +import { isCallServiceActionConfig } from '../types/strategy/type-guards'; /** * Scene Card Class diff --git a/src/configurationDefaults.ts b/src/configurationDefaults.ts index 6282fb6..add6a08 100644 --- a/src/configurationDefaults.ts +++ b/src/configurationDefaults.ts @@ -23,6 +23,11 @@ export const ConfigurationDefaults: StrategyDefaults = { }, }, card_options: {}, + device_options: { + _: { + group_entities: false, + }, + }, chips: { // TODO: Make chips sortable. weather_entity: 'auto', @@ -57,10 +62,14 @@ export const ConfigurationDefaults: StrategyDefaults = { cover: { title: localize('cover.covers'), showControls: true, - iconOn: 'mdi:arrow-up', - iconOff: 'mdi:arrow-down', - onService: 'cover.open_cover', - offService: 'cover.close_cover', + on: { + icon: 'mdi:arrow-up-drop-circle-outline', + service: 'cover.open_cover', + }, + off: { + icon: 'mdi:arrow-up-down-circle-outline', + service: 'cover.close_cover', + }, hidden: false, }, default: { @@ -71,10 +80,14 @@ export const ConfigurationDefaults: StrategyDefaults = { fan: { title: localize('fan.fans'), showControls: true, - iconOn: 'mdi:fan', - iconOff: 'mdi:fan-off', - onService: 'fan.turn_on', - offService: 'fan.turn_off', + on: { + icon: 'mdi:fan', + service: 'fan.turn_on', + }, + off: { + icon: 'mdi:fan-off', + service: 'fan.turn_off', + }, hidden: false, }, input_select: { @@ -85,10 +98,14 @@ export const ConfigurationDefaults: StrategyDefaults = { light: { title: localize('light.lights'), showControls: true, - iconOn: 'mdi:lightbulb', - iconOff: 'mdi:lightbulb-off', - onService: 'light.turn_on', - offService: 'light.turn_off', + on: { + icon: 'mdi:lightbulb', + service: 'light.turn_on', + }, + off: { + icon: 'mdi:lightbulb-off', + service: 'light.turn_off', + }, hidden: false, }, lock: { @@ -109,7 +126,6 @@ export const ConfigurationDefaults: StrategyDefaults = { scene: { title: localize('scene.scenes'), showControls: false, - onService: 'scene.turn_on', hidden: false, }, select: { @@ -125,15 +141,27 @@ export const ConfigurationDefaults: StrategyDefaults = { switch: { title: localize('switch.switches'), showControls: true, - iconOn: 'mdi:power-plug', - iconOff: 'mdi:power-plug-off', - onService: 'switch.turn_on', - offService: 'switch.turn_off', + on: { + icon: 'mdi:light-switch', + service: 'switch.turn_on', + }, + off: { + icon: 'mdi:light-switch-off', + service: 'switch.turn_off', + }, hidden: false, }, vacuum: { title: localize('vacuum.vacuums'), showControls: true, + on: { + icon: 'mdi:robot-vacuum', + service: 'vacuum.start', + }, + off: { + icon: 'mdi:robot-vacuum-off', + service: 'vacuum.stop', + }, hidden: false, }, }, diff --git a/src/generators/AreaCardsGenerator.ts b/src/generators/AreaCardsGenerator.ts new file mode 100644 index 0000000..b9e2d5c --- /dev/null +++ b/src/generators/AreaCardsGenerator.ts @@ -0,0 +1,61 @@ +import DomainCardsGenerator from './domainCardsGenerator'; +import RegistryFilter from '../utilities/RegistryFilter'; +import { Registry } from '../Registry'; +import { AreaRegistryEntry } from '../types/homeassistant/data/area_registry'; +import { isSupportedDomain, SupportedDomains } from '../types/strategy/strategy-generics'; +import { filterNonNullValues } from '../utilities/auxiliaries'; +import { logMessage, lvlError } from '../utilities/debug'; + +/** + * Class responsible for generating Lovelace card configurations for an area view in the Home Assistant UI. + * + * The generator creates configurations for each entity (e.g., sensors, switches, etc.) of a device. + * It uses a combination of Header cards and entity-specific cards, and it handles miscellaneous entities + * that do not fit into any supported domain. + */ +class AreaCardsGenerator extends DomainCardsGenerator { + constructor(area: AreaRegistryEntry) { + super({ + entities: new RegistryFilter(Registry.entities).whereAreaId(area.area_id).toList(), + domains: Registry.getExposedNames('domain') as Set, + }); + + this.parent.type = 'area'; + this.parent.id = area.area_id; + } + + /** + * Creates a list of Lovelace card configurations. + * + * @remarks + * Take care about the order of calling the methods. + * Each time a card is created, the regarding entity is removed from the list of entities to process. + */ + public async getCards() { + try { + const deviceCards = await this.createDeviceCards(); + + const [sensorCards, miscellaneousCards] = await Promise.all([ + this.createSensorCards(), + this.createMiscellaneousCards(), + ]); + + const domainCardsPromises = [...this.domains] + .filter((domain) => isSupportedDomain(domain) && domain !== 'sensor') + .map((domain) => this.createSupportedDomainCards(domain)); + const supportedDomainCards = filterNonNullValues(await Promise.all(domainCardsPromises)); + + if (sensorCards) { + const insertIndex = supportedDomainCards.findIndex((card) => card.strategy.domain > 'sensor'); + supportedDomainCards.splice(insertIndex, 0, sensorCards); + } + + return filterNonNullValues([deviceCards, ...supportedDomainCards, miscellaneousCards]); + } catch (e) { + logMessage(lvlError, 'Error creating area cards', e); + return []; + } + } +} + +export default AreaCardsGenerator; diff --git a/src/generators/AreaView.ts b/src/generators/AreaView.ts deleted file mode 100644 index 854a621..0000000 --- a/src/generators/AreaView.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ViewInfo } from '../types/strategy/strategy-generics'; -import { LovelaceViewConfig } from '../types/homeassistant/data/lovelace/config/view'; -import RegistryFilter from '../utilities/RegistryFilter'; -import { Registry } from '../Registry'; -import { generateDomainCards } from './domainCardGenerator'; -import { isAreaRegistryEntry } from '../types/strategy/type-guards'; -import { logMessage, lvlFatal } from '../utilities/debug'; - -class AreaView extends HTMLTemplateElement { - /** - * Generate an Area view. - * - * The method creates cards for each domain (e.g., sensors, switches, etc.) in the current area, using a combination - * of Header cards and entity-specific cards. - * It also handles miscellaneous entities that don't fit into any supported domain. - * - * @param {ViewInfo} info The view's strategy information object. - * - * @remarks - * Called upon opening a subview. - */ - static async generateView(info: ViewInfo): Promise { - const parentEntity = info.view.strategy.parentEntry; - - if (!isAreaRegistryEntry(parentEntity)) { - logMessage(lvlFatal, `Entity ${parentEntity?.area_id} is not recognized as an area!`); - throw new Error(); - } - - return { - cards: await generateDomainCards( - Registry.getExposedNames('domain'), - new RegistryFilter(Registry.entities).whereAreaId(parentEntity.area_id).toList(), - ), - }; - } -} - -export default AreaView; diff --git a/src/generators/AreaViewGenerator.ts b/src/generators/AreaViewGenerator.ts new file mode 100644 index 0000000..64bed4f --- /dev/null +++ b/src/generators/AreaViewGenerator.ts @@ -0,0 +1,40 @@ +import { ViewInfo } from '../types/strategy/strategy-generics'; +import { LovelaceViewConfig } from '../types/homeassistant/data/lovelace/config/view'; +import { isAreaRegistryEntry } from '../types/strategy/type-guards'; +import { logMessage, lvlFatal } from '../utilities/debug'; +import AreaCardsGenerator from './AreaCardsGenerator'; + +/** + * Class responsible for generating an Area view in the Home Assistant UI. + * + * The generator creates cards for each domain (e.g., sensors, switches, etc.) in the current area. + * It uses a combination of Header cards and entity-specific cards, and it handles miscellaneous entities + * that do not fit into any supported domain. + * + * @remarks + * This class is instantiated with an area registry entry and is used to generate the view when a subview is opened. + */ +class AreaViewGenerator extends HTMLTemplateElement { + /** + * Generates a view for a Home Assistant area. + * + * The view is generated as a list of cards, one for each domain in the current area. + * If the parent entity associated with the view is not recognized as an area, an error is thrown. + * + * @param {ViewInfo} info - The object with information about the current view. + */ + static async generateView(info: ViewInfo): Promise { + const parentEntity = info.view.strategy.parentEntry; + + if (!isAreaRegistryEntry(parentEntity)) { + logMessage(lvlFatal, `Entry ${parentEntity?.id} is not recognized as an area!`); + throw new Error(); + } + + return { + cards: (await new AreaCardsGenerator(parentEntity).getCards()) || [], + }; + } +} + +export default AreaViewGenerator; diff --git a/src/generators/DeviceCardsGenerator.ts b/src/generators/DeviceCardsGenerator.ts new file mode 100644 index 0000000..1f5ea69 --- /dev/null +++ b/src/generators/DeviceCardsGenerator.ts @@ -0,0 +1,56 @@ +import DomainCardsGenerator from './domainCardsGenerator'; +import RegistryFilter from '../utilities/RegistryFilter'; +import { Registry } from '../Registry'; +import { isSupportedDomain, SupportedDomains } from '../types/strategy/strategy-generics'; +import { filterNonNullValues } from '../utilities/auxiliaries'; +import { DeviceRegistryEntry } from '../types/homeassistant/data/device_registry'; +import { LovelaceCardConfig } from '../types/homeassistant/data/lovelace/config/card'; +import { logMessage, lvlError } from '../utilities/debug'; + +/** + * Class responsible for generating Lovelace card configurations for a device view in the Home Assistant UI. + * + * The generator creates configurations for each entity (e.g., sensors, switches, etc.) of a device. + * It uses a combination of Header cards and entity-specific cards, and it handles miscellaneous entities + * that do not fit into any supported domain. + */ +class DeviceCardsGenerator extends DomainCardsGenerator { + constructor(device: DeviceRegistryEntry) { + super({ + entities: new RegistryFilter(Registry.entities).whereDeviceId(device.id).toList(), + domains: Registry.getExposedNames('domain') as Set, + }); + + this.parent.type = 'device'; + this.parent.id = device.id; + } + + /** + * Creates a list of Lovelace card configurations. + */ + public async getCards(): Promise { + try { + const domainCardsPromises = [...this.domains] + .filter((domain) => isSupportedDomain(domain) && domain !== 'sensor') + .map((domain) => this.createSupportedDomainCards(domain)); + + const supportedDomainCards = filterNonNullValues(await Promise.all(domainCardsPromises)); + const [sensorCards, miscellaneousCards] = await Promise.all([ + this.createSensorCards(), + this.createMiscellaneousCards(), + ]); + + if (sensorCards) { + const insertIndex = supportedDomainCards.findIndex((card) => card.strategy.domain > 'sensor'); + supportedDomainCards.splice(insertIndex, 0, sensorCards); + } + + return filterNonNullValues([...supportedDomainCards, miscellaneousCards]); + } catch (e) { + logMessage(lvlError, 'Error creating device cards', e); + return []; + } + } +} + +export default DeviceCardsGenerator; diff --git a/src/generators/DeviceViewGenerator.ts b/src/generators/DeviceViewGenerator.ts new file mode 100644 index 0000000..757c183 --- /dev/null +++ b/src/generators/DeviceViewGenerator.ts @@ -0,0 +1,40 @@ +import { ViewInfo } from '../types/strategy/strategy-generics'; +import { LovelaceViewConfig } from '../types/homeassistant/data/lovelace/config/view'; +import { isDeviceRegistryEntry } from '../types/strategy/type-guards'; +import { logMessage, lvlFatal } from '../utilities/debug'; +import DeviceCardsGenerator from './DeviceCardsGenerator'; + +/** + * Class responsible for generating a Device view in the Home Assistant UI. + * + * The generator creates cards for each entity (e.g., sensors, switches, etc.) of a device. + * It uses a combination of Header cards and entity-specific cards, and it handles miscellaneous entities + * that do not fit into any supported domain. + * + * @remarks + * This class is instantiated with a device registry entry and is used to generate the view when a subview is opened. + */ +class DeviceViewGenerator extends HTMLTemplateElement { + /** + * Generates a view for a Home Assistant device. + * + * The view is generated as a list of cards, one for each entity of the current device. + * If the parent entity associated with the view is not recognized as a device, an error is thrown. + * + * @param {ViewInfo} info - The object with information about the current view. + */ + static async generateView(info: ViewInfo): Promise { + const parentEntity = info.view.strategy.parentEntry; + + if (!isDeviceRegistryEntry(parentEntity)) { + logMessage(lvlFatal, `Entry ${parentEntity?.area_id} is not recognized as a device!`); + throw new Error(); + } + + return { + cards: (await new DeviceCardsGenerator(parentEntity).getCards()) || [], + }; + } +} + +export default DeviceViewGenerator; diff --git a/src/generators/domainCardGenerator.ts b/src/generators/domainCardGenerator.ts deleted file mode 100644 index 672f6a3..0000000 --- a/src/generators/domainCardGenerator.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { isSupportedDomain } from '../types/strategy/strategy-generics'; -import { sanitizeClassName } from '../utilities/auxiliaries'; -import RegistryFilter from '../utilities/RegistryFilter'; -import { Registry } from '../Registry'; -import HeaderCard from '../cards/HeaderCard'; -import SensorCard from '../cards/SensorCard'; -import { stackHorizontal } from '../utilities/cardStacking'; -import { logMessage, lvlError } from '../utilities/debug'; -import { EntityRegistryEntry } from '../types/homeassistant/data/entity_registry'; -import { LovelaceCardConfig } from '../types/homeassistant/data/lovelace/config/card'; - -export async function generateDomainCards( - domains: string[], - domainEntities: EntityRegistryEntry[], -): Promise { - const miscaleaniousCardsPromise = async (): Promise => { - if (Registry.strategyOptions.domains.default.hidden) { - return []; - } - - const miscellaneousEntities = new RegistryFilter(domainEntities) - .not() - .where((entity) => isSupportedDomain(entity.entity_id.split('.', 1)[0])) - .toList(); - - if (!miscellaneousEntities.length) { - return []; - } - - try { - const MiscellaneousCard = (await import('../cards/MiscellaneousCard')).default; - const miscellaneousCards = [ - new HeaderCard({}, Registry.strategyOptions.domains.default).createCard(), - ...miscellaneousEntities.map( - (entity) => - new MiscellaneousCard( - entity, - Registry.strategyOptions.card_options?.[entity.entity_id], - ).getCard() as LovelaceCardConfig, - ), - ]; - - return [ - { - type: 'vertical-stack', - cards: miscellaneousCards, - }, - ]; - } catch (e) { - logMessage(lvlError, 'Error creating card configurations for domain `miscellaneous`', e); - - return []; - } - }; - - const otherDomainCardPromises = domains - .filter(isSupportedDomain) - .map(async (domain): Promise => { - const moduleName = sanitizeClassName(domain + 'Card'); - const module = import(`../cards/${moduleName}`); - - const entities = new RegistryFilter(domainEntities) - .whereDomain(domain) - .where((entity) => !(domain === 'switch' && entity.entity_id.endsWith('_stateful_scene'))) - .toList(); - - if (!entities.length) { - return []; - } - - const titleCard = new HeaderCard( - { entity_id: entities.map((entity) => entity.entity_id) }, - Registry.strategyOptions.domains[domain], - ).createCard(); - - try { - const DomainCard = (await module).default; - - if (domain === 'sensor') { - const domainCards = entities - .filter((entity) => Registry.hassStates[entity.entity_id]?.attributes.unit_of_measurement) - .map((entity) => { - const options = { - ...(entity.device_id && Registry.strategyOptions.card_options?.[entity.device_id]), - ...Registry.strategyOptions.card_options?.[entity.entity_id], - type: 'custom:mini-graph-card', - entities: [entity.entity_id], - }; - - return new SensorCard(entity, options).getCard(); - }); - - return domainCards.length ? [{ type: 'vertical-stack', cards: [titleCard, ...domainCards] }] : []; - } - - let domainCards = entities.map((entity) => { - const cardOptions = { - ...(entity.device_id && Registry.strategyOptions.card_options?.[entity.device_id]), - ...Registry.strategyOptions.card_options?.[entity.entity_id], - }; - - return new DomainCard(entity, cardOptions).getCard() as LovelaceCardConfig; - }); - - if (domain === 'binary_sensor') { - domainCards = stackHorizontal(domainCards); - } - - return domainCards.length ? [{ type: 'vertical-stack', cards: [titleCard, ...domainCards] }] : []; - } catch (e) { - logMessage(lvlError, `Error creating card configurations for domain ${domain}`, e); - - return []; - } - }); - - return (await Promise.all([...otherDomainCardPromises, miscaleaniousCardsPromise()])).flat(); -} diff --git a/src/generators/domainCardsGenerator.ts b/src/generators/domainCardsGenerator.ts new file mode 100644 index 0000000..91d761d --- /dev/null +++ b/src/generators/domainCardsGenerator.ts @@ -0,0 +1,244 @@ +import { logMessage, lvlError, lvlInfo } from '../utilities/debug'; +import { EntityRegistryEntry } from '../types/homeassistant/data/entity_registry'; +import { LovelaceCardConfig } from '../types/homeassistant/data/lovelace/config/card'; +import HeaderCard from '../cards/HeaderCard'; +import { DeviceCard } from '../cards/DeviceCard'; +import RegistryFilter from '../utilities/RegistryFilter'; +import { Registry } from '../Registry'; +import { isSupportedDomain, SupportedDomains } from '../types/strategy/strategy-generics'; +import { localize } from '../utilities/localize'; +import { filterNonNullValues, sanitizeClassName } from '../utilities/auxiliaries'; +import { stackHorizontal } from '../utilities/cardStacking'; +import { CustomCardConfiguration } from '../types/strategy/strategy-cards'; + +/** + * Abstract class responsible for generating Lovelace card configurations for various domains in the Home Assistant UI. + * + * This class serves as a base for generating card configurations erp domain. + * Subclasses should implement specific logic for creating cards based on domain requirements. + * + * @remarks + * Take care about the order of calling the methods. + * Each time a card is created, the regarding entity is removed from the list of entities to process. + */ +abstract class DomainCardsGenerator { + protected domains: Set; + protected parent: { + type: 'device' | 'area' | undefined; + id: string | undefined; + } = { + type: undefined, + id: undefined, + }; + private entities: EntityRegistryEntry[]; + + /** + * Initializes the DomainCardsGenerator with the specified entities and domains. + * + * @param properties - An object containing the entities and domains to be used for card generation. + */ + protected constructor(properties: { entities: EntityRegistryEntry[]; domains: Set }) { + this.entities = properties.entities; + this.domains = properties.domains; + } + + /** + * Creates Lovelace card configurations for devices. + * + * This method generates cards devices. + * + * @returns A promise that resolves to a Lovelace card configuration for devices or null if no devices are available. + */ + public async createDeviceCards(): Promise { + const devices = new RegistryFilter(Registry.devices) + .where((device) => Registry.groupingDeviceIds.has(device.id)) + .toList(); + + if (!devices.length) { + logMessage(lvlInfo, `No sensors available for view of ${this.parent.type} ${this.parent.id}.`); + return null; + } + + const cards = await Promise.all( + devices.map(async (device) => { + try { + const deviceCard = new DeviceCard(device).getCard(); + this.entities = this.entities.filter((entity) => entity.device_id !== device.id); + + return deviceCard; + } catch (e) { + logMessage(lvlError, `Error creating card for device with id ${device.id}`, e); + + return null; + } + }), + ); + + const headerCard = new HeaderCard( + {}, + { + title: localize('generic.devices', 'title'), + showControls: false, + }, + ).createCard(); + + return { + type: 'vertical-stack', + cards: [headerCard, ...cards.filter((card): card is LovelaceCardConfig => card !== null)], + }; + } + + /** + * Creates Lovelace card configurations for sensor entities. + * + * This method filters the entities to only include sensors and generates cards for them. + * + * @returns A promise that resolves to a Lovelace card configuration for sensors or null if no sensors are available. + */ + protected async createSensorCards(): Promise { + const entities = new RegistryFilter(this.entities) + .whereDomain('sensor') + .where((entity) => Registry.hassStates[entity.entity_id]?.attributes.unit_of_measurement !== undefined) + .toList(); + + if (!entities.length) { + logMessage(lvlInfo, `No sensors available for view of ${this.parent.type} ${this.parent.id}.`); + return null; + } + + const cards = await Promise.all( + entities.map(async (entity) => { + return this.createEntityCard(entity, 'SensorCard', { + ...Registry.strategyOptions.card_options[entity.entity_id], + type: 'custom:mini-graph-card', + entities: [entity.entity_id], + }); + }), + ); + + const headerCard = new HeaderCard({}, Registry.strategyOptions.domains['sensor']).createCard(); + + return { + type: 'vertical-stack', + cards: [headerCard, ...cards.filter((card): card is LovelaceCardConfig => card !== null)], + strategy: { domain: 'sensor' }, + }; + } + + /** + * Creates Lovelace card configurations for miscellaneous entities. + * + * This method filters the entities to include those that do not belong to any supported domain and generates cards + * for them. + * + * @returns A promise that resolves to a Lovelace card configuration for miscellaneous entities or null if none are + * available. + */ + protected async createMiscellaneousCards(): Promise { + const entities = new RegistryFilter(this.entities) + .where((entity) => !isSupportedDomain(entity.entity_id.split('.', 1)[0])) + .toList(); + + if (!entities.length) { + logMessage(lvlInfo, `No sensors available for view of ${this.parent.type} ${this.parent.id}.`); + return null; + } + + const cards = await Promise.all( + entities.map(async (entity) => { + return this.createEntityCard( + entity, + 'MiscellaneousCard', + Registry.strategyOptions.card_options[entity.entity_id], + ); + }), + ); + + const headerCard = new HeaderCard({}, { title: Registry.strategyOptions.domains['default'].title }).createCard(); + + return { + type: 'vertical-stack', + cards: [headerCard, ...cards.filter((card): card is LovelaceCardConfig => card !== null)], + strategy: { domain: 'default' }, + }; + } + + /** + * Creates Lovelace card configurations for entities within a supported domain. + * + * This method generates cards for all entities that belong to the specified domain. + * + * @param domainName - The name of the domain for which to create cards. + * @returns A promise that resolves to a Lovelace card configuration for the specified domain or null if no entities + * of that domain are available. + */ + protected async createSupportedDomainCards(domainName: SupportedDomains): Promise { + const targets: EntityRegistryEntry['entity_id'][] = []; + const entities = new RegistryFilter(this.entities) + .whereDomain(domainName) + .where((entity) => !(domainName === 'switch' && entity.entity_id.endsWith('_stateful_scene'))) + .toList(); + + if (!entities.length) { + logMessage(lvlInfo, `No ${domainName} entities available for view of ${this.parent.type} ${this.parent.id}.`); + return null; + } + + let cards: (LovelaceCardConfig | null)[] = await Promise.all( + entities.map(async (entity) => { + targets.push(entity.entity_id); + + return this.createEntityCard( + entity, + `${domainName}Card`, + Registry.strategyOptions.card_options[entity.entity_id], + ); + }), + ); + + if (domainName === 'binary_sensor') { + cards = stackHorizontal(filterNonNullValues(cards)); + } + + const headerCard = new HeaderCard( + { entity_id: targets }, + Registry.strategyOptions.domains[domainName], + ).createCard(); + + return { + type: 'vertical-stack', + cards: [headerCard, ...cards], + strategy: { domain: domainName }, + }; + } + + /** + * Creates a Lovelace card configuration for a specified entity. + * + * @param entity - The entity for which to create a card. + * @param entityClassName - The class name of the card to create. + * @param customConfiguration - Optional custom configuration for the card. + * + * @returns A promise that resolves to the Lovelace card configuration or null if creation fails. + */ + private async createEntityCard( + entity: EntityRegistryEntry, + entityClassName: string, + customConfiguration?: CustomCardConfiguration, + ): Promise { + try { + const { default: entityClass } = await import(`../cards/${sanitizeClassName(entityClassName)}`); + const card = new entityClass(entity, customConfiguration).getCard(); + + this.entities = this.entities.filter((unprocessedEntity) => unprocessedEntity.entity_id !== entity.entity_id); + + return card; + } catch (e) { + logMessage(lvlError, `Error creating a card for entity with id ${entity.entity_id}`, e); + + return null; + } + } +} + +export default DomainCardsGenerator; diff --git a/src/mushroom-strategy.ts b/src/mushroom-strategy.ts index 443b780..38785fa 100644 --- a/src/mushroom-strategy.ts +++ b/src/mushroom-strategy.ts @@ -1,12 +1,13 @@ import { Registry } from './Registry'; import { LovelaceConfig } from './types/homeassistant/data/lovelace/config/types'; -import { LovelaceViewRawConfig } from './types/homeassistant/data/lovelace/config/view'; +import { LovelaceViewConfig } from './types/homeassistant/data/lovelace/config/view'; import { DashboardInfo, isSupportedView } from './types/strategy/strategy-generics'; -import { sanitizeClassName } from './utilities/auxiliaries'; +import { filterNonNullValues, sanitizeClassName } from './utilities/auxiliaries'; import { logMessage, lvlError, lvlFatal } from './utilities/debug'; +import AreaViewGenerator from './generators/AreaViewGenerator'; import RegistryFilter from './utilities/RegistryFilter'; -import DeviceView from './generators/DeviceView'; -import AreaView from './generators/AreaView'; +import { localize } from './utilities/localize'; +import DeviceViewGenerator from './generators/DeviceViewGenerator'; /** * Mushroom Dashboard Strategy.
@@ -35,30 +36,53 @@ class MushroomStrategy extends HTMLTemplateElement { logMessage(lvlFatal, 'Error initializing the Registry!', e); } - const viewPromises = Registry.getExposedNames('view') - .filter(isSupportedView) - .map(async (viewName) => { - try { - const moduleName = sanitizeClassName(`${viewName}View`); - const View = (await import(`./views/${moduleName}`)).default; - const currentView = new View(Registry.strategyOptions.views[viewName]); - const viewConfiguration = await currentView.getView(); + // Main views. + const viewPromises = [...Registry.getExposedNames('view')].filter(isSupportedView).map(async (viewName) => { + try { + const moduleName = sanitizeClassName(`${viewName}View`); + const View = (await import(`./views/${moduleName}`)).default; + const currentView = new View(Registry.strategyOptions.views[viewName]); + const viewConfiguration = await currentView.getView(); - if (viewConfiguration.cards.length) { - return viewConfiguration; - } - } catch (e) { - logMessage(lvlError, `Error importing ${viewName} view!`, e); + if (viewConfiguration.cards.length) { + return viewConfiguration; } + } catch (e) { + logMessage(lvlError, `Error importing ${viewName} view!`, e); + } - return null; - }); + return null; + }); - const views = (await Promise.all(viewPromises)).filter(Boolean) as LovelaceViewRawConfig[]; + const views = filterNonNullValues(await Promise.all(viewPromises)) as LovelaceViewConfig[]; - views.push(...resolvedViews); + // Device views. + const devices = new RegistryFilter(Registry.devices) + .where((device) => Registry.groupingDeviceIds.has(device.id)) + .toList(); - // Subviews for areas + const deviceViews = devices.map((device) => { + const deviceName = device.name_by_user || device.name || localize('generic.unknown', 'title'); + + return { + title: `${localize('generic.device', 'title')}: ${deviceName}`, + path: device.id, + subview: true, + icon: 'mdi:devices', + strategy: { + type: 'custom:mushroom-strategy-device-view', + parentEntry: device, + }, + }; + }); + + views.push(...deviceViews); + + if (devices.length && !customElements.get('ll-strategy-mushroom-strategy-device-view')) { + customElements.define('ll-strategy-mushroom-strategy-device-view', DeviceViewGenerator); + } + + // Area views. views.push( ...Registry.areas.map((area) => ({ title: area.name, @@ -71,8 +95,8 @@ class MushroomStrategy extends HTMLTemplateElement { })), ); - if (Registry.areas.length) { - customElements.define('ll-strategy-mushroom-strategy-area-view', AreaView); + if (Registry.areas.length && !customElements.get('ll-strategy-mushroom-strategy-area-view')) { + customElements.define('ll-strategy-mushroom-strategy-area-view', AreaViewGenerator); } // Extra views @@ -100,4 +124,6 @@ async function main() { } } -main(); +main().catch((error) => { + throw 'Mushroom Strategy - An error occurred. Check the console (F12) for details.'; +}); diff --git a/src/translations/de.json b/src/translations/de.json index 731220d..a4426da 100644 --- a/src/translations/de.json +++ b/src/translations/de.json @@ -19,6 +19,8 @@ "all": "Alle", "areas": "Bereiche", "busy": "Beschäftigt", + "device": "Gerät", + "devices": "Geräte", "good_afternoon": "Guten Nachmittag", "good_evening": "Guten Abend", "good_morning": "Guten Morgen", @@ -29,6 +31,7 @@ "off": "Aus", "on": "Ein", "open": "Offen", + "tap_here": "Tippen Sie hier", "unavailable": "Nicht verfügbar", "unclosed": "Nicht Geschlossen", "undisclosed": "Sonstiges", diff --git a/src/translations/en.json b/src/translations/en.json index 1ec9369..2591f69 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -19,6 +19,8 @@ "all": "All", "areas": "Areas", "busy": "Busy", + "device": "Device", + "devices": "Devices", "good_afternoon": "Good afternoon", "good_evening": "Good evening", "good_morning": "Good morning", @@ -29,6 +31,7 @@ "off": "Off", "on": "On", "open": "Open", + "tap_here": "Tap here", "unavailable": "Unavailable", "unclosed": "Unclosed", "undisclosed": "Other", diff --git a/src/translations/es.json b/src/translations/es.json index a991c95..a3e9564 100644 --- a/src/translations/es.json +++ b/src/translations/es.json @@ -19,6 +19,8 @@ "all": "Todo", "areas": "Áreas", "busy": "Ocupado", + "device": "Dispositivo", + "devices": "Dispositivos", "good_afternoon": "Buenas tardes", "good_evening": "Buenas noches", "good_morning": "Buenos días", @@ -29,6 +31,7 @@ "off": "Apagado", "on": "Encendido", "open": "Abierto", + "tap_here": "Toca aquí", "unavailable": "No Disponible", "unclosed": "Sin Cerrar", "undisclosed": "Varios", diff --git a/src/translations/nl.json b/src/translations/nl.json index 45a1e2b..32536b5 100644 --- a/src/translations/nl.json +++ b/src/translations/nl.json @@ -19,6 +19,8 @@ "all": "Alle", "areas": "Ruimtes", "busy": "Bezig", + "device": "Apparaat", + "devices": "Apparaten", "good_afternoon": "Goedemiddag", "good_evening": "Goedeavond", "good_morning": "Goedemorgen", @@ -29,6 +31,7 @@ "off": "Uit", "on": "Aan", "open": "Open", + "tap_here": "Tik hier", "unavailable": "Onbeschikbaar", "unclosed": "Niet Gesloten", "undisclosed": "Overige", diff --git a/src/types/homeassistant/data/config_entries.ts b/src/types/homeassistant/data/config_entries.ts new file mode 100644 index 0000000..5fc3ff2 --- /dev/null +++ b/src/types/homeassistant/data/config_entries.ts @@ -0,0 +1,26 @@ +export interface ConfigEntry { + entry_id: string; + domain: string; + title: string; + source: string; + state: + | 'loaded' + | 'setup_error' + | 'migration_error' + | 'setup_retry' + | 'not_loaded' + | 'failed_unload' + | 'setup_in_progress'; + supports_options: boolean; + supports_remove_device: boolean; + supports_unload: boolean; + supports_reconfigure: boolean; + supported_subentry_types: Record; + num_subentries: number; + pref_disable_new_entities: boolean; + pref_disable_polling: boolean; + disabled_by: 'user' | null; + reason: string | null; + error_reason_translation_key: string | null; + error_reason_translation_placeholders: Record | null; +} diff --git a/src/types/strategy/strategy-cards.ts b/src/types/strategy/strategy-cards.ts index 2650738..f552bf6 100644 --- a/src/types/strategy/strategy-cards.ts +++ b/src/types/strategy/strategy-cards.ts @@ -1,8 +1,21 @@ import { LovelaceCardConfig } from '../homeassistant/data/lovelace/config/card'; -import { TitleCardConfig as MushroomTitleCardConfig } from '../lovelace-mushroom/cards/title-card-config'; +import { TitleCardConfig } from '../lovelace-mushroom/cards/title-card-config'; import { ActionsSharedConfig } from '../lovelace-mushroom/shared/config/actions-config'; import { AppearanceSharedConfig } from '../lovelace-mushroom/shared/config/appearance-config'; import { EntitySharedConfig } from '../lovelace-mushroom/shared/config/entity-config'; +import { ChipsCardConfig } from '../lovelace-mushroom/cards/chips-card'; +import { ClimateCardConfig } from '../lovelace-mushroom/cards/climate-card-config'; +import { CoverCardConfig } from '../lovelace-mushroom/cards/cover-card-config'; +import { EntityCardConfig } from '../lovelace-mushroom/cards/entity-card-config'; +import { FanCardConfig } from '../lovelace-mushroom/cards/fan-card-config'; +import { LightCardConfig } from '../lovelace-mushroom/cards/light-card-config'; +import { LockCardConfig } from '../lovelace-mushroom/cards/lock-card-config'; +import { MediaPlayerCardConfig } from '../lovelace-mushroom/cards/media-player-card-config'; +import { NumberCardConfig } from '../lovelace-mushroom/cards/number-card-config'; +import { PersonCardConfig } from '../lovelace-mushroom/cards/person-card-config'; +import { SelectCardConfig } from '../lovelace-mushroom/cards/select-card-config'; +import { TemplateCardConfig } from '../lovelace-mushroom/cards/template-card-config'; +import { VacuumCardConfig } from '../lovelace-mushroom/cards/vacuum-card-config'; /** * Abstract Card Config. @@ -10,22 +23,51 @@ import { EntitySharedConfig } from '../lovelace-mushroom/shared/config/entity-co export type AbstractCardConfig = LovelaceCardConfig & EntitySharedConfig & AppearanceSharedConfig & ActionsSharedConfig; /** - * Header Card Config. + * Header Card Control Configuration. * - * @property {boolean} [showControls=true] - False to hide controls. - * @property {string} [iconOn] - Icon to show for switching entities from the off state. - * @property {string} [iconOff] - Icon to show for switching entities to the off state. - * @property {string} [onService=none] - Service to call for switching entities from the off state. - * @property {string} [offService=none] - Service to call for switching entities to the off state. + * @property {string} [icon] - Icon to display for the control. + * @property {string} [icon_color] - Color of the icon. + * @property {string} [service] - Service to call when the control is activated. */ -export interface StrategyHeaderCardConfig extends MushroomTitleCardConfig { - type: 'custom:mushroom-title-card'; - showControls?: boolean; - iconOn?: string; - iconOff?: string; - onService?: string; - offService?: string; +interface HeaderCardControlConfig { + icon?: string; + icon_color?: string; + service?: string; } -/** Custom Configuration of a Strategy Header Card. */ -export type CustomHeaderCardConfig = Omit; +/** + * Header Card Config. + * + * @property {string} [type] - Optional property specifying the card type, set to 'custom:mushroom-title-card'. + * @property {boolean} [showControls] - Optional flag to show or hide controls on the card (default is true). + * @property {HeaderCardControlConfig} [on] - Configuration for the 'on' state of the card. + * @property {HeaderCardControlConfig} [off] - Configuration for the 'off' state of the card. + */ +export interface HeaderCardConfig extends Omit { + type?: 'custom:mushroom-title-card'; + showControls?: boolean; + on?: HeaderCardControlConfig; + off?: HeaderCardControlConfig; +} + +/** + * Union type representing the custom configuration options for the various cards. + * + * This type includes all possible card configurations that can be used within the Home Assistant UI for different + * entity types. + */ +export type CustomCardConfiguration = + | ChipsCardConfig + | ClimateCardConfig + | CoverCardConfig + | EntityCardConfig + | FanCardConfig + | LightCardConfig + | LockCardConfig + | MediaPlayerCardConfig + | NumberCardConfig + | PersonCardConfig + | SelectCardConfig + | TemplateCardConfig + | TitleCardConfig + | VacuumCardConfig; diff --git a/src/types/strategy/strategy-generics.ts b/src/types/strategy/strategy-generics.ts index 1ad69d5..35e9ea0 100644 --- a/src/types/strategy/strategy-generics.ts +++ b/src/types/strategy/strategy-generics.ts @@ -1,13 +1,13 @@ 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'; 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 { HeaderCardConfig } from './strategy-cards'; +import { ConfigEntry } from '../homeassistant/data/config_entries'; /** * List of supported domains. @@ -76,47 +76,10 @@ export type SupportedViews = (typeof SUPPORTED_VIEWS)[number]; export type SupportedChips = (typeof SUPPORTED_CHIPS)[number]; export type HomeViewSections = (typeof HOME_VIEW_SECTIONS)[number]; -/** - * Base interface for sortable items. - * - * @property {number} order - Numeric value used for sorting items - */ -interface SortableBase { - order: number; -} - -/** - * Sortable item that uses a title for identification. - * Mutually exclusive with SortableWithName. - * - * @property {string} title - Display title of the item - * @property {never} [name] - Prevents using name property - */ -type SortableWithTitle = SortableBase & { title: string; name?: never }; - -/** - * Sortable item that uses a name for identification. - * Mutually exclusive with SortableWithTitle. - * - * @property {string} name - Identifier name of the item - * @property {never} [title] - Prevents using title property - */ -type SortableWithName = SortableBase & { name: string; title?: never }; - -/** - * Union type for items that can be sorted. - * Items must have either a title or a name property, but not both. - * - * @remarks - * This type is used to sort objects by title or by name. - * The `order` property is used to sort the items. - */ -export type Sortable = SortableWithTitle | SortableWithName; - /** * An entry of a Home Assistant Registry. */ -export type RegistryEntry = StrategyArea | DeviceRegistryEntry | EntityRegistryEntry; +export type RegistryEntry = StrategyArea | DeviceRegistryEntry | EntityRegistryEntry | ConfigEntry; /** * View Configuration of the strategy. @@ -189,48 +152,11 @@ 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. */ -export interface SingleDomainConfig extends Partial { +export interface SingleDomainConfig extends Partial { hidden: boolean; order?: number; } -/** - * Strategy Configuration. - * - * @property {Object.} areas - List 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 {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. - */ -export interface StrategyConfig { - areas: { [S: string]: StrategyArea }; - card_options: { [S: string]: CustomCardConfig }; - chips: ChipConfiguration; - debug: boolean; - domains: { [K in SupportedDomains]: K extends '_' ? AllDomainsConfig : SingleDomainConfig }; - extra_cards: LovelaceCardConfig[]; - extra_views: StrategyViewConfig[]; - home_view: { - hidden: HomeViewSections[] | []; - }; - views: Record; - quick_access_cards: LovelaceCardConfig[]; -} - -/** - * Represents the default configuration for a strategy. - */ -export interface StrategyDefaults extends StrategyConfig { - areas: { undisclosed: StrategyArea } & { [S: string]: StrategyArea }; -} - /** * Strategy Area. * @@ -279,29 +205,81 @@ export interface CustomCardConfig extends LovelaceCardConfig { } /** - * Checks if the given object is of a sortable type. + * Strategy Configuration. * - * Sortable types are objects that have an `order`, `title` or `name` property. - * - * @param {object} object - The object to check. - * @returns {boolean} - True if the object is an instance of Sortable, false otherwise. + * @property {Object.} areas - List 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 {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. */ -export function isSortable(object: object): object is Sortable { - return object && ('order' in object || 'title' in object || 'name' in object); +export interface StrategyConfig { + areas: { [S in AreaRegistryEntry['area_id']]: StrategyArea }; + device_options: { [S in DeviceRegistryEntry['id']]: { hidden?: boolean; group_entities?: boolean } }; + // TODO: Move device entries from card_options to device_options. + card_options: { [S in EntityRegistryEntry['entity_id']]: CustomCardConfig }; + chips: ChipConfiguration; + debug: boolean; + domains: { [K in SupportedDomains]: K extends '_' ? AllDomainsConfig : SingleDomainConfig }; + extra_cards: LovelaceCardConfig[]; + extra_views: StrategyViewConfig[]; + home_view: { + hidden: HomeViewSections[] | []; + }; + views: Record; + quick_access_cards: LovelaceCardConfig[]; } /** - * Type guard to check if an object matches the CallServiceActionConfig interface. - * - * @param {ActionConfig} [object] - The object to check. - * @returns {boolean} - True if the object represents a valid service action configuration. + * Represents the default configuration for a strategy. */ -export function isCallServiceActionConfig(object?: ActionConfig): object is CallServiceActionConfig { - return ( - !!object && (object.action === 'perform-action' || object.action === 'call-service') && 'perform_action' in object - ); +export interface StrategyDefaults extends StrategyConfig { + areas: { undisclosed: StrategyArea } & { [S: string]: StrategyArea }; } +/** + * Base interface for sortable items. + * + * @property {number} order - Numeric value used for sorting items + */ +interface SortableBase { + order: number; +} + +/** + * Sortable item that uses a title for identification. + * Mutually exclusive with SortableWithName. + * + * @property {string} title - Display title of the item + * @property {never} [name] - Prevents using name property + */ +type SortableWithTitle = SortableBase & { title: string; name?: never }; + +/** + * Sortable item that uses a name for identification. + * Mutually exclusive with SortableWithTitle. + * + * @property {string} name - Identifier name of the item + * @property {never} [title] - Prevents using title property + */ +type SortableWithName = SortableBase & { name: string; title?: never }; + +/** + * Union type for items that can be sorted. + * Items must have either a title or a name property, but not both. + * + * @remarks + * This type is used to sort objects by title or by name. + * The `order` property is used to sort the items. + */ +export type Sortable = SortableWithTitle | SortableWithName; + /** * Type guard to check if a given identifier exists in a list of supported identifiers. * diff --git a/src/types/strategy/strategy-views.ts b/src/types/strategy/strategy-views.ts index 23f9103..fa80c52 100644 --- a/src/types/strategy/strategy-views.ts +++ b/src/types/strategy/strategy-views.ts @@ -1,20 +1,20 @@ import { LovelaceViewConfig } from '../homeassistant/data/lovelace/config/view'; -import { CustomHeaderCardConfig } from './strategy-cards'; import { SupportedDomains } from './strategy-generics'; +import { HeaderCardConfig } from './strategy-cards'; /** * Options for the extended View class. * - * @property {StrategyHeaderCardConfig} [headerCardConfiguration] - Options for the Header card. + * @property {HeaderCardConfig} [headerCardConfiguration] - Options for the Header card. */ export interface ViewConfig extends LovelaceViewConfig { - headerCardConfiguration?: CustomHeaderCardConfig; + headerCardConfiguration?: HeaderCardConfig; } /** * Interface for constructors of AbstractView subclasses that are expected to define a static domain property. * - * @property {SupportedDomains | "home"} domain - The domain which the view is representing. + * @property {SupportedDomains | 'home'} domain - The domain which the view is representing. */ export interface ViewConstructor { domain: SupportedDomains | 'home'; diff --git a/src/types/strategy/type-guards.ts b/src/types/strategy/type-guards.ts index 57478de..816f332 100644 --- a/src/types/strategy/type-guards.ts +++ b/src/types/strategy/type-guards.ts @@ -1,6 +1,7 @@ import { DeviceRegistryEntry } from '../homeassistant/data/device_registry'; -import { RegistryEntry } from './strategy-generics'; +import { RegistryEntry, Sortable } from './strategy-generics'; import { AreaRegistryEntry } from '../homeassistant/data/area_registry'; +import { ActionConfig, CallServiceActionConfig } from '../homeassistant/data/lovelace/config/action'; /** * Type guard to check if the given object is a DeviceRegistryEntry. @@ -21,3 +22,27 @@ export function isDeviceRegistryEntry(object?: RegistryEntry): object is DeviceR export function isAreaRegistryEntry(object?: RegistryEntry): object is AreaRegistryEntry { return !!object && 'area_id' in object; } + +/** + * Checks if the given object is of a sortable type. + * + * Sortable types are objects that have an `order`, `title` or `name` property. + * + * @param {object} object - The object to check. + * @returns {boolean} - True if the object is an instance of Sortable, false otherwise. + */ +export function isSortable(object: object): object is Sortable { + return object && ('order' in object || 'title' in object || 'name' in object); +} + +/** + * Type guard to check if an object matches the CallServiceActionConfig interface. + * + * @param {ActionConfig} [object] - The object to check. + * @returns {boolean} - True if the object represents a valid service action configuration. + */ +export function isCallServiceActionConfig(object?: ActionConfig): object is CallServiceActionConfig { + return ( + !!object && (object.action === 'perform-action' || object.action === 'call-service') && 'perform_action' in object + ); +} diff --git a/src/utilities/RegistryFilter.ts b/src/utilities/RegistryFilter.ts index e95e01a..4e49a43 100644 --- a/src/utilities/RegistryFilter.ts +++ b/src/utilities/RegistryFilter.ts @@ -16,7 +16,7 @@ import { logMessage, lvlWarn } from './debug'; class RegistryFilter { private readonly entries: T[]; private filters: (((entry: T) => boolean) | ((entry: T, index: number) => boolean))[] = []; - private readonly entryIdentifier: ('entity_id' | 'floor_id' | 'id') & K; + private readonly entryIdentifier: ('entity_id' | 'floor_id' | 'entry_id' | 'id') & K; private invertNext: boolean = false; /** @@ -27,7 +27,13 @@ class RegistryFilter { constructor(entries: T[]) { this.entries = entries; this.entryIdentifier = ( - entries.length === 0 || 'entity_id' in entries[0] ? 'entity_id' : 'floor_id' in entries[0] ? 'floor_id' : 'id' + entries.length === 0 || 'entity_id' in entries[0] + ? 'entity_id' + : 'floor_id' in entries[0] + ? 'floor_id' + : 'entry_id' in entries[0] + ? 'entry_id' + : 'id' ) as ('entity_id' | 'floor_id' | 'id') & K; } @@ -81,9 +87,14 @@ class RegistryFilter { */ whereAreaId(areaId?: string, expandToDevice: boolean = true): this { const predicate = (entry: T) => { - let deviceAreaId: string | null | undefined = undefined; + if ('entry_id' in entry) { + return false; + } + const entryObject = entry as EntityRegistryEntry; + let deviceAreaId: string | null | undefined = undefined; + // Retrieve the device area ID only if expandToDevice is true if (expandToDevice && entryObject.device_id) { deviceAreaId = Registry.devices.find((device) => device.id === entryObject.device_id)?.area_id; @@ -104,6 +115,7 @@ class RegistryFilter { }; this.filters.push(this.checkInversion(predicate)); + return this; } @@ -261,7 +273,9 @@ class RegistryFilter { } const id = entry[this.entryIdentifier] as keyof StrategyConfig['card_options']; - const isHiddenByConfig = Registry.strategyOptions?.card_options?.[id]?.hidden === true; + const isHiddenByConfig = + Registry.strategyOptions.device_options['_'].hidden || + Registry.strategyOptions.card_options[id]?.hidden === true; return !isHiddenByProperty && !isHiddenByConfig; }; @@ -302,9 +316,7 @@ class RegistryFilter { const predicate = (entry: T) => { const category = 'entity_category' in entry ? entry.entity_category : undefined; const hideOption = - typeof category === 'string' - ? Registry.strategyOptions?.domains?.['_']?.[`hide_${category}_entities`] - : undefined; + typeof category === 'string' ? Registry.strategyOptions.domains['_']?.[`hide_${category}_entities`] : undefined; if (hideOption === true) { return false; diff --git a/src/utilities/auxiliaries.ts b/src/utilities/auxiliaries.ts index 233dc55..fb41ca9 100644 --- a/src/utilities/auxiliaries.ts +++ b/src/utilities/auxiliaries.ts @@ -36,3 +36,39 @@ export function deepClone(obj: T): T { return obj; } } + +/** + * Get the keys of nested objects by its property value. + * + * @param {Object} object An object of objects. + * @param {string|number} property The name of the property to evaluate. + * @param {*} value The value which the property should match. + * + * @return {string[]} An array with keys. + */ +export function getObjectKeysByPropertyValue( + object: Record, + property: string, + value: unknown, +): string[] { + const keys: string[] = []; + + for (const key of Object.keys(object)) { + if (object[key] && (object[key] as Record)[property] === value) { + keys.push(key); + } + } + + return keys; +} + +/** + * Filters out null values from an array. + * + * @template T The type of the array elements. + * @param {Array} arr The array to filter. + * @returns {Array} An array containing the non-null elements. + */ +export function filterNonNullValues(arr: (T | null)[]): T[] { + return arr.filter((item): item is T => item !== null); +} diff --git a/src/utilities/localize.ts b/src/utilities/localize.ts index 5d444c0..14a88e7 100644 --- a/src/utilities/localize.ts +++ b/src/utilities/localize.ts @@ -62,12 +62,35 @@ export default function setupCustomLocalize(hass?: HomeAssistant): void { /** * Translate a key using the globally configured localize function. + * + * @param key - The key to be translated. + * @param caseType - Optional parameter to specify the case transformation: + * - 'upper': Converts the localized key to uppercase. + * - 'lower': Converts the localized key to lowercase. + * - 'title': Converts the localized key to a title case (capitalizing the first letter of each word). + * + * @returns The translated key in the specified case format or the original key if not initialized. */ -export function localize(key: string): string { +export function localize(key: string, caseType?: 'upper' | 'lower' | 'title'): string { if (!_localize) { logMessage(lvlWarn, 'localize is not initialized! Call setupCustomLocalize first.'); - return key; } - return _localize(key); + + const localizedKey = _localize(key); + + // Transform the case based on the caseType parameter + switch (caseType) { + case 'upper': + return localizedKey.toUpperCase(); + case 'lower': + return localizedKey.toLowerCase(); + case 'title': + return localizedKey + .split(' ') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); + } + + return localizedKey; // Return the original localized key if no caseType is specified } diff --git a/src/views/CameraView.ts b/src/views/CameraView.ts index 0674cac..9d959d8 100644 --- a/src/views/CameraView.ts +++ b/src/views/CameraView.ts @@ -1,11 +1,11 @@ // noinspection JSUnusedGlobalSymbols Class is dynamically imported. import { Registry } from '../Registry'; -import { CustomHeaderCardConfig } from '../types/strategy/strategy-cards'; -import { SupportedDomains } from '../types/strategy/strategy-generics'; +import { SingleDomainConfig, SupportedDomains } from '../types/strategy/strategy-generics'; import { ViewConfig } from '../types/strategy/strategy-views'; import { localize } from '../utilities/localize'; import AbstractView from './AbstractView'; +import { HeaderCardConfig } from '../types/strategy/strategy-cards'; /** * Camera View Class. @@ -18,19 +18,23 @@ class CameraView extends AbstractView { /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { + const domainConfig = Registry.strategyOptions.domains[CameraView.domain] as SingleDomainConfig; + return { - title: localize('camera.cameras'), + title: domainConfig.title, path: 'cameras', icon: 'mdi:cctv', subview: false, headerCardConfiguration: { - showControls: false, + showControls: domainConfig.showControls, + on: domainConfig.on, + off: domainConfig.off, }, }; } /** Returns the default configuration of the view's Header card. */ - static getViewHeaderCardConfig(): CustomHeaderCardConfig { + static getViewHeaderCardConfig(): HeaderCardConfig { return { title: localize('camera.all_cameras'), subtitle: diff --git a/src/views/ClimateView.ts b/src/views/ClimateView.ts index 7853567..1de4928 100644 --- a/src/views/ClimateView.ts +++ b/src/views/ClimateView.ts @@ -1,11 +1,11 @@ // noinspection JSUnusedGlobalSymbols Class is dynamically imported. import { Registry } from '../Registry'; -import { CustomHeaderCardConfig } from '../types/strategy/strategy-cards'; -import { SupportedDomains } from '../types/strategy/strategy-generics'; +import { SingleDomainConfig, SupportedDomains } from '../types/strategy/strategy-generics'; import { ViewConfig } from '../types/strategy/strategy-views'; import { localize } from '../utilities/localize'; import AbstractView from './AbstractView'; +import { HeaderCardConfig } from '../types/strategy/strategy-cards'; /** * Climate View Class. @@ -18,19 +18,23 @@ class ClimateView extends AbstractView { /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { + const domainConfig = Registry.strategyOptions.domains[ClimateView.domain] as SingleDomainConfig; + return { - title: localize('climate.climates'), + title: domainConfig.title, path: 'climates', icon: 'mdi:thermostat', subview: false, headerCardConfiguration: { - showControls: false, + showControls: domainConfig.showControls, + on: domainConfig.on, + off: domainConfig.off, }, }; } /** Returns the default configuration of the view's Header card. */ - static getViewHeaderCardConfig(): CustomHeaderCardConfig { + static getViewHeaderCardConfig(): HeaderCardConfig { return { title: localize('climate.all_climates'), subtitle: diff --git a/src/views/CoverView.ts b/src/views/CoverView.ts index 78ec2f6..cecca58 100644 --- a/src/views/CoverView.ts +++ b/src/views/CoverView.ts @@ -1,11 +1,11 @@ // noinspection JSUnusedGlobalSymbols Class is dynamically imported. import { Registry } from '../Registry'; -import { CustomHeaderCardConfig } from '../types/strategy/strategy-cards'; -import { SupportedDomains } from '../types/strategy/strategy-generics'; +import { SingleDomainConfig, SupportedDomains } from '../types/strategy/strategy-generics'; import { ViewConfig } from '../types/strategy/strategy-views'; import { localize } from '../utilities/localize'; import AbstractView from './AbstractView'; +import { HeaderCardConfig } from '../types/strategy/strategy-cards'; /** * Cover View Class. @@ -18,22 +18,23 @@ class CoverView extends AbstractView { /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { + const domainConfig = Registry.strategyOptions.domains[CoverView.domain] as SingleDomainConfig; + return { - title: localize('cover.covers'), + title: domainConfig.title, path: 'covers', - icon: 'mdi:window-open', + icon: 'mdi:arrow-up-down-bold-outline', subview: false, headerCardConfiguration: { - iconOn: 'mdi:arrow-up', - iconOff: 'mdi:arrow-down', - onService: 'cover.open_cover', - offService: 'cover.close_cover', + showControls: domainConfig.showControls, + on: domainConfig.on, + off: domainConfig.off, }, }; } /** Returns the default configuration of the view's Header card. */ - static getViewHeaderCardConfig(): CustomHeaderCardConfig { + static getViewHeaderCardConfig(): HeaderCardConfig { return { title: localize('cover.all_covers'), subtitle: diff --git a/src/views/FanView.ts b/src/views/FanView.ts index 17997bb..70af0df 100644 --- a/src/views/FanView.ts +++ b/src/views/FanView.ts @@ -1,11 +1,11 @@ // noinspection JSUnusedGlobalSymbols Class is dynamically imported. import { Registry } from '../Registry'; -import { CustomHeaderCardConfig } from '../types/strategy/strategy-cards'; -import { SupportedDomains } from '../types/strategy/strategy-generics'; +import { SingleDomainConfig, SupportedDomains } from '../types/strategy/strategy-generics'; import { ViewConfig } from '../types/strategy/strategy-views'; import { localize } from '../utilities/localize'; import AbstractView from './AbstractView'; +import { HeaderCardConfig } from '../types/strategy/strategy-cards'; /** * Fan View Class. @@ -18,22 +18,23 @@ class FanView extends AbstractView { /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { + const domainConfig = Registry.strategyOptions.domains[FanView.domain] as SingleDomainConfig; + return { - title: localize('fan.fans'), + title: domainConfig.title, path: 'fans', icon: 'mdi:fan', subview: false, headerCardConfiguration: { - iconOn: 'mdi:fan', - iconOff: 'mdi:fan-off', - onService: 'fan.turn_on', - offService: 'fan.turn_off', + showControls: domainConfig.showControls, + on: domainConfig.on, + off: domainConfig.off, }, }; } /** Returns the default configuration of the view's Header card. */ - static getViewHeaderCardConfig(): CustomHeaderCardConfig { + static getViewHeaderCardConfig(): HeaderCardConfig { return { title: localize('fan.all_fans'), subtitle: diff --git a/src/views/LightView.ts b/src/views/LightView.ts index 7aa0f24..e989900 100644 --- a/src/views/LightView.ts +++ b/src/views/LightView.ts @@ -1,10 +1,11 @@ // noinspection JSUnusedGlobalSymbols Class is dynamically imported. import { Registry } from '../Registry'; -import { CustomHeaderCardConfig } from '../types/strategy/strategy-cards'; import { ViewConfig } from '../types/strategy/strategy-views'; import { localize } from '../utilities/localize'; import AbstractView from './AbstractView'; +import { HeaderCardConfig } from '../types/strategy/strategy-cards'; +import { SingleDomainConfig } from '../types/strategy/strategy-generics'; /** * Light View Class. @@ -20,22 +21,23 @@ class LightView extends AbstractView { /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { + const domainConfig = Registry.strategyOptions.domains[LightView.domain] as SingleDomainConfig; + return { - title: localize('light.lights'), + title: domainConfig.title, path: 'lights', icon: 'mdi:lightbulb-group', subview: false, headerCardConfiguration: { - iconOn: 'mdi:lightbulb', - iconOff: 'mdi:lightbulb-off', - onService: 'light.turn_on', - offService: 'light.turn_off', + showControls: domainConfig.showControls, + on: domainConfig.on, + off: domainConfig.off, }, }; } /** Returns the default configuration of the view's Header card. */ - static getViewHeaderCardConfig(): CustomHeaderCardConfig { + static getViewHeaderCardConfig(): HeaderCardConfig { return { title: localize('light.all_lights'), subtitle: diff --git a/src/views/LockView.ts b/src/views/LockView.ts index 9011b16..3010c03 100644 --- a/src/views/LockView.ts +++ b/src/views/LockView.ts @@ -1,10 +1,11 @@ // noinspection JSUnusedGlobalSymbols Class is dynamically imported. import { Registry } from '../Registry'; -import { CustomHeaderCardConfig } from '../types/strategy/strategy-cards'; import { ViewConfig } from '../types/strategy/strategy-views'; import { localize } from '../utilities/localize'; import AbstractView from './AbstractView'; +import { HeaderCardConfig } from '../types/strategy/strategy-cards'; +import { SingleDomainConfig } from '../types/strategy/strategy-generics'; /** * Lock View Class. @@ -17,22 +18,23 @@ class LockView extends AbstractView { /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { + const domainConfig = Registry.strategyOptions.domains[LockView.domain] as SingleDomainConfig; + return { - title: localize('locks.locks'), + title: domainConfig.title, path: 'locks', icon: 'mdi:lock-open', subview: false, headerCardConfiguration: { - iconOn: 'mdi:lock-open', - iconOff: 'mdi:lock', - onService: 'lock.lock', - offService: 'lock.unlock', + showControls: domainConfig.showControls, + on: domainConfig.on, + off: domainConfig.off, }, }; } /** Returns the default configuration of the view's Header card. */ - static getViewHeaderCardConfig(): CustomHeaderCardConfig { + static getViewHeaderCardConfig(): HeaderCardConfig { return { title: localize('lock.all_locks'), subtitle: diff --git a/src/views/SceneView.ts b/src/views/SceneView.ts index f741415..8951d40 100644 --- a/src/views/SceneView.ts +++ b/src/views/SceneView.ts @@ -1,9 +1,10 @@ // noinspection JSUnusedGlobalSymbols Class is dynamically imported. -import { CustomHeaderCardConfig } from '../types/strategy/strategy-cards'; import { ViewConfig } from '../types/strategy/strategy-views'; -import { localize } from '../utilities/localize'; import AbstractView from './AbstractView'; +import { HeaderCardConfig } from '../types/strategy/strategy-cards'; +import { Registry } from '../Registry'; +import { SingleDomainConfig } from '../types/strategy/strategy-generics'; /** * Scene View Class. @@ -16,19 +17,23 @@ class SceneView extends AbstractView { /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { + const domainConfig = Registry.strategyOptions.domains[SceneView.domain] as SingleDomainConfig; + return { - title: localize('scene.scenes'), + title: domainConfig.title, path: 'scenes', icon: 'mdi:palette', subview: false, headerCardConfiguration: { - showControls: false, + showControls: domainConfig.showControls, + on: domainConfig.on, + off: domainConfig.off, }, }; } /** Returns the default configuration of the view's Header card. */ - static getViewHeaderCardConfig(): CustomHeaderCardConfig { + static getViewHeaderCardConfig(): HeaderCardConfig { return {}; } diff --git a/src/views/SwitchView.ts b/src/views/SwitchView.ts index bb10dc1..1b0a6b6 100644 --- a/src/views/SwitchView.ts +++ b/src/views/SwitchView.ts @@ -1,10 +1,11 @@ // noinspection JSUnusedGlobalSymbols Class is dynamically imported. import { Registry } from '../Registry'; -import { CustomHeaderCardConfig } from '../types/strategy/strategy-cards'; import { ViewConfig } from '../types/strategy/strategy-views'; import { localize } from '../utilities/localize'; import AbstractView from './AbstractView'; +import { HeaderCardConfig } from '../types/strategy/strategy-cards'; +import { SingleDomainConfig } from '../types/strategy/strategy-generics'; /** * Switch View Class. @@ -17,22 +18,23 @@ class SwitchView extends AbstractView { /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { + const domainConfig = Registry.strategyOptions.domains[SwitchView.domain] as SingleDomainConfig; + return { - title: localize('switch.switches'), + title: domainConfig.title, path: 'switches', icon: 'mdi:dip-switch', subview: false, headerCardConfiguration: { - iconOn: 'mdi:power-plug', - iconOff: 'mdi:power-plug-off', - onService: 'switch.turn_on', - offService: 'switch.turn_off', + showControls: domainConfig.showControls, + on: domainConfig.on, + off: domainConfig.off, }, }; } /** Returns the default configuration of the view's Header card. */ - static getViewHeaderCardConfig(): CustomHeaderCardConfig { + static getViewHeaderCardConfig(): HeaderCardConfig { return { title: localize('switch.all_switches'), subtitle: diff --git a/src/views/VacuumView.ts b/src/views/VacuumView.ts index 050a58e..4aea269 100644 --- a/src/views/VacuumView.ts +++ b/src/views/VacuumView.ts @@ -1,10 +1,11 @@ // noinspection JSUnusedGlobalSymbols Class is dynamically imported. import { Registry } from '../Registry'; -import { CustomHeaderCardConfig } from '../types/strategy/strategy-cards'; import { ViewConfig } from '../types/strategy/strategy-views'; import { localize } from '../utilities/localize'; import AbstractView from './AbstractView'; +import { HeaderCardConfig } from '../types/strategy/strategy-cards'; +import { SingleDomainConfig } from '../types/strategy/strategy-generics'; /** * Vacuum View Class. @@ -17,22 +18,23 @@ class VacuumView extends AbstractView { /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { + const domainConfig = Registry.strategyOptions.domains[VacuumView.domain] as SingleDomainConfig; + return { - title: localize('vacuum.vacuums'), + title: domainConfig.title, path: 'vacuums', icon: 'mdi:robot-vacuum', subview: false, headerCardConfiguration: { - iconOn: 'mdi:robot-vacuum', - iconOff: 'mdi:robot-vacuum-off', - onService: 'vacuum.start', - offService: 'vacuum.stop', + showControls: domainConfig.showControls, + on: domainConfig.on, + off: domainConfig.off, }, }; } /** Returns the default configuration of the view's Header card. */ - static getViewHeaderCardConfig(): CustomHeaderCardConfig { + static getViewHeaderCardConfig(): HeaderCardConfig { return { title: localize('vacuum.all_vacuums'), subtitle: