diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 4f099ea..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "root": true, - "env": { - "es2020": true, - "node": true - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 2020, - "project": "./tsconfig.json", - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended" - ], - "ignorePatterns": [ - "dist/", - "node_modules/", - "*.js", - "src/types/homeassistant/", - "src/types/lovelace-mushroom/" - ], - "overrides": [ - { - "files": [ - "webpack.config.ts", - "webpack.dev.config.ts" - ], - "parserOptions": { - "project": null - } - } - ], - "rules": { - "@typescript-eslint/no-empty-function": "warn", - "@typescript-eslint/no-unused-vars": [ - "warn", - { - "argsIgnorePattern": "^_" - } - ], - "comma-dangle": [ - "error", - "always-multiline" - ], - "max-len": [ - "warn", - { - "code": 120 - } - ], - "no-console": "off", - "no-empty-function": "off", - "no-unused-vars": "off", - "quotes": [ - "error", - "single", - { - "avoidEscape": true - } - ], - "semi": [ - "error", - "always" - ] - } -} diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index a539189..2adfcd8 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -5,7 +5,7 @@ body: - type: markdown attributes: value: | - Thank you for taking the time to suggest a new feature! + Thank you for taking the time to suggest a new feature! Please provide as much detail as possible so we can understand your idea and its potential impact. - type: input @@ -58,7 +58,7 @@ body: - type: input id: affected-area-other attributes: - label: Other Affected Area (if selected above) + label: The Other Affected Area (if selected above) validations: required: false diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..e08c9e2 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,97 @@ +import { defineConfig, globalIgnores } from 'eslint/config'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import globals from 'globals'; +import tsParser from '@typescript-eslint/parser'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import js from '@eslint/js'; +import { FlatCompat } from '@eslint/eslintrc'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default defineConfig([ + globalIgnores([ + '**/dist/', + '**/node_modules/', + '**/*.js', + 'src/types/homeassistant/', + 'src/types/lovelace-mushroom/', + ]), + { + extends: compat.extends( + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ), + + plugins: { + '@typescript-eslint': typescriptEslint, + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + parser: tsParser, + ecmaVersion: 2020, + sourceType: 'module', + + parserOptions: { + project: ['./tsconfig.json', './tsconfig.eslint.json'], + }, + }, + + rules: { + '@typescript-eslint/no-empty-function': 'warn', + + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + }, + ], + + 'comma-dangle': ['error', 'always-multiline'], + + 'max-len': [ + 'warn', + { + code: 120, + }, + ], + + 'no-console': 'off', + 'no-empty-function': 'off', + 'no-unused-vars': 'off', + + quotes: [ + 'error', + 'single', + { + avoidEscape: true, + }, + ], + + semi: ['error', 'always'], + }, + }, + { + files: ['**/webpack.config.ts', '**/webpack.dev.config.ts'], + + languageOptions: { + ecmaVersion: 5, + sourceType: 'script', + + parserOptions: { + project: null, + }, + }, + }, +]); diff --git a/package-lock.json b/package-lock.json index 72bdd5f..c306198 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,17 +6,20 @@ "packages": { "": { "name": "mushroom-strategy", - "version": "2.3.2", + "version": "2.3.3-alpha.1", "license": "MIT", "dependencies": { "deepmerge": "^4" }, "devDependencies": { + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.27.0", "@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/parser": "^8.32.1", "eslint": "^9.27.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-prettier": "^5.4.0", + "globals": "^16.1.0", "home-assistant-js-websocket": "^9.5.0", "prettier": "^3.5.3", "superstruct": "^2.0.2", @@ -190,6 +193,19 @@ "concat-map": "0.0.1" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2104,9 +2120,9 @@ "license": "BSD-2-Clause" }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", + "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 151e387..2da9dca 100644 --- a/package.json +++ b/package.json @@ -30,11 +30,14 @@ "deepmerge": "^4" }, "devDependencies": { + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.27.0", "@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/parser": "^8.32.1", "eslint": "^9.27.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-prettier": "^5.4.0", + "globals": "^16.1.0", "home-assistant-js-websocket": "^9.5.0", "prettier": "^3.5.3", "superstruct": "^2.0.2", diff --git a/src/Registry.ts b/src/Registry.ts index 37c49b8..86dd49d 100644 --- a/src/Registry.ts +++ b/src/Registry.ts @@ -13,7 +13,7 @@ import { SupportedDomains, SupportedViews, } from './types/strategy/strategy-generics'; -import { logMessage, lvlFatal, lvlOff, lvlWarn, setDebugLevel } from './utilities/debug'; +import { logMessage, lvlFatal, lvlOff, setDebugLevel } from './utilities/debug'; import setupCustomLocalize from './utilities/localize'; import RegistryFilter from './utilities/RegistryFilter'; import { getObjectKeysByPropertyValue } from './utilities/auxiliaries'; @@ -26,30 +26,9 @@ import { isSortable } from './types/strategy/type-guards'; * Contains the entries of Home Assistant's registries and Strategy configuration. */ class Registry { - /** Entries of Home Assistant's entity registry. */ - private static _entities: EntityRegistryEntry[]; - /** Entries of Home Assistant's device registry. */ - private static _devices: DeviceRegistryEntry[]; - /** Entries of Home Assistant's area registry. */ - private static _areas: StrategyArea[] = []; - /** Entries of Home Assistant's state registry */ - private static _hassStates: HassEntities; - /** Entries of Home Assistant's config registry */ - private static _configEntries: ConfigEntry[] = []; - /** The Custom strategy configuration. */ - private static _strategyOptions: StrategyConfig; - /** Indicates whether this module is initialized. */ - private static _initialized: boolean = false; /** Indicates whether dark mode is enabled */ static darkMode: boolean; - /** - * Home Assistant's Config Entries. - */ - static get configEntries() { - return Registry._configEntries; - } - /** * Class constructor. * @@ -61,16 +40,42 @@ class Registry { // eslint-disable-next-line @typescript-eslint/no-empty-function private constructor() {} - private static _groupingDeviceIds: Set; + /** Entries of Home Assistant's device registry. */ + private static _devices: DeviceRegistryEntry[]; + /** Entries of Home Assistant's state registry */ + private static _hassStates: HassEntities; + /** Indicates whether this module is initialized. */ + private static _initialized: boolean = false; - /** Get the initialization status of the Registry class. */ - static get groupingDeviceIds() { - return Registry._groupingDeviceIds; + /** + * Home Assistant's Device registry. + * + * @remarks + * This module makes changes to the registry at {@link Registry.initialize}. + */ + static get devices() { + return Registry._devices; } - /** The configuration of the strategy. */ - static get strategyOptions() { - return Registry._strategyOptions; + /** Entries of Home Assistant's entity registry. */ + private static _entities: EntityRegistryEntry[]; + + /** + * Home Assistant's Entity registry. + * + * @remarks + * This module makes changes to the registry at {@link Registry.initialize}. + */ + static get entities() { + return Registry._entities; + } + + /** Entries of Home Assistant's area registry. */ + private static _areas: StrategyArea[] = []; + + /** Home Assistant's State registry. */ + static get hassStates() { + return Registry._hassStates; } /** @@ -83,29 +88,22 @@ class Registry { return Registry._areas; } - /** - * Home Assistant's Device registry. - * - * @remarks - * This module makes changes to the registry at {@link Registry.initialize}. - */ - static get devices() { - return Registry._devices; - } + /** Entries of Home Assistant's config registry */ + private static _configEntries: ConfigEntry[] = []; /** - * Home Assistant's Entity registry. - * - * @remarks - * This module makes changes to the registry at {@link Registry.initialize}. + * Home Assistant's Config Entries. */ - static get entities() { - return Registry._entities; + static get configEntries() { + return Registry._configEntries; } - /** Home Assistant's State registry. */ - static get hassStates() { - return Registry._hassStates; + /** The Custom strategy configuration. */ + private static _strategyOptions: StrategyConfig; + + /** The configuration of the strategy. */ + static get strategyOptions() { + return Registry._strategyOptions; } /** Get the initialization status of the Registry class. */ @@ -113,6 +111,13 @@ class Registry { return Registry._initialized; } + private static _groupingDeviceIds: Set; + + /** Get the initialization status of the Registry class. */ + static get groupingDeviceIds() { + return Registry._groupingDeviceIds; + } + /** * Initialize this module. * diff --git a/src/cards/AbstractCard.ts b/src/cards/AbstractCard.ts index a76a5f6..1c628ae 100644 --- a/src/cards/AbstractCard.ts +++ b/src/cards/AbstractCard.ts @@ -36,7 +36,7 @@ abstract class AbstractCard { */ protected constructor(entity: RegistryEntry) { if (!Registry.initialized) { - logMessage(lvlFatal, 'Registry not initialized!'); + logMessage(lvlFatal, 'Registry is not initialized!'); } this.entity = entity; diff --git a/src/cards/FanCard.ts b/src/cards/FanCard.ts index 90fee9d..3a14def 100644 --- a/src/cards/FanCard.ts +++ b/src/cards/FanCard.ts @@ -10,6 +10,18 @@ import AbstractCard from './AbstractCard'; * Used to create a card configuration to control an entity of the fan domain. */ class FanCard extends AbstractCard { + /** + * Class constructor. + * + * @param {EntityRegistryEntry} entity The HASS entity to create a card configuration for. + * @param {FanCardConfig} [customConfiguration] Custom card configuration. + */ + constructor(entity: EntityRegistryEntry, customConfiguration?: FanCardConfig) { + super(entity); + + this.configuration = { ...this.configuration, ...FanCard.getDefaultConfig(), ...customConfiguration }; + } + /** Returns the default configuration object for the card. */ static getDefaultConfig(): FanCardConfig { return { @@ -22,18 +34,6 @@ class FanCard extends AbstractCard { show_percentage_control: true, }; } - - /** - * Class constructor. - * - * @param {EntityRegistryEntry} entity The HASS entity to create a card configuration for. - * @param {FanCardConfig} [customConfiguration] Custom card configuration. - */ - constructor(entity: EntityRegistryEntry, customConfiguration?: FanCardConfig) { - super(entity); - - this.configuration = { ...this.configuration, ...FanCard.getDefaultConfig(), ...customConfiguration }; - } } export default FanCard; diff --git a/src/cards/LightCard.ts b/src/cards/LightCard.ts index c3075bc..c7df0d1 100644 --- a/src/cards/LightCard.ts +++ b/src/cards/LightCard.ts @@ -11,6 +11,24 @@ import { isCallServiceActionConfig } from '../types/strategy/type-guards'; * Used to create a card configuration to control an entity of the light domain. */ class LightCard extends AbstractCard { + /** + * Class constructor. + * + * @param {EntityRegistryEntry} entity The HASS entity to create a card configuration for. + * @param {LightCardConfig} [customConfiguration] Custom card configuration. + */ + constructor(entity: EntityRegistryEntry, customConfiguration?: LightCardConfig) { + super(entity); + + const configuration = LightCard.getDefaultConfig(); + + if (isCallServiceActionConfig(configuration.double_tap_action)) { + configuration.double_tap_action.target = { entity_id: entity.entity_id }; + } + + this.configuration = { ...this.configuration, ...configuration, ...customConfiguration }; + } + /** Returns the default configuration object for the card. */ static getDefaultConfig(): LightCardConfig { return { @@ -32,24 +50,6 @@ class LightCard extends AbstractCard { }, }; } - - /** - * Class constructor. - * - * @param {EntityRegistryEntry} entity The HASS entity to create a card configuration for. - * @param {LightCardConfig} [customConfiguration] Custom card configuration. - */ - constructor(entity: EntityRegistryEntry, customConfiguration?: LightCardConfig) { - super(entity); - - const configuration = LightCard.getDefaultConfig(); - - if (isCallServiceActionConfig(configuration.double_tap_action)) { - configuration.double_tap_action.target = { entity_id: entity.entity_id }; - } - - this.configuration = { ...this.configuration, ...configuration, ...customConfiguration }; - } } export default LightCard; diff --git a/src/cards/MiscellaneousCard.ts b/src/cards/MiscellaneousCard.ts index 3c98395..bc62b80 100644 --- a/src/cards/MiscellaneousCard.ts +++ b/src/cards/MiscellaneousCard.ts @@ -1,3 +1,5 @@ +// noinspection JSUnusedGlobalSymbols Class is dynamically imported. + import { EntityRegistryEntry } from '../types/homeassistant/data/entity_registry'; import { EntityCardConfig } from '../types/lovelace-mushroom/cards/entity-card-config'; import AbstractCard from './AbstractCard'; diff --git a/src/cards/SceneCard.ts b/src/cards/SceneCard.ts index 78827b3..3096cc6 100644 --- a/src/cards/SceneCard.ts +++ b/src/cards/SceneCard.ts @@ -16,18 +16,6 @@ import { isCallServiceActionConfig } from '../types/strategy/type-guards'; * If the stateful scene entity is available, it will be used instead of the original scene entity. */ class SceneCard extends AbstractCard { - /** Returns the default configuration object for the card. */ - static getDefaultConfig(): EntityCardConfig { - return { - type: 'custom:mushroom-entity-card', - tap_action: { - action: 'perform-action', - perform_action: 'scene.turn_on', - target: {}, - }, - }; - } - /** * Class constructor. * @@ -58,6 +46,18 @@ class SceneCard extends AbstractCard { this.configuration = { ...this.configuration, ...configuration, ...customConfiguration }; } + + /** Returns the default configuration object for the card. */ + static getDefaultConfig(): EntityCardConfig { + return { + type: 'custom:mushroom-entity-card', + tap_action: { + action: 'perform-action', + perform_action: 'scene.turn_on', + target: {}, + }, + }; + } } export default SceneCard; diff --git a/src/chips/AbstractChip.ts b/src/chips/AbstractChip.ts index d330508..e33e66d 100644 --- a/src/chips/AbstractChip.ts +++ b/src/chips/AbstractChip.ts @@ -31,7 +31,7 @@ abstract class AbstractChip { */ protected constructor() { if (!Registry.initialized) { - logMessage(lvlFatal, 'Registry not initialized!'); + logMessage(lvlFatal, 'Registry is not initialized!'); } } diff --git a/src/generators/AreaCardsGenerator.ts b/src/generators/AreaCardsGenerator.ts index b9e2d5c..a66a188 100644 --- a/src/generators/AreaCardsGenerator.ts +++ b/src/generators/AreaCardsGenerator.ts @@ -43,14 +43,19 @@ class AreaCardsGenerator extends DomainCardsGenerator { const domainCardsPromises = [...this.domains] .filter((domain) => isSupportedDomain(domain) && domain !== 'sensor') .map((domain) => this.createSupportedDomainCards(domain)); - const supportedDomainCards = filterNonNullValues(await Promise.all(domainCardsPromises)); + const supportedDomainCards = await Promise.all(domainCardsPromises); + + filterNonNullValues(supportedDomainCards); if (sensorCards) { const insertIndex = supportedDomainCards.findIndex((card) => card.strategy.domain > 'sensor'); supportedDomainCards.splice(insertIndex, 0, sensorCards); } - return filterNonNullValues([deviceCards, ...supportedDomainCards, miscellaneousCards]); + const viewCards = [deviceCards, ...supportedDomainCards, miscellaneousCards]; + filterNonNullValues(viewCards); + + return viewCards; } catch (e) { logMessage(lvlError, 'Error creating area cards', e); return []; diff --git a/src/generators/DeviceCardsGenerator.ts b/src/generators/DeviceCardsGenerator.ts index 1f5ea69..11f7d0f 100644 --- a/src/generators/DeviceCardsGenerator.ts +++ b/src/generators/DeviceCardsGenerator.ts @@ -34,18 +34,23 @@ class DeviceCardsGenerator extends DomainCardsGenerator { .filter((domain) => isSupportedDomain(domain) && domain !== 'sensor') .map((domain) => this.createSupportedDomainCards(domain)); - const supportedDomainCards = filterNonNullValues(await Promise.all(domainCardsPromises)); + const supportedDomainCards = await Promise.all(domainCardsPromises); const [sensorCards, miscellaneousCards] = await Promise.all([ this.createSensorCards(), this.createMiscellaneousCards(), ]); + filterNonNullValues(supportedDomainCards); + if (sensorCards) { const insertIndex = supportedDomainCards.findIndex((card) => card.strategy.domain > 'sensor'); supportedDomainCards.splice(insertIndex, 0, sensorCards); } - return filterNonNullValues([...supportedDomainCards, miscellaneousCards]); + const viewCards = [...supportedDomainCards, miscellaneousCards]; + filterNonNullValues(viewCards); + + return viewCards; } catch (e) { logMessage(lvlError, 'Error creating device cards', e); return []; diff --git a/src/generators/domainCardsGenerator.ts b/src/generators/domainCardsGenerator.ts index ffd683f..6ce35d5 100644 --- a/src/generators/domainCardsGenerator.ts +++ b/src/generators/domainCardsGenerator.ts @@ -74,7 +74,7 @@ abstract class DomainCardsGenerator { return deviceCard; } catch (e) { - logMessage(lvlError, `Error creating card for device with id ${device.id}`, e); + logMessage(lvlError, `Error creating card for the device with id ${device.id}`, e); return null; } @@ -88,7 +88,7 @@ abstract class DomainCardsGenerator { showControls: false, }, ).createCard(); - + // TODO: add horizontal stacking return { type: 'vertical-stack', cards: [headerCard, ...cards.filter((card): card is LovelaceCardConfig => card !== null)], @@ -113,7 +113,7 @@ abstract class DomainCardsGenerator { return null; } - const cards = await Promise.all( + let cards = await Promise.all( entities.map(async (entity) => { return this.createEntityCard(entity, 'SensorCard', { ...Registry.strategyOptions.card_options[entity.entity_id], @@ -123,13 +123,24 @@ abstract class DomainCardsGenerator { }), ); - const headerCard = new HeaderCard({}, Registry.strategyOptions.domains['sensor']).createCard(); + filterNonNullValues(cards); - return { - type: 'vertical-stack', - cards: [headerCard, ...cards.filter((card): card is LovelaceCardConfig => card !== null)], - strategy: { domain: 'sensor' }, - }; + if (cards.length) { + const headerCard = new HeaderCard({}, Registry.strategyOptions.domains['sensor']).createCard(); + + cards = stackHorizontal( + cards, + Registry.strategyOptions.domains['sensor'].stack_count ?? Registry.strategyOptions.domains['_'].stack_count, + ); + + return { + type: 'vertical-stack', + cards: [headerCard, ...cards], + strategy: { domain: 'sensor' }, + }; + } + + return null; } /** @@ -147,11 +158,11 @@ abstract class DomainCardsGenerator { .toList(); if (!entities.length) { - logMessage(lvlInfo, `No sensors available for view of ${this.parent.type} ${this.parent.id}.`); + logMessage(lvlInfo, `No entities available for view of ${this.parent.type} ${this.parent.id}.`); return null; } - const cards = await Promise.all( + let cards = await Promise.all( entities.map(async (entity) => { return this.createEntityCard( entity, @@ -161,13 +172,24 @@ abstract class DomainCardsGenerator { }), ); - const headerCard = new HeaderCard({}, { title: Registry.strategyOptions.domains['default'].title }).createCard(); + filterNonNullValues(cards); - return { - type: 'vertical-stack', - cards: [headerCard, ...cards.filter((card): card is LovelaceCardConfig => card !== null)], - strategy: { domain: 'default' }, - }; + if (cards.length) { + const headerCard = new HeaderCard({}, { title: Registry.strategyOptions.domains['default'].title }).createCard(); + + cards = stackHorizontal( + cards, + Registry.strategyOptions.domains['default'].stack_count ?? Registry.strategyOptions.domains['_'].stack_count, + ); + + return { + type: 'vertical-stack', + cards: [headerCard, ...cards], + strategy: { domain: 'default' }, + }; + } + + return null; } /** @@ -191,7 +213,7 @@ abstract class DomainCardsGenerator { return null; } - let cards: (LovelaceCardConfig | null)[] = await Promise.all( + let cards = await Promise.all( entities.map(async (entity) => { targets.push(entity.entity_id); @@ -203,20 +225,27 @@ abstract class DomainCardsGenerator { }), ); - if (domainName === 'binary_sensor') { - cards = stackHorizontal(filterNonNullValues(cards)); + filterNonNullValues(cards); + + if (cards.length) { + const headerCard = new HeaderCard( + { entity_id: targets }, + Registry.strategyOptions.domains[domainName], + ).createCard(); + + cards = stackHorizontal( + cards, + Registry.strategyOptions.domains[domainName].stack_count ?? Registry.strategyOptions.domains['_'].stack_count, + ); + + return { + type: 'vertical-stack', + cards: [headerCard, ...cards], + strategy: { domain: domainName }, + }; } - const headerCard = new HeaderCard( - { entity_id: targets }, - Registry.strategyOptions.domains[domainName], - ).createCard(); - - return { - type: 'vertical-stack', - cards: [headerCard, ...cards], - strategy: { domain: domainName }, - }; + return null; } /** diff --git a/src/mushroom-strategy.ts b/src/mushroom-strategy.ts index 6d02d63..8820088 100644 --- a/src/mushroom-strategy.ts +++ b/src/mushroom-strategy.ts @@ -1,6 +1,5 @@ import { Registry } from './Registry'; import { LovelaceConfig } from './types/homeassistant/data/lovelace/config/types'; -import { LovelaceViewConfig } from './types/homeassistant/data/lovelace/config/view'; import { DashboardInfo, isSupportedView } from './types/strategy/strategy-generics'; import { filterNonNullValues, sanitizeClassName } from './utilities/auxiliaries'; import { logMessage, lvlError, lvlFatal } from './utilities/debug'; @@ -54,7 +53,8 @@ class MushroomStrategy extends HTMLTemplateElement { return null; }); - const views = filterNonNullValues(await Promise.all(viewPromises)) as LovelaceViewConfig[]; + const views = await Promise.all(viewPromises); + filterNonNullValues(views); // Device views. const devices = new RegistryFilter(Registry.devices) diff --git a/src/types/strategy/strategy-generics.ts b/src/types/strategy/strategy-generics.ts index 7e2784f..2fa8774 100644 --- a/src/types/strategy/strategy-generics.ts +++ b/src/types/strategy/strategy-generics.ts @@ -285,19 +285,13 @@ export interface StrategyConfig { extra_cards: LovelaceCardConfig[]; extra_views: StrategyViewConfig[]; home_view: { - hidden: HomeViewSections[] | []; + hidden: HomeViewSections[]; + stack_count: { _: number } & { [K in HomeViewSections]?: K extends 'areas' ? [number, number] : number }; }; views: Record; quick_access_cards: LovelaceCardConfig[]; } -/** - * Represents the default configuration for a strategy. - */ -export interface StrategyDefaults extends StrategyConfig { - areas: { undisclosed: StrategyArea } & { [S: string]: StrategyArea }; -} - /** * Base interface for sortable items. * diff --git a/src/utilities/RegistryFilter.ts b/src/utilities/RegistryFilter.ts index 87a8933..70b8c15 100644 --- a/src/utilities/RegistryFilter.ts +++ b/src/utilities/RegistryFilter.ts @@ -86,14 +86,14 @@ class RegistryFilter { } if (areaId === undefined) { - return entry.area_id === undefined && deviceAreaId === undefined; + return entryObject.area_id === undefined && deviceAreaId === undefined; } - if (entry.area_id === 'undisclosed' || !entry.area_id) { + if (entryObject.area_id === 'undisclosed' || !entryObject.area_id) { return deviceAreaId === areaId; } - return entry.area_id === areaId; + return entryObject.area_id === areaId; }; this.filters.push(this.checkInversion(predicate)); diff --git a/src/utilities/auxiliaries.ts b/src/utilities/auxiliaries.ts index fb41ca9..b2ec42c 100644 --- a/src/utilities/auxiliaries.ts +++ b/src/utilities/auxiliaries.ts @@ -7,6 +7,7 @@ * @param {string} className Name of the class to sanitize. */ export function sanitizeClassName(className: string): string { + //TODO: In place sanitization. return className.replace(/^([a-z])|([-_][a-z])/g, (match) => match.toUpperCase().replace(/[-_]/g, '')); } @@ -22,6 +23,7 @@ export function sanitizeClassName(className: string): string { * @returns {T} A deep clone of the input value, or the original value if cloning fails. */ export function deepClone(obj: T): T { + // TODO: In place clone. if (typeof structuredClone === 'function') { try { return structuredClone(obj); @@ -63,12 +65,15 @@ export function getObjectKeysByPropertyValue( } /** - * Filters out null values from an array. + * Filters out null values from the given array and asserts that the remaining values are non-null. + * The original array is modified in place to only contain non-null values. * - * @template T The type of the array elements. - * @param {Array} arr The array to filter. - * @returns {Array} An array containing the non-null elements. + * @template T The type of non-null elements in the array + * @param {Array<(T | null)>} arr The array to be filtered. + * @return {asserts arr is Array} The array containing non-null values of type T. */ -export function filterNonNullValues(arr: (T | null)[]): T[] { - return arr.filter((item): item is T => item !== null); +export function filterNonNullValues(arr: (T | null)[]): asserts arr is T[] { + const filtered = arr.filter((item): item is T => item !== null); + arr.length = 0; + arr.push(...filtered); } diff --git a/src/utilities/cardStacking.ts b/src/utilities/cardStacking.ts index 61b462f..6f81a38 100644 --- a/src/utilities/cardStacking.ts +++ b/src/utilities/cardStacking.ts @@ -22,6 +22,7 @@ import { StackCardConfig } from '../types/homeassistant/panels/lovelace/cards/ty * ``` */ export function stackHorizontal( + // TODO: In place stacking. cardConfigurations: LovelaceCardConfig[], defaultCount: number = 2, columnCounts?: { diff --git a/src/utilities/debug.ts b/src/utilities/debug.ts index 5886605..bef7294 100644 --- a/src/utilities/debug.ts +++ b/src/utilities/debug.ts @@ -107,7 +107,7 @@ export function setDebugLevel(level: DebugLevel) { * @throws {Error} After logging, if the level is `lvlError` or `lvlFatal`. * * @remarks - * It might be required to throw an additional Error after logging with `lvlError ` or `lvlFatal` to satify the + * It might be required to throw an additional Error after logging with `lvlError ` or `lvlFatal` to satisfy the * TypeScript compiler. */ export function logMessage(level: DebugLevel, message: string, ...details: unknown[]): void { diff --git a/src/views/CameraView.ts b/src/views/CameraView.ts index 77d6e65..5091689 100644 --- a/src/views/CameraView.ts +++ b/src/views/CameraView.ts @@ -16,6 +16,17 @@ class CameraView extends AbstractView { /** The domain of the entities that the view is representing. */ static readonly domain: SupportedDomains = 'camera' as const; + /** + * Class constructor. + * + * @param {ViewConfig} [customConfiguration] Custom view configuration. + */ + constructor(customConfiguration?: ViewConfig) { + super(); + + this.initializeViewConfig(CameraView.getDefaultConfig(), customConfiguration, CameraView.getViewHeaderCardConfig()); + } + /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { const domainConfig = Registry.strategyOptions.domains[CameraView.domain] as SingleDomainConfig; @@ -26,7 +37,8 @@ class CameraView extends AbstractView { icon: 'mdi:cctv', subview: false, headerCardConfiguration: { - showControls: domainConfig.showControls, // FIXME: This should be named "show_controls". Also in other files and Wiki. + // FIXME: This should be named "show_controls". Also in other files and Wiki. + showControls: domainConfig.showControls, on: domainConfig.on, off: domainConfig.off, }, @@ -42,17 +54,6 @@ class CameraView extends AbstractView { localize('generic.busy'), }; } - - /** - * Class constructor. - * - * @param {ViewConfig} [customConfiguration] Custom view configuration. - */ - constructor(customConfiguration?: ViewConfig) { - super(); - - this.initializeViewConfig(CameraView.getDefaultConfig(), customConfiguration, CameraView.getViewHeaderCardConfig()); - } } export default CameraView; diff --git a/src/views/ClimateView.ts b/src/views/ClimateView.ts index 1de4928..bda6b4e 100644 --- a/src/views/ClimateView.ts +++ b/src/views/ClimateView.ts @@ -16,6 +16,21 @@ class ClimateView extends AbstractView { /**The domain of the entities that the view is representing. */ static readonly domain: SupportedDomains = 'climate' as const; + /** + * Class constructor. + * + * @param {ViewConfig} [customConfiguration] Custom view configuration. + */ + constructor(customConfiguration?: ViewConfig) { + super(); + + this.initializeViewConfig( + ClimateView.getDefaultConfig(), + customConfiguration, + ClimateView.getViewHeaderCardConfig(), + ); + } + /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { const domainConfig = Registry.strategyOptions.domains[ClimateView.domain] as SingleDomainConfig; @@ -42,21 +57,6 @@ class ClimateView extends AbstractView { localize('generic.busy'), }; } - - /** - * Class constructor. - * - * @param {ViewConfig} [customConfiguration] Custom view configuration. - */ - constructor(customConfiguration?: ViewConfig) { - super(); - - this.initializeViewConfig( - ClimateView.getDefaultConfig(), - customConfiguration, - ClimateView.getViewHeaderCardConfig(), - ); - } } export default ClimateView; diff --git a/src/views/CoverView.ts b/src/views/CoverView.ts index cecca58..cc3b963 100644 --- a/src/views/CoverView.ts +++ b/src/views/CoverView.ts @@ -16,6 +16,17 @@ class CoverView extends AbstractView { /** The domain of the entities that the view is representing. */ static readonly domain: SupportedDomains = 'cover' as const; + /** + * Class constructor. + * + * @param {ViewConfig} [customConfiguration] Custom view configuration. + */ + constructor(customConfiguration?: ViewConfig) { + super(); + + this.initializeViewConfig(CoverView.getDefaultConfig(), customConfiguration, CoverView.getViewHeaderCardConfig()); + } + /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { const domainConfig = Registry.strategyOptions.domains[CoverView.domain] as SingleDomainConfig; @@ -43,17 +54,6 @@ class CoverView extends AbstractView { `${localize('generic.unclosed')}`, }; } - - /** - * Class constructor. - * - * @param {ViewConfig} [customConfiguration] Custom view configuration. - */ - constructor(customConfiguration?: ViewConfig) { - super(); - - this.initializeViewConfig(CoverView.getDefaultConfig(), customConfiguration, CoverView.getViewHeaderCardConfig()); - } } export default CoverView; diff --git a/src/views/FanView.ts b/src/views/FanView.ts index 70af0df..473e303 100644 --- a/src/views/FanView.ts +++ b/src/views/FanView.ts @@ -16,6 +16,17 @@ class FanView extends AbstractView { /** The domain of the entities that the view is representing. */ static readonly domain: SupportedDomains = 'fan' as const; + /** + * Class constructor. + * + * @param {ViewConfig} [customConfiguration] Custom view configuration. + */ + constructor(customConfiguration?: ViewConfig) { + super(); + + this.initializeViewConfig(FanView.getDefaultConfig(), customConfiguration, FanView.getViewHeaderCardConfig()); + } + /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { const domainConfig = Registry.strategyOptions.domains[FanView.domain] as SingleDomainConfig; @@ -41,17 +52,6 @@ class FanView extends AbstractView { `${Registry.getCountTemplate(FanView.domain, 'eq', 'on')} ${localize('fan.fans')} ` + localize('generic.on'), }; } - - /** - * Class constructor. - * - * @param {ViewConfig} [customConfiguration] Custom view configuration. - */ - constructor(customConfiguration?: ViewConfig) { - super(); - - this.initializeViewConfig(FanView.getDefaultConfig(), customConfiguration, FanView.getViewHeaderCardConfig()); - } } export default FanView; diff --git a/src/views/HomeView.ts b/src/views/HomeView.ts index f199c55..fabf07b 100644 --- a/src/views/HomeView.ts +++ b/src/views/HomeView.ts @@ -8,7 +8,7 @@ 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 { HomeViewSections, isSupportedChip } from '../types/strategy/strategy-generics'; +import { isSupportedChip } from '../types/strategy/strategy-generics'; import { ViewConfig } from '../types/strategy/strategy-views'; import { sanitizeClassName } from '../utilities/auxiliaries'; import { logMessage, lvlError, lvlInfo } from '../utilities/debug'; @@ -26,16 +26,6 @@ class HomeView extends AbstractView { /** The domain of the entities that the view is representing. */ static readonly domain = 'home' as const; - /** Returns the default configuration object for the view. */ - static getDefaultConfig(): ViewConfig { - return { - title: localize('generic.home'), - icon: 'mdi:home-assistant', - path: 'home', - subview: false, - }; - } - /** * Class constructor. * @@ -47,6 +37,16 @@ class HomeView extends AbstractView { this.baseConfiguration = { ...this.baseConfiguration, ...HomeView.getDefaultConfig(), ...customConfiguration }; } + /** Returns the default configuration object for the view. */ + static getDefaultConfig(): ViewConfig { + return { + title: localize('generic.home'), + icon: 'mdi:home-assistant', + path: 'home', + subview: false, + }; + } + /** * Create the configuration of the cards to include in the view. * diff --git a/src/views/LockView.ts b/src/views/LockView.ts index 3010c03..26363da 100644 --- a/src/views/LockView.ts +++ b/src/views/LockView.ts @@ -16,6 +16,17 @@ class LockView extends AbstractView { /** The domain of the entities that the view is representing. */ static readonly domain = 'lock' as const; + /** + * Class constructor. + * + * @param {ViewConfig} [customConfiguration] Custom view configuration. + */ + constructor(customConfiguration?: ViewConfig) { + super(); + + this.initializeViewConfig(LockView.getDefaultConfig(), customConfiguration, LockView.getViewHeaderCardConfig()); + } + /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { const domainConfig = Registry.strategyOptions.domains[LockView.domain] as SingleDomainConfig; @@ -42,17 +53,6 @@ class LockView extends AbstractView { localize('lock.unlocked'), }; } - - /** - * Class constructor. - * - * @param {ViewConfig} [customConfiguration] Custom view configuration. - */ - constructor(customConfiguration?: ViewConfig) { - super(); - - this.initializeViewConfig(LockView.getDefaultConfig(), customConfiguration, LockView.getViewHeaderCardConfig()); - } } export default LockView; diff --git a/src/views/SceneView.ts b/src/views/SceneView.ts index 8951d40..9b85bf3 100644 --- a/src/views/SceneView.ts +++ b/src/views/SceneView.ts @@ -15,6 +15,17 @@ class SceneView extends AbstractView { /** The domain of the entities that the view is representing. */ static readonly domain = 'scene' as const; + /** + * Class constructor. + * + * @param {ViewConfig} [customConfiguration] Custom view configuration. + */ + constructor(customConfiguration?: ViewConfig) { + super(); + + this.initializeViewConfig(SceneView.getDefaultConfig(), customConfiguration, SceneView.getViewHeaderCardConfig()); + } + /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { const domainConfig = Registry.strategyOptions.domains[SceneView.domain] as SingleDomainConfig; @@ -36,17 +47,6 @@ class SceneView extends AbstractView { static getViewHeaderCardConfig(): HeaderCardConfig { return {}; } - - /** - * Class constructor. - * - * @param {ViewConfig} [customConfiguration] Custom view configuration. - */ - constructor(customConfiguration?: ViewConfig) { - super(); - - this.initializeViewConfig(SceneView.getDefaultConfig(), customConfiguration, SceneView.getViewHeaderCardConfig()); - } } export default SceneView; diff --git a/src/views/SwitchView.ts b/src/views/SwitchView.ts index 1b0a6b6..8e41622 100644 --- a/src/views/SwitchView.ts +++ b/src/views/SwitchView.ts @@ -16,6 +16,17 @@ class SwitchView extends AbstractView { /** The domain of the entities that the view is representing. */ static readonly domain = 'switch' as const; + /** + * Class constructor. + * + * @param {ViewConfig} [customConfiguration] Custom view configuration. + */ + constructor(customConfiguration?: ViewConfig) { + super(); + + this.initializeViewConfig(SwitchView.getDefaultConfig(), customConfiguration, SwitchView.getViewHeaderCardConfig()); + } + /** Returns the default configuration object for the view. */ static getDefaultConfig(): ViewConfig { const domainConfig = Registry.strategyOptions.domains[SwitchView.domain] as SingleDomainConfig; @@ -42,17 +53,6 @@ class SwitchView extends AbstractView { localize('generic.on'), }; } - - /** - * Class constructor. - * - * @param {ViewConfig} [customConfiguration] Custom view configuration. - */ - constructor(customConfiguration?: ViewConfig) { - super(); - - this.initializeViewConfig(SwitchView.getDefaultConfig(), customConfiguration, SwitchView.getViewHeaderCardConfig()); - } } export default SwitchView; diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000..4c81748 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,12 @@ +{ + "include": [ + "eslint.config.mjs" + ], + "compilerOptions": { + "allowJs": true, + "module": "ESNext", + "moduleResolution": "Node", + "target": "ESNext", + "noEmit": true + } +}