Refactor code for compatibility

The code needed to be refactored to be compatible with the changes of
the `main` branch.
Also, the configuration for ESLint is refactored to migrate it to v9.
This commit is contained in:
DigiLive
2025-05-18 18:17:21 +02:00
parent 181e297330
commit 28d3e9d4bc
30 changed files with 419 additions and 316 deletions

View File

@@ -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"
]
}
}

View File

@@ -5,7 +5,7 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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. Please provide as much detail as possible so we can understand your idea and its potential impact.
- type: input - type: input
@@ -58,7 +58,7 @@ body:
- type: input - type: input
id: affected-area-other id: affected-area-other
attributes: attributes:
label: Other Affected Area (if selected above) label: The Other Affected Area (if selected above)
validations: validations:
required: false required: false

97
eslint.config.mjs Normal file
View File

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

24
package-lock.json generated
View File

@@ -6,17 +6,20 @@
"packages": { "packages": {
"": { "": {
"name": "mushroom-strategy", "name": "mushroom-strategy",
"version": "2.3.2", "version": "2.3.3-alpha.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"deepmerge": "^4" "deepmerge": "^4"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.27.0",
"@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1", "@typescript-eslint/parser": "^8.32.1",
"eslint": "^9.27.0", "eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.0", "eslint-plugin-prettier": "^5.4.0",
"globals": "^16.1.0",
"home-assistant-js-websocket": "^9.5.0", "home-assistant-js-websocket": "^9.5.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"superstruct": "^2.0.2", "superstruct": "^2.0.2",
@@ -190,6 +193,19 @@
"concat-map": "0.0.1" "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": { "node_modules/@eslint/eslintrc/node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -2104,9 +2120,9 @@
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/globals": { "node_modules/globals": {
"version": "14.0.0", "version": "16.1.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {

View File

@@ -30,11 +30,14 @@
"deepmerge": "^4" "deepmerge": "^4"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.27.0",
"@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1", "@typescript-eslint/parser": "^8.32.1",
"eslint": "^9.27.0", "eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.0", "eslint-plugin-prettier": "^5.4.0",
"globals": "^16.1.0",
"home-assistant-js-websocket": "^9.5.0", "home-assistant-js-websocket": "^9.5.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"superstruct": "^2.0.2", "superstruct": "^2.0.2",

View File

@@ -13,7 +13,7 @@ import {
SupportedDomains, SupportedDomains,
SupportedViews, SupportedViews,
} from './types/strategy/strategy-generics'; } 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 setupCustomLocalize from './utilities/localize';
import RegistryFilter from './utilities/RegistryFilter'; import RegistryFilter from './utilities/RegistryFilter';
import { getObjectKeysByPropertyValue } from './utilities/auxiliaries'; 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. * Contains the entries of Home Assistant's registries and Strategy configuration.
*/ */
class Registry { 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 */ /** Indicates whether dark mode is enabled */
static darkMode: boolean; static darkMode: boolean;
/**
* Home Assistant's Config Entries.
*/
static get configEntries() {
return Registry._configEntries;
}
/** /**
* Class constructor. * Class constructor.
* *
@@ -61,16 +40,42 @@ class Registry {
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {} private constructor() {}
private static _groupingDeviceIds: Set<string>; /** 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() { * Home Assistant's Device registry.
return Registry._groupingDeviceIds; *
* @remarks
* This module makes changes to the registry at {@link Registry.initialize}.
*/
static get devices() {
return Registry._devices;
} }
/** The configuration of the strategy. */ /** Entries of Home Assistant's entity registry. */
static get strategyOptions() { private static _entities: EntityRegistryEntry[];
return Registry._strategyOptions;
/**
* 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; return Registry._areas;
} }
/** /** Entries of Home Assistant's config registry */
* Home Assistant's Device registry. private static _configEntries: ConfigEntry[] = [];
*
* @remarks
* This module makes changes to the registry at {@link Registry.initialize}.
*/
static get devices() {
return Registry._devices;
}
/** /**
* Home Assistant's Entity registry. * Home Assistant's Config Entries.
*
* @remarks
* This module makes changes to the registry at {@link Registry.initialize}.
*/ */
static get entities() { static get configEntries() {
return Registry._entities; return Registry._configEntries;
} }
/** Home Assistant's State registry. */ /** The Custom strategy configuration. */
static get hassStates() { private static _strategyOptions: StrategyConfig;
return Registry._hassStates;
/** The configuration of the strategy. */
static get strategyOptions() {
return Registry._strategyOptions;
} }
/** Get the initialization status of the Registry class. */ /** Get the initialization status of the Registry class. */
@@ -113,6 +111,13 @@ class Registry {
return Registry._initialized; return Registry._initialized;
} }
private static _groupingDeviceIds: Set<string>;
/** Get the initialization status of the Registry class. */
static get groupingDeviceIds() {
return Registry._groupingDeviceIds;
}
/** /**
* Initialize this module. * Initialize this module.
* *

View File

@@ -36,7 +36,7 @@ abstract class AbstractCard {
*/ */
protected constructor(entity: RegistryEntry) { protected constructor(entity: RegistryEntry) {
if (!Registry.initialized) { if (!Registry.initialized) {
logMessage(lvlFatal, 'Registry not initialized!'); logMessage(lvlFatal, 'Registry is not initialized!');
} }
this.entity = entity; this.entity = entity;

View File

@@ -10,6 +10,18 @@ import AbstractCard from './AbstractCard';
* Used to create a card configuration to control an entity of the fan domain. * Used to create a card configuration to control an entity of the fan domain.
*/ */
class FanCard extends AbstractCard { 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. */ /** Returns the default configuration object for the card. */
static getDefaultConfig(): FanCardConfig { static getDefaultConfig(): FanCardConfig {
return { return {
@@ -22,18 +34,6 @@ class FanCard extends AbstractCard {
show_percentage_control: true, 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; export default FanCard;

View File

@@ -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. * Used to create a card configuration to control an entity of the light domain.
*/ */
class LightCard extends AbstractCard { 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. */ /** Returns the default configuration object for the card. */
static getDefaultConfig(): LightCardConfig { static getDefaultConfig(): LightCardConfig {
return { 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; export default LightCard;

View File

@@ -1,3 +1,5 @@
// noinspection JSUnusedGlobalSymbols Class is dynamically imported.
import { EntityRegistryEntry } from '../types/homeassistant/data/entity_registry'; import { EntityRegistryEntry } from '../types/homeassistant/data/entity_registry';
import { EntityCardConfig } from '../types/lovelace-mushroom/cards/entity-card-config'; import { EntityCardConfig } from '../types/lovelace-mushroom/cards/entity-card-config';
import AbstractCard from './AbstractCard'; import AbstractCard from './AbstractCard';

View File

@@ -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. * If the stateful scene entity is available, it will be used instead of the original scene entity.
*/ */
class SceneCard extends AbstractCard { 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. * Class constructor.
* *
@@ -58,6 +46,18 @@ class SceneCard extends AbstractCard {
this.configuration = { ...this.configuration, ...configuration, ...customConfiguration }; 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; export default SceneCard;

View File

@@ -31,7 +31,7 @@ abstract class AbstractChip {
*/ */
protected constructor() { protected constructor() {
if (!Registry.initialized) { if (!Registry.initialized) {
logMessage(lvlFatal, 'Registry not initialized!'); logMessage(lvlFatal, 'Registry is not initialized!');
} }
} }

View File

@@ -43,14 +43,19 @@ class AreaCardsGenerator extends DomainCardsGenerator {
const domainCardsPromises = [...this.domains] const domainCardsPromises = [...this.domains]
.filter((domain) => isSupportedDomain(domain) && domain !== 'sensor') .filter((domain) => isSupportedDomain(domain) && domain !== 'sensor')
.map((domain) => this.createSupportedDomainCards(domain)); .map((domain) => this.createSupportedDomainCards(domain));
const supportedDomainCards = filterNonNullValues(await Promise.all(domainCardsPromises)); const supportedDomainCards = await Promise.all(domainCardsPromises);
filterNonNullValues(supportedDomainCards);
if (sensorCards) { if (sensorCards) {
const insertIndex = supportedDomainCards.findIndex((card) => card.strategy.domain > 'sensor'); const insertIndex = supportedDomainCards.findIndex((card) => card.strategy.domain > 'sensor');
supportedDomainCards.splice(insertIndex, 0, sensorCards); supportedDomainCards.splice(insertIndex, 0, sensorCards);
} }
return filterNonNullValues([deviceCards, ...supportedDomainCards, miscellaneousCards]); const viewCards = [deviceCards, ...supportedDomainCards, miscellaneousCards];
filterNonNullValues(viewCards);
return viewCards;
} catch (e) { } catch (e) {
logMessage(lvlError, 'Error creating area cards', e); logMessage(lvlError, 'Error creating area cards', e);
return []; return [];

View File

@@ -34,18 +34,23 @@ class DeviceCardsGenerator extends DomainCardsGenerator {
.filter((domain) => isSupportedDomain(domain) && domain !== 'sensor') .filter((domain) => isSupportedDomain(domain) && domain !== 'sensor')
.map((domain) => this.createSupportedDomainCards(domain)); .map((domain) => this.createSupportedDomainCards(domain));
const supportedDomainCards = filterNonNullValues(await Promise.all(domainCardsPromises)); const supportedDomainCards = await Promise.all(domainCardsPromises);
const [sensorCards, miscellaneousCards] = await Promise.all([ const [sensorCards, miscellaneousCards] = await Promise.all([
this.createSensorCards(), this.createSensorCards(),
this.createMiscellaneousCards(), this.createMiscellaneousCards(),
]); ]);
filterNonNullValues(supportedDomainCards);
if (sensorCards) { if (sensorCards) {
const insertIndex = supportedDomainCards.findIndex((card) => card.strategy.domain > 'sensor'); const insertIndex = supportedDomainCards.findIndex((card) => card.strategy.domain > 'sensor');
supportedDomainCards.splice(insertIndex, 0, sensorCards); supportedDomainCards.splice(insertIndex, 0, sensorCards);
} }
return filterNonNullValues([...supportedDomainCards, miscellaneousCards]); const viewCards = [...supportedDomainCards, miscellaneousCards];
filterNonNullValues(viewCards);
return viewCards;
} catch (e) { } catch (e) {
logMessage(lvlError, 'Error creating device cards', e); logMessage(lvlError, 'Error creating device cards', e);
return []; return [];

View File

@@ -74,7 +74,7 @@ abstract class DomainCardsGenerator {
return deviceCard; return deviceCard;
} catch (e) { } 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; return null;
} }
@@ -88,7 +88,7 @@ abstract class DomainCardsGenerator {
showControls: false, showControls: false,
}, },
).createCard(); ).createCard();
// TODO: add horizontal stacking
return { return {
type: 'vertical-stack', type: 'vertical-stack',
cards: [headerCard, ...cards.filter((card): card is LovelaceCardConfig => card !== null)], cards: [headerCard, ...cards.filter((card): card is LovelaceCardConfig => card !== null)],
@@ -113,7 +113,7 @@ abstract class DomainCardsGenerator {
return null; return null;
} }
const cards = await Promise.all( let cards = await Promise.all(
entities.map(async (entity) => { entities.map(async (entity) => {
return this.createEntityCard(entity, 'SensorCard', { return this.createEntityCard(entity, 'SensorCard', {
...Registry.strategyOptions.card_options[entity.entity_id], ...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 { if (cards.length) {
type: 'vertical-stack', const headerCard = new HeaderCard({}, Registry.strategyOptions.domains['sensor']).createCard();
cards: [headerCard, ...cards.filter((card): card is LovelaceCardConfig => card !== null)],
strategy: { domain: 'sensor' }, 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(); .toList();
if (!entities.length) { 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; return null;
} }
const cards = await Promise.all( let cards = await Promise.all(
entities.map(async (entity) => { entities.map(async (entity) => {
return this.createEntityCard( return this.createEntityCard(
entity, entity,
@@ -161,13 +172,24 @@ abstract class DomainCardsGenerator {
}), }),
); );
const headerCard = new HeaderCard({}, { title: Registry.strategyOptions.domains['default'].title }).createCard(); filterNonNullValues(cards);
return { if (cards.length) {
type: 'vertical-stack', const headerCard = new HeaderCard({}, { title: Registry.strategyOptions.domains['default'].title }).createCard();
cards: [headerCard, ...cards.filter((card): card is LovelaceCardConfig => card !== null)],
strategy: { domain: 'default' }, 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; return null;
} }
let cards: (LovelaceCardConfig | null)[] = await Promise.all( let cards = await Promise.all(
entities.map(async (entity) => { entities.map(async (entity) => {
targets.push(entity.entity_id); targets.push(entity.entity_id);
@@ -203,20 +225,27 @@ abstract class DomainCardsGenerator {
}), }),
); );
if (domainName === 'binary_sensor') { filterNonNullValues(cards);
cards = stackHorizontal(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( return null;
{ entity_id: targets },
Registry.strategyOptions.domains[domainName],
).createCard();
return {
type: 'vertical-stack',
cards: [headerCard, ...cards],
strategy: { domain: domainName },
};
} }
/** /**

View File

@@ -1,6 +1,5 @@
import { Registry } from './Registry'; import { Registry } from './Registry';
import { LovelaceConfig } from './types/homeassistant/data/lovelace/config/types'; 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 { DashboardInfo, isSupportedView } from './types/strategy/strategy-generics';
import { filterNonNullValues, sanitizeClassName } from './utilities/auxiliaries'; import { filterNonNullValues, sanitizeClassName } from './utilities/auxiliaries';
import { logMessage, lvlError, lvlFatal } from './utilities/debug'; import { logMessage, lvlError, lvlFatal } from './utilities/debug';
@@ -54,7 +53,8 @@ class MushroomStrategy extends HTMLTemplateElement {
return null; return null;
}); });
const views = filterNonNullValues(await Promise.all(viewPromises)) as LovelaceViewConfig[]; const views = await Promise.all(viewPromises);
filterNonNullValues(views);
// Device views. // Device views.
const devices = new RegistryFilter(Registry.devices) const devices = new RegistryFilter(Registry.devices)

View File

@@ -285,19 +285,13 @@ export interface StrategyConfig {
extra_cards: LovelaceCardConfig[]; extra_cards: LovelaceCardConfig[];
extra_views: StrategyViewConfig[]; extra_views: StrategyViewConfig[];
home_view: { home_view: {
hidden: HomeViewSections[] | []; hidden: HomeViewSections[];
stack_count: { _: number } & { [K in HomeViewSections]?: K extends 'areas' ? [number, number] : number };
}; };
views: Record<SupportedViews, StrategyViewConfig>; views: Record<SupportedViews, StrategyViewConfig>;
quick_access_cards: LovelaceCardConfig[]; 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. * Base interface for sortable items.
* *

View File

@@ -86,14 +86,14 @@ class RegistryFilter<T extends RegistryEntry, K extends keyof T = keyof T> {
} }
if (areaId === undefined) { 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 deviceAreaId === areaId;
} }
return entry.area_id === areaId; return entryObject.area_id === areaId;
}; };
this.filters.push(this.checkInversion(predicate)); this.filters.push(this.checkInversion(predicate));

View File

@@ -7,6 +7,7 @@
* @param {string} className Name of the class to sanitize. * @param {string} className Name of the class to sanitize.
*/ */
export function sanitizeClassName(className: string): string { export function sanitizeClassName(className: string): string {
//TODO: In place sanitization.
return className.replace(/^([a-z])|([-_][a-z])/g, (match) => match.toUpperCase().replace(/[-_]/g, '')); 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. * @returns {T} A deep clone of the input value, or the original value if cloning fails.
*/ */
export function deepClone<T>(obj: T): T { export function deepClone<T>(obj: T): T {
// TODO: In place clone.
if (typeof structuredClone === 'function') { if (typeof structuredClone === 'function') {
try { try {
return structuredClone(obj); 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. * @template T The type of non-null elements in the array
* @param {Array<T | null>} arr The array to filter. * @param {Array<(T | null)>} arr The array to be filtered.
* @returns {Array<T>} An array containing the non-null elements. * @return {asserts arr is Array<T>} The array containing non-null values of type T.
*/ */
export function filterNonNullValues<T>(arr: (T | null)[]): T[] { export function filterNonNullValues<T>(arr: (T | null)[]): asserts arr is T[] {
return arr.filter((item): item is T => item !== null); const filtered = arr.filter((item): item is T => item !== null);
arr.length = 0;
arr.push(...filtered);
} }

View File

@@ -22,6 +22,7 @@ import { StackCardConfig } from '../types/homeassistant/panels/lovelace/cards/ty
* ``` * ```
*/ */
export function stackHorizontal( export function stackHorizontal(
// TODO: In place stacking.
cardConfigurations: LovelaceCardConfig[], cardConfigurations: LovelaceCardConfig[],
defaultCount: number = 2, defaultCount: number = 2,
columnCounts?: { columnCounts?: {

View File

@@ -107,7 +107,7 @@ export function setDebugLevel(level: DebugLevel) {
* @throws {Error} After logging, if the level is `lvlError` or `lvlFatal`. * @throws {Error} After logging, if the level is `lvlError` or `lvlFatal`.
* *
* @remarks * @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. * TypeScript compiler.
*/ */
export function logMessage(level: DebugLevel, message: string, ...details: unknown[]): void { export function logMessage(level: DebugLevel, message: string, ...details: unknown[]): void {

View File

@@ -16,6 +16,17 @@ class CameraView extends AbstractView {
/** The domain of the entities that the view is representing. */ /** The domain of the entities that the view is representing. */
static readonly domain: SupportedDomains = 'camera' as const; 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. */ /** Returns the default configuration object for the view. */
static getDefaultConfig(): ViewConfig { static getDefaultConfig(): ViewConfig {
const domainConfig = Registry.strategyOptions.domains[CameraView.domain] as SingleDomainConfig; const domainConfig = Registry.strategyOptions.domains[CameraView.domain] as SingleDomainConfig;
@@ -26,7 +37,8 @@ class CameraView extends AbstractView {
icon: 'mdi:cctv', icon: 'mdi:cctv',
subview: false, subview: false,
headerCardConfiguration: { 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, on: domainConfig.on,
off: domainConfig.off, off: domainConfig.off,
}, },
@@ -42,17 +54,6 @@ class CameraView extends AbstractView {
localize('generic.busy'), 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; export default CameraView;

View File

@@ -16,6 +16,21 @@ class ClimateView extends AbstractView {
/**The domain of the entities that the view is representing. */ /**The domain of the entities that the view is representing. */
static readonly domain: SupportedDomains = 'climate' as const; 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. */ /** Returns the default configuration object for the view. */
static getDefaultConfig(): ViewConfig { static getDefaultConfig(): ViewConfig {
const domainConfig = Registry.strategyOptions.domains[ClimateView.domain] as SingleDomainConfig; const domainConfig = Registry.strategyOptions.domains[ClimateView.domain] as SingleDomainConfig;
@@ -42,21 +57,6 @@ class ClimateView extends AbstractView {
localize('generic.busy'), 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; export default ClimateView;

View File

@@ -16,6 +16,17 @@ class CoverView extends AbstractView {
/** The domain of the entities that the view is representing. */ /** The domain of the entities that the view is representing. */
static readonly domain: SupportedDomains = 'cover' as const; 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. */ /** Returns the default configuration object for the view. */
static getDefaultConfig(): ViewConfig { static getDefaultConfig(): ViewConfig {
const domainConfig = Registry.strategyOptions.domains[CoverView.domain] as SingleDomainConfig; const domainConfig = Registry.strategyOptions.domains[CoverView.domain] as SingleDomainConfig;
@@ -43,17 +54,6 @@ class CoverView extends AbstractView {
`${localize('generic.unclosed')}`, `${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; export default CoverView;

View File

@@ -16,6 +16,17 @@ class FanView extends AbstractView {
/** The domain of the entities that the view is representing. */ /** The domain of the entities that the view is representing. */
static readonly domain: SupportedDomains = 'fan' as const; 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. */ /** Returns the default configuration object for the view. */
static getDefaultConfig(): ViewConfig { static getDefaultConfig(): ViewConfig {
const domainConfig = Registry.strategyOptions.domains[FanView.domain] as SingleDomainConfig; 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'), `${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; export default FanView;

View File

@@ -8,7 +8,7 @@ import { ChipsCardConfig } from '../types/lovelace-mushroom/cards/chips-card';
import { PersonCardConfig } from '../types/lovelace-mushroom/cards/person-card-config'; import { PersonCardConfig } from '../types/lovelace-mushroom/cards/person-card-config';
import { TemplateCardConfig } from '../types/lovelace-mushroom/cards/template-card-config'; import { TemplateCardConfig } from '../types/lovelace-mushroom/cards/template-card-config';
import { LovelaceChipConfig } from '../types/lovelace-mushroom/utils/lovelace/chip/types'; 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 { ViewConfig } from '../types/strategy/strategy-views';
import { sanitizeClassName } from '../utilities/auxiliaries'; import { sanitizeClassName } from '../utilities/auxiliaries';
import { logMessage, lvlError, lvlInfo } from '../utilities/debug'; 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. */ /** The domain of the entities that the view is representing. */
static readonly domain = 'home' as const; 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. * Class constructor.
* *
@@ -47,6 +37,16 @@ class HomeView extends AbstractView {
this.baseConfiguration = { ...this.baseConfiguration, ...HomeView.getDefaultConfig(), ...customConfiguration }; 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. * Create the configuration of the cards to include in the view.
* *

View File

@@ -16,6 +16,17 @@ class LockView extends AbstractView {
/** The domain of the entities that the view is representing. */ /** The domain of the entities that the view is representing. */
static readonly domain = 'lock' as const; 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. */ /** Returns the default configuration object for the view. */
static getDefaultConfig(): ViewConfig { static getDefaultConfig(): ViewConfig {
const domainConfig = Registry.strategyOptions.domains[LockView.domain] as SingleDomainConfig; const domainConfig = Registry.strategyOptions.domains[LockView.domain] as SingleDomainConfig;
@@ -42,17 +53,6 @@ class LockView extends AbstractView {
localize('lock.unlocked'), 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; export default LockView;

View File

@@ -15,6 +15,17 @@ class SceneView extends AbstractView {
/** The domain of the entities that the view is representing. */ /** The domain of the entities that the view is representing. */
static readonly domain = 'scene' as const; 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. */ /** Returns the default configuration object for the view. */
static getDefaultConfig(): ViewConfig { static getDefaultConfig(): ViewConfig {
const domainConfig = Registry.strategyOptions.domains[SceneView.domain] as SingleDomainConfig; const domainConfig = Registry.strategyOptions.domains[SceneView.domain] as SingleDomainConfig;
@@ -36,17 +47,6 @@ class SceneView extends AbstractView {
static getViewHeaderCardConfig(): HeaderCardConfig { static getViewHeaderCardConfig(): HeaderCardConfig {
return {}; return {};
} }
/**
* Class constructor.
*
* @param {ViewConfig} [customConfiguration] Custom view configuration.
*/
constructor(customConfiguration?: ViewConfig) {
super();
this.initializeViewConfig(SceneView.getDefaultConfig(), customConfiguration, SceneView.getViewHeaderCardConfig());
}
} }
export default SceneView; export default SceneView;

View File

@@ -16,6 +16,17 @@ class SwitchView extends AbstractView {
/** The domain of the entities that the view is representing. */ /** The domain of the entities that the view is representing. */
static readonly domain = 'switch' as const; 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. */ /** Returns the default configuration object for the view. */
static getDefaultConfig(): ViewConfig { static getDefaultConfig(): ViewConfig {
const domainConfig = Registry.strategyOptions.domains[SwitchView.domain] as SingleDomainConfig; const domainConfig = Registry.strategyOptions.domains[SwitchView.domain] as SingleDomainConfig;
@@ -42,17 +53,6 @@ class SwitchView extends AbstractView {
localize('generic.on'), 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; export default SwitchView;

12
tsconfig.eslint.json Normal file
View File

@@ -0,0 +1,12 @@
{
"include": [
"eslint.config.mjs"
],
"compilerOptions": {
"allowJs": true,
"module": "ESNext",
"moduleResolution": "Node",
"target": "ESNext",
"noEmit": true
}
}