mirror of
https://github.com/DigiLive/mushroom-strategy.git
synced 2025-06-25 01:21:52 +02:00
Release v2.3.3
* Add Configuration Option: Set card count for horizontal stacks. * Add Brazilian Portuguese language. * Add Valve view. * Add a log message for domain views. * Fix Entities shown twice. * Fix Entities not shown. * Fix Area not found error. * Fix Unable to hide areas. * Fix Home view section split. * Fix filtering on AreaId. * Refactor sorting logic. * Refactor sorting logic of extra_views. * Bump Strategy version to v2.3.3. * Build Distribution.
This commit is contained in:
@ -10,26 +10,12 @@
|
||||
"project": "./tsconfig.json",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"dist/",
|
||||
"node_modules/",
|
||||
"src/types/homeassistant/",
|
||||
"src/types/lovelace-mushroom/"
|
||||
],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
|
||||
"ignorePatterns": ["dist/", "node_modules/", "src/types/homeassistant/", "src/types/lovelace-mushroom/"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"webpack.config.ts",
|
||||
"webpack.dev.config.ts"
|
||||
],
|
||||
"files": ["webpack.config.ts", "webpack.dev.config.ts"],
|
||||
"parserOptions": {
|
||||
"project": null
|
||||
}
|
||||
@ -43,29 +29,8 @@
|
||||
"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"
|
||||
]
|
||||
"no-unused-vars": "off"
|
||||
}
|
||||
}
|
||||
|
2
dist/mushroom-strategy.js
vendored
2
dist/mushroom-strategy.js
vendored
File diff suppressed because one or more lines are too long
@ -53,6 +53,8 @@ views: []
|
||||
The `extra_views` group enables you to specify the configuration of additional views.
|
||||
Each view can have the options as described in the [Home Assistant documentation][viewDocUrl]{: target="_blank"}.
|
||||
|
||||
Extra views are sorted by order first and then by title, together with the build-in views.
|
||||
|
||||
!!! tip
|
||||
|
||||
You can build your view in a temporary dashboard and copy the `views` group from the YAML of that dashboard into
|
||||
@ -68,6 +70,7 @@ strategy:
|
||||
- theme: Backend-selected
|
||||
title: cool view
|
||||
path: cool-view
|
||||
order: Infinity
|
||||
icon: mdi:emoticon-cool
|
||||
badges: []
|
||||
cards:
|
||||
|
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mushroom-strategy",
|
||||
"version": "2.3.2",
|
||||
"version": "2.3.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mushroom-strategy",
|
||||
"version": "2.3.2",
|
||||
"version": "2.3.3",
|
||||
"description": "Automatically generate a dashboard of Mushroom cards.",
|
||||
"keywords": [
|
||||
"dashboard",
|
||||
@ -54,6 +54,6 @@
|
||||
"md:lint:fix": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#dist\" --fix",
|
||||
"build-dev": "webpack --config webpack.dev.config.ts",
|
||||
"build": "webpack",
|
||||
"bump": "bump --preid alpha package.json package-lock.json README.md ./src/mushroom-strategy.ts"
|
||||
"bump": "bump --preid alpha package.json package-lock.json README.md ./docs/index.md ./src/mushroom-strategy.ts"
|
||||
}
|
||||
}
|
||||
|
102
src/Registry.ts
102
src/Registry.ts
@ -12,7 +12,7 @@ import {
|
||||
StrategyConfig,
|
||||
StrategyViewConfig,
|
||||
SupportedDomains,
|
||||
SupportedViews,
|
||||
SupportedViews
|
||||
} from './types/strategy/strategy-generics';
|
||||
import { logMessage, lvlFatal, lvlOff, lvlWarn, setDebugLevel } from './utilities/debug';
|
||||
import setupCustomLocalize from './utilities/localize';
|
||||
@ -24,19 +24,6 @@ import RegistryFilter from './utilities/RegistryFilter';
|
||||
* 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;
|
||||
/** Indicates whether this module is initialized. */
|
||||
private static _initialized: boolean = false;
|
||||
/** The Custom strategy configuration. */
|
||||
private static _strategyOptions: StrategyConfig;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
@ -48,9 +35,42 @@ class Registry {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
private constructor() {}
|
||||
|
||||
/** The configuration of the strategy. */
|
||||
static get strategyOptions(): StrategyConfig {
|
||||
return Registry._strategyOptions;
|
||||
/** 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;
|
||||
|
||||
/**
|
||||
* Home Assistant's Device registry.
|
||||
*
|
||||
* @remarks
|
||||
* This module makes changes to the registry at {@link Registry.initialize}.
|
||||
*/
|
||||
static get devices(): DeviceRegistryEntry[] {
|
||||
return Registry._devices;
|
||||
}
|
||||
|
||||
/** 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(): EntityRegistryEntry[] {
|
||||
return Registry._entities;
|
||||
}
|
||||
|
||||
/** Entries of Home Assistant's area registry. */
|
||||
private static _areas: StrategyArea[] = [];
|
||||
|
||||
/** Home Assistant's State registry. */
|
||||
static get hassStates(): HassEntities {
|
||||
return Registry._hassStates;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,36 +83,19 @@ 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(): DeviceRegistryEntry[] {
|
||||
return Registry._devices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Home Assistant's Entity registry.
|
||||
*
|
||||
* @remarks
|
||||
* This module makes changes to the registry at {@link Registry.initialize}.
|
||||
*/
|
||||
static get entities(): EntityRegistryEntry[] {
|
||||
return Registry._entities;
|
||||
}
|
||||
|
||||
/** Home Assistant's State registry. */
|
||||
static get hassStates(): HassEntities {
|
||||
return Registry._hassStates;
|
||||
}
|
||||
|
||||
/** Get the initialization status of the Registry class. */
|
||||
static get initialized(): boolean {
|
||||
return Registry._initialized;
|
||||
}
|
||||
|
||||
/** The Custom strategy configuration. */
|
||||
private static _strategyOptions: StrategyConfig;
|
||||
|
||||
/** The configuration of the strategy. */
|
||||
static get strategyOptions(): StrategyConfig {
|
||||
return Registry._strategyOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize this module.
|
||||
*
|
||||
@ -165,7 +168,7 @@ class Registry {
|
||||
}));
|
||||
|
||||
// Process entries of the HASS area registry.
|
||||
if (Registry.strategyOptions.areas._?.hidden) {
|
||||
if (Registry.strategyOptions.areas._.hidden) {
|
||||
Registry._areas = [];
|
||||
} else {
|
||||
// Create and add the undisclosed area if not hidden in the strategy options.
|
||||
@ -217,16 +220,6 @@ class Registry {
|
||||
|
||||
sortDomains();
|
||||
|
||||
// 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) => {
|
||||
return (a.order ?? Infinity) - (b.order ?? Infinity) || (a.title ?? '').localeCompare(b.title ?? '');
|
||||
});
|
||||
};
|
||||
|
||||
sortExtraViews();
|
||||
|
||||
Registry._initialized = true;
|
||||
}
|
||||
|
||||
@ -251,7 +244,7 @@ class Registry {
|
||||
const states: string[] = [];
|
||||
|
||||
if (!Registry.initialized) {
|
||||
logMessage(lvlWarn, 'Registry not initialized!');
|
||||
logMessage(lvlWarn, 'Registry is not initialized!');
|
||||
|
||||
return '?';
|
||||
}
|
||||
@ -264,6 +257,7 @@ class Registry {
|
||||
.map((entity) => `states['${entity.entity_id}']`),
|
||||
);
|
||||
|
||||
// noinspection SpellCheckingInspection
|
||||
return `{% set entities = [${states}] %}
|
||||
{{ entities
|
||||
| selectattr('state','${operator}','${value}')
|
||||
|
@ -6,6 +6,9 @@ import { localize } from './utilities/localize';
|
||||
*/
|
||||
export const ConfigurationDefaults: StrategyDefaults = {
|
||||
areas: {
|
||||
_: {
|
||||
type: 'AreaCard',
|
||||
},
|
||||
undisclosed: {
|
||||
// TODO: Refactor undisclosed to other.
|
||||
aliases: [],
|
||||
@ -20,6 +23,7 @@ export const ConfigurationDefaults: StrategyDefaults = {
|
||||
name: localize('generic.undisclosed'),
|
||||
picture: null,
|
||||
temperature_entity_id: null,
|
||||
order: Infinity,
|
||||
},
|
||||
},
|
||||
card_options: {},
|
||||
@ -39,11 +43,13 @@ export const ConfigurationDefaults: StrategyDefaults = {
|
||||
hide_config_entities: undefined,
|
||||
hide_diagnostic_entities: undefined,
|
||||
showControls: true,
|
||||
stack_count: 1,
|
||||
},
|
||||
binary_sensor: {
|
||||
title: `${localize('sensor.binary')} ` + localize('sensor.sensors'),
|
||||
showControls: false,
|
||||
hidden: false,
|
||||
stack_count: 2, // TODO: Add to wiki. also for other configurations.
|
||||
},
|
||||
camera: {
|
||||
title: localize('camera.cameras'),
|
||||
@ -136,11 +142,22 @@ export const ConfigurationDefaults: StrategyDefaults = {
|
||||
offService: 'vacuum.stop',
|
||||
hidden: false,
|
||||
},
|
||||
valve: {
|
||||
title: localize('valve.valves'),
|
||||
iconOn: 'mdi:valve-open',
|
||||
iconOff: 'mdi:valve-closed',
|
||||
onService: 'valve.open_valve',
|
||||
offService: 'valve.close_valve',
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
extra_cards: [],
|
||||
extra_views: [],
|
||||
home_view: {
|
||||
hidden: [],
|
||||
stack_count: {
|
||||
_: 2,
|
||||
},
|
||||
},
|
||||
views: {
|
||||
camera: {
|
||||
@ -183,6 +200,10 @@ export const ConfigurationDefaults: StrategyDefaults = {
|
||||
order: 8,
|
||||
hidden: false,
|
||||
},
|
||||
valve: {
|
||||
order: 11,
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
quick_access_cards: [],
|
||||
};
|
||||
|
@ -4,16 +4,17 @@ import SensorCard from './cards/SensorCard';
|
||||
import { Registry } from './Registry';
|
||||
import { LovelaceCardConfig } from './types/homeassistant/data/lovelace/config/card';
|
||||
import { LovelaceConfig } from './types/homeassistant/data/lovelace/config/types';
|
||||
import { LovelaceViewConfig, LovelaceViewRawConfig } from './types/homeassistant/data/lovelace/config/view';
|
||||
import { LovelaceViewConfig } from './types/homeassistant/data/lovelace/config/view';
|
||||
import {
|
||||
DashboardInfo,
|
||||
isSupportedDomain,
|
||||
isSupportedView,
|
||||
StrategyArea,
|
||||
StrategyViewConfig,
|
||||
ViewInfo,
|
||||
} from './types/strategy/strategy-generics';
|
||||
import { sanitizeClassName } from './utilities/auxiliaries';
|
||||
import { logMessage, lvlError } from './utilities/debug';
|
||||
import { logMessage, lvlError, lvlInfo } from './utilities/debug';
|
||||
import RegistryFilter from './utilities/RegistryFilter';
|
||||
import { stackHorizontal } from './utilities/cardStacking';
|
||||
|
||||
@ -40,7 +41,7 @@ class MushroomStrategy extends HTMLTemplateElement {
|
||||
static async generateDashboard(info: DashboardInfo): Promise<LovelaceConfig> {
|
||||
await Registry.initialize(info);
|
||||
|
||||
const views: LovelaceViewRawConfig[] = [];
|
||||
const views: StrategyViewConfig[] = [];
|
||||
|
||||
// Parallelize view imports and creation.
|
||||
const viewPromises = Registry.getExposedNames('view')
|
||||
@ -55,6 +56,8 @@ class MushroomStrategy extends HTMLTemplateElement {
|
||||
if (viewConfiguration.cards.length) {
|
||||
return viewConfiguration;
|
||||
}
|
||||
|
||||
logMessage(lvlInfo, `View ${viewName} has no entities available!`);
|
||||
} catch (e) {
|
||||
logMessage(lvlError, `Error importing ${viewName} view!`, e);
|
||||
}
|
||||
@ -62,16 +65,27 @@ class MushroomStrategy extends HTMLTemplateElement {
|
||||
return null;
|
||||
});
|
||||
|
||||
const resolvedViews = (await Promise.all(viewPromises)).filter(Boolean) as LovelaceViewRawConfig[];
|
||||
const resolvedViews = (await Promise.all(viewPromises)).filter(Boolean) as StrategyViewConfig[];
|
||||
|
||||
views.push(...resolvedViews);
|
||||
|
||||
// Extra views
|
||||
if (Registry.strategyOptions.extra_views) {
|
||||
views.push(...Registry.strategyOptions.extra_views);
|
||||
|
||||
views.sort((a, b) => {
|
||||
return (a.order ?? Infinity) - (b.order ?? Infinity) || (a.title ?? '').localeCompare(b.title ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
// Subviews for areas
|
||||
views.push(
|
||||
...Registry.areas.map((area) => ({
|
||||
title: area.name,
|
||||
path: area.area_id,
|
||||
subview: true,
|
||||
hidden: area.hidden ?? false,
|
||||
order: area.order ?? Infinity,
|
||||
strategy: {
|
||||
type: 'custom:mushroom-strategy',
|
||||
options: { area },
|
||||
@ -79,11 +93,6 @@ class MushroomStrategy extends HTMLTemplateElement {
|
||||
})),
|
||||
);
|
||||
|
||||
// Extra views
|
||||
if (Registry.strategyOptions.extra_views) {
|
||||
views.push(...Registry.strategyOptions.extra_views);
|
||||
}
|
||||
|
||||
return { views };
|
||||
}
|
||||
|
||||
@ -121,7 +130,7 @@ class MushroomStrategy extends HTMLTemplateElement {
|
||||
return null;
|
||||
}
|
||||
|
||||
const titleCard = new HeaderCard(
|
||||
const headerCard = new HeaderCard(
|
||||
{ entity_id: entities.map((entity) => entity.entity_id) },
|
||||
{
|
||||
...Registry.strategyOptions.domains['_'],
|
||||
@ -133,7 +142,7 @@ class MushroomStrategy extends HTMLTemplateElement {
|
||||
const DomainCard = (await import(`./cards/${moduleName}`)).default;
|
||||
|
||||
if (domain === 'sensor') {
|
||||
const domainCards = entities
|
||||
let domainCards = entities
|
||||
.filter((entity) => Registry.hassStates[entity.entity_id]?.attributes.unit_of_measurement)
|
||||
.map((entity) => {
|
||||
const options = {
|
||||
@ -144,7 +153,17 @@ class MushroomStrategy extends HTMLTemplateElement {
|
||||
};
|
||||
return new SensorCard(entity, options).getCard();
|
||||
});
|
||||
return domainCards.length ? { type: 'vertical-stack', cards: [titleCard, ...domainCards] } : null;
|
||||
|
||||
if (domainCards.length) {
|
||||
domainCards = stackHorizontal(
|
||||
domainCards,
|
||||
Registry.strategyOptions.domains[domain].stack_count ?? Registry.strategyOptions.domains['_'].stack_count,
|
||||
);
|
||||
|
||||
return { type: 'vertical-stack', cards: [headerCard, ...domainCards] };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
let domainCards = entities.map((entity) => {
|
||||
@ -155,11 +174,12 @@ class MushroomStrategy extends HTMLTemplateElement {
|
||||
return new DomainCard(entity, cardOptions).getCard();
|
||||
});
|
||||
|
||||
if (domain === 'binary_sensor') {
|
||||
domainCards = stackHorizontal(domainCards);
|
||||
}
|
||||
domainCards = stackHorizontal(
|
||||
domainCards,
|
||||
Registry.strategyOptions.domains[domain].stack_count ?? Registry.strategyOptions.domains['_'].stack_count,
|
||||
);
|
||||
|
||||
return domainCards.length ? { type: 'vertical-stack', cards: [titleCard, ...domainCards] } : null;
|
||||
return domainCards.length ? { type: 'vertical-stack', cards: [headerCard, ...domainCards] } : null;
|
||||
} catch (e) {
|
||||
logMessage(lvlError, `Error creating card configurations for domain ${domain}`, e);
|
||||
return null;
|
||||
@ -180,17 +200,27 @@ class MushroomStrategy extends HTMLTemplateElement {
|
||||
if (miscellaneousEntities.length) {
|
||||
try {
|
||||
const MiscellaneousCard = (await import('./cards/MiscellaneousCard')).default;
|
||||
const miscellaneousCards = [
|
||||
new HeaderCard(target, Registry.strategyOptions.domains.default).createCard(),
|
||||
...miscellaneousEntities.map((entity) =>
|
||||
new MiscellaneousCard(entity, Registry.strategyOptions.card_options?.[entity.entity_id]).getCard(),
|
||||
),
|
||||
];
|
||||
let miscellaneousCards = miscellaneousEntities.map((entity) =>
|
||||
new MiscellaneousCard(entity, Registry.strategyOptions.card_options?.[entity.entity_id]).getCard(),
|
||||
);
|
||||
|
||||
viewCards.push({
|
||||
type: 'vertical-stack',
|
||||
cards: miscellaneousCards,
|
||||
});
|
||||
const headerCard = new HeaderCard(target, {
|
||||
...Registry.strategyOptions.domains['_'],
|
||||
...Registry.strategyOptions.domains['default'],
|
||||
}).createCard();
|
||||
|
||||
if (miscellaneousCards.length) {
|
||||
miscellaneousCards = stackHorizontal(
|
||||
miscellaneousCards,
|
||||
Registry.strategyOptions.domains['default'].stack_count ??
|
||||
Registry.strategyOptions.domains['_'].stack_count,
|
||||
);
|
||||
|
||||
viewCards.push({
|
||||
type: 'vertical-stack',
|
||||
cards: [headerCard, ...miscellaneousCards],
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logMessage(lvlError, 'Error creating card configurations for domain `miscellaneous`', e);
|
||||
}
|
||||
@ -203,7 +233,7 @@ class MushroomStrategy extends HTMLTemplateElement {
|
||||
|
||||
customElements.define('ll-strategy-mushroom-strategy', MushroomStrategy);
|
||||
|
||||
const version = 'v2.3.2';
|
||||
const version = 'v2.3.3';
|
||||
console.info(
|
||||
'%c Mushroom Strategy %c '.concat(version, ' '),
|
||||
'color: white; background: coral; font-weight: 700;',
|
||||
|
@ -75,6 +75,7 @@
|
||||
"opening": "Öffnet",
|
||||
"closed": "Geschlossen",
|
||||
"closing": "Schließt",
|
||||
"stopped": "Gestoppt"
|
||||
"stopped": "Gestoppt",
|
||||
"unclosed": "Nicht geschlossen"
|
||||
}
|
||||
}
|
||||
|
@ -75,6 +75,7 @@
|
||||
"opening": "Opening",
|
||||
"closed": "Closed",
|
||||
"closing": "Closing",
|
||||
"stopped": "Stopped"
|
||||
"stopped": "Stopped",
|
||||
"unclosed": "Unclosed"
|
||||
}
|
||||
}
|
||||
|
@ -75,6 +75,7 @@
|
||||
"opening": "Abriendo",
|
||||
"closed": "Cerrada",
|
||||
"closing": "Cerrando",
|
||||
"stopped": "Detenida"
|
||||
"stopped": "Detenida",
|
||||
"unclosed": "No Cerrada"
|
||||
}
|
||||
}
|
||||
|
@ -75,6 +75,7 @@
|
||||
"opening": "Openen",
|
||||
"closed": "Gesloten",
|
||||
"closing": "Sluiten",
|
||||
"stopped": "Gestopt"
|
||||
"stopped": "Gestopt",
|
||||
"unclosed": "Niet gesloten"
|
||||
}
|
||||
}
|
||||
|
@ -75,6 +75,7 @@
|
||||
"opening": "Abrindo",
|
||||
"closed": "Fechado",
|
||||
"closing": "Fechando",
|
||||
"stopped": "Parado"
|
||||
"stopped": "Parado",
|
||||
"unclosed": "Nao fechado"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
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';
|
||||
@ -8,6 +7,7 @@ import { LovelaceViewConfig, LovelaceViewRawConfig } from '../homeassistant/data
|
||||
import { HomeAssistant } from '../homeassistant/types';
|
||||
import { LovelaceChipConfig } from '../lovelace-mushroom/utils/lovelace/chip/types';
|
||||
import { StrategyHeaderCardConfig } from './strategy-cards';
|
||||
import { AreaRegistryEntry } from '../homeassistant/data/area_registry';
|
||||
|
||||
/**
|
||||
* List of supported domains.
|
||||
@ -37,6 +37,7 @@ const SUPPORTED_DOMAINS = [
|
||||
'sensor',
|
||||
'switch',
|
||||
'vacuum',
|
||||
'valve',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
@ -55,6 +56,7 @@ const SUPPORTED_VIEWS = [
|
||||
'scene',
|
||||
'switch',
|
||||
'vacuum',
|
||||
'valve',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
@ -69,6 +71,7 @@ const SUPPORTED_CHIPS = ['light', 'fan', 'cover', 'switch', 'climate', 'weather'
|
||||
*
|
||||
* This constant array defines the sections that are present in the home view.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const HOME_VIEW_SECTIONS = ['areas', 'areasTitle', 'chips', 'greeting', 'persons'] as const;
|
||||
|
||||
export type SupportedDomains = (typeof SUPPORTED_DOMAINS)[number];
|
||||
@ -175,11 +178,13 @@ export interface ViewInfo {
|
||||
* @property {boolean} [hide_config_entities] - If True, all configuration entities are hidden from the dashboard.
|
||||
* @property {boolean} [hide_diagnostic_entities] - If True, all diagnostic entities are hidden from the dashboard.
|
||||
* @property {boolean} [showControls] - False to hide controls.
|
||||
* @property {number} [stack_count] - Number of cards per row.
|
||||
*/
|
||||
export interface AllDomainsConfig {
|
||||
hide_config_entities?: boolean;
|
||||
hide_diagnostic_entities?: boolean;
|
||||
showControls?: boolean;
|
||||
stack_count?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -187,23 +192,25 @@ 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.
|
||||
* @property {number} [stack_count] - Number of cards per row.
|
||||
*/
|
||||
export interface SingleDomainConfig extends Partial<StrategyHeaderCardConfig> {
|
||||
hidden: boolean;
|
||||
order?: number;
|
||||
stack_count?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategy Configuration.
|
||||
*
|
||||
* @property {Object.<K in keyof StrategyArea, StrategyArea>} areas - List of areas.
|
||||
* @property {Object.<K in keyof RegistryEntry, CustomCardConfig>} card_options - Card options for entities.
|
||||
* @property {Object.<string, StrategyArea>} areas - The configuration of areas.
|
||||
* @property {Object.<string, CustomCardConfig>} card_options - Card options for entities.
|
||||
* @property {ChipConfiguration} chips - The configuration of chips in the Home view.
|
||||
* @property {boolean} debug - If True, the strategy outputs more verbose debug information in the console.
|
||||
* @property {Object.<string, AllDomainsConfig | SingleDomainConfig>} domains - List of domains.
|
||||
* @property {LovelaceCardConfig[]} extra_cards - List of cards to show below room cards.
|
||||
* @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 {{ Object }} home_view - List of views to add to the dashboard.
|
||||
* @property {Record<SupportedViews, StrategyViewConfig>} views - The configurations of views.
|
||||
* @property {LovelaceCardConfig[]} quick_access_cards - List of custom-defined cards to show between the welcome card
|
||||
* and rooms cards.
|
||||
@ -218,6 +225,7 @@ export interface StrategyConfig {
|
||||
extra_views: StrategyViewConfig[];
|
||||
home_view: {
|
||||
hidden: HomeViewSections[];
|
||||
stack_count: { _: number } & { [K in HomeViewSections]?: K extends 'areas' ? [number, number] : number };
|
||||
};
|
||||
views: Record<SupportedViews, StrategyViewConfig>;
|
||||
quick_access_cards: LovelaceCardConfig[];
|
||||
@ -226,9 +234,12 @@ export interface StrategyConfig {
|
||||
/**
|
||||
* Represents the default configuration for a strategy.
|
||||
*/
|
||||
export interface StrategyDefaults extends StrategyConfig {
|
||||
areas: { undisclosed: StrategyArea } & { [S: string]: StrategyArea };
|
||||
}
|
||||
export type StrategyDefaults = Omit<StrategyConfig, 'areas'> & {
|
||||
areas: {
|
||||
_: AllAreasConfig;
|
||||
undisclosed: StrategyArea;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Strategy Area.
|
||||
@ -246,6 +257,15 @@ export interface StrategyArea extends AreaRegistryEntry {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for all areas.
|
||||
*
|
||||
* @property {string} [type] - The type of area card.
|
||||
*/
|
||||
export interface AllAreasConfig {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of chips to show in the Home view.
|
||||
*
|
||||
|
@ -12,6 +12,7 @@ import { logMessage, lvlWarn } from './debug';
|
||||
* Supports chaining for building complex filter queries.
|
||||
*
|
||||
* @template T The specific type of RegistryEntry being filtered.
|
||||
* @template K - A property key of T.
|
||||
*/
|
||||
class RegistryFilter<T extends RegistryEntry, K extends keyof T = keyof T> {
|
||||
private readonly entries: T[];
|
||||
@ -73,10 +74,14 @@ class RegistryFilter<T extends RegistryEntry, K extends keyof T = keyof T> {
|
||||
* @param {boolean} [expandToDevice=true] - Whether to evaluate the device's `area_id` (see remarks).
|
||||
*
|
||||
* @remarks
|
||||
* For entries with area id `undisclosed` or `undefined`, the device's `area_id` must also match if `expandToDevice`
|
||||
* is `true`.
|
||||
* The entry's `area_id` must match `areaId` (with special handling for 'undisclosed').
|
||||
*
|
||||
* If `expandToDevice` is true, additional rules apply based on `areaId`:
|
||||
* - `areaId` is `null`/`undefined`: The device's `area_id` must be `null`.
|
||||
* - `areaId` is `'undisclosed'`: The device's `area_id` must match or be `'undisclosed'`/`null`.
|
||||
* - For other `areaId` values: If entry's `area_id` is `'undisclosed'`, the device's `area_id` must match `areaId`.
|
||||
*/
|
||||
whereAreaId(areaId?: string, expandToDevice: boolean = true): this {
|
||||
whereAreaId(areaId?: string | null, expandToDevice: boolean = true): this {
|
||||
const predicate = (entry: T) => {
|
||||
let deviceAreaId: string | null | undefined = undefined;
|
||||
const entryObject = entry as EntityRegistryEntry;
|
||||
@ -85,15 +90,19 @@ class RegistryFilter<T extends RegistryEntry, K extends keyof T = keyof T> {
|
||||
deviceAreaId = Registry.devices.find((device) => device.id === entryObject.device_id)?.area_id;
|
||||
}
|
||||
|
||||
if (areaId === undefined) {
|
||||
return entry.area_id === undefined && deviceAreaId === undefined;
|
||||
if (!areaId) {
|
||||
return entry.area_id === areaId && deviceAreaId === areaId;
|
||||
}
|
||||
|
||||
if (entry.area_id === 'undisclosed' || !entry.area_id) {
|
||||
return deviceAreaId === areaId;
|
||||
if (areaId === 'undisclosed') {
|
||||
return entry.area_id === areaId && (deviceAreaId === areaId || deviceAreaId == null);
|
||||
}
|
||||
|
||||
return entry.area_id === areaId;
|
||||
if (entry.area_id === areaId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return entry.area_id === 'undisclosed' && deviceAreaId === areaId;
|
||||
};
|
||||
|
||||
this.filters.push(this.checkInversion(predicate));
|
||||
@ -319,58 +328,84 @@ class RegistryFilter<T extends RegistryEntry, K extends keyof T = keyof T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the entries based in priority order of the provided keys.
|
||||
* Sorts the entries based on the specified keys in priority order.
|
||||
*
|
||||
* @template K A key to sort by, which must be a key of the registry entry types.
|
||||
* @template T The specific type of RegistryEntry being sorted.
|
||||
*
|
||||
* @param {K[]} keys The keys to sort on, in order of priority.
|
||||
* @param {'asc' | 'desc'} [direction='asc'] The sorting direction ('asc' for ascending, 'desc' for descending).
|
||||
*
|
||||
* @returns {RegistryFilter<T>} A new RegistryFilter instance with the sorted entries and the current filters.
|
||||
* @template K - The type of keys to sort by (must be keys of T).
|
||||
* @param {K[]} keys - Array of property keys to sort by, in order of priority.
|
||||
* @param {'asc' | 'desc'} [direction='asc'] - Sort direction.
|
||||
* @returns {RegistryFilter<T>} A new RegistryFilter instance with sorted entries.
|
||||
*/
|
||||
orderBy<K extends keyof T>(keys: K[], direction: 'asc' | 'desc' = 'asc'): RegistryFilter<T> {
|
||||
// Helper to get the first defined value from an entry for the given keys.
|
||||
const getValue = (entry: T, keys: K[]): unknown => {
|
||||
for (const k of keys) {
|
||||
const value = entry[k];
|
||||
|
||||
for (const key of keys) {
|
||||
const value = entry[key];
|
||||
if (value !== null && value !== undefined) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Assign sort priorities for special values.
|
||||
const getSortValue = (value: unknown): [number, unknown] => {
|
||||
switch (value) {
|
||||
case -Infinity:
|
||||
return [0, 0]; // First.
|
||||
case undefined:
|
||||
case null:
|
||||
return [2, 0]; // In between.
|
||||
case Infinity:
|
||||
return [3, 0]; // Last.
|
||||
default:
|
||||
return [1, value]; // Normal value comparison.
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new array to avoid mutating the original.
|
||||
const sortedEntries = [...this.entries].sort((a, b) => {
|
||||
// Get the first defined value for each entry using the provided keys
|
||||
const valueA = getValue(a, keys);
|
||||
const valueB = getValue(b, keys);
|
||||
|
||||
// If values are strictly equal, they're in the same position.
|
||||
if (valueA === valueB) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ascendingMultiplier = direction === 'asc' ? 1 : -1;
|
||||
// Get sort priorities and comparable values
|
||||
const [priorityA, comparableA] = getSortValue(valueA);
|
||||
const [priorityB, comparableB] = getSortValue(valueB);
|
||||
|
||||
if (valueA === undefined || valueA === null) {
|
||||
return ascendingMultiplier;
|
||||
// First, compare by priority (handles special values).
|
||||
if (priorityA !== priorityB) {
|
||||
return (priorityA - priorityB) * (direction === 'asc' ? 1 : -1);
|
||||
}
|
||||
|
||||
if (valueB === undefined || valueB === null) {
|
||||
return -ascendingMultiplier;
|
||||
// For same priority, compare the actual values.
|
||||
// Handle undefined/null cases
|
||||
if (comparableA === undefined || comparableA === null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (typeof valueA === 'string' && typeof valueB === 'string') {
|
||||
return valueA.localeCompare(valueB) * ascendingMultiplier;
|
||||
if (comparableB === undefined || comparableB === null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (valueA < valueB ? -1 : 1) * ascendingMultiplier;
|
||||
// String comparison.
|
||||
if (typeof comparableA === 'string' && typeof comparableB === 'string') {
|
||||
return comparableA.localeCompare(comparableB) * (direction === 'asc' ? 1 : -1);
|
||||
}
|
||||
|
||||
// Numeric/other comparison.
|
||||
return (comparableA < comparableB ? -1 : 1) * (direction === 'asc' ? 1 : -1);
|
||||
});
|
||||
|
||||
// Create a new filter with the sorted entries.
|
||||
const newFilter = new RegistryFilter(sortedEntries);
|
||||
|
||||
// Copy over existing filters.
|
||||
newFilter.filters = [...this.filters];
|
||||
|
||||
return newFilter;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { LovelaceCardConfig } from '../types/homeassistant/data/lovelace/config/card';
|
||||
import { StackCardConfig } from '../types/homeassistant/panels/lovelace/cards/types';
|
||||
|
||||
// noinspection GrazieInspection
|
||||
/**
|
||||
* Stacks an array of Lovelace card configurations into horizontal stacks based on their type.
|
||||
*
|
||||
@ -9,6 +10,7 @@ import { StackCardConfig } from '../types/homeassistant/panels/lovelace/cards/ty
|
||||
* It returns a new array of stacked card configurations, preserving the original order of the cards.
|
||||
*
|
||||
* @param cardConfigurations - An array of Lovelace card configurations to be stacked.
|
||||
* @param defaultCount - The default number of cards to stack if the type or column count is not found in the mapping.
|
||||
* @param [columnCounts] - An object mapping card types to their respective column counts.
|
||||
* If a type is not found in the mapping, it defaults to 2.
|
||||
* @returns An array of stacked card configurations, where each configuration is a horizontal stack
|
||||
@ -16,17 +18,26 @@ import { StackCardConfig } from '../types/homeassistant/panels/lovelace/cards/ty
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* stackedCards = stackHorizontal(card, {area: 1, "custom:card": 2});
|
||||
* stackedCards = stackHorizontal(card, 2, {area: 1, 'custom:card': 2});
|
||||
* ```
|
||||
*/
|
||||
export function stackHorizontal(
|
||||
cardConfigurations: LovelaceCardConfig[],
|
||||
defaultCount: number = 2,
|
||||
columnCounts?: {
|
||||
[key: string]: number;
|
||||
[key: string]: number | undefined;
|
||||
},
|
||||
): LovelaceCardConfig[] {
|
||||
if (cardConfigurations.length <= 1) {
|
||||
return cardConfigurations;
|
||||
}
|
||||
|
||||
// Function to process a sequence of cards
|
||||
const doStack = (cards: LovelaceCardConfig[], columnCount: number) => {
|
||||
if (cards.length <= 1) {
|
||||
return cards;
|
||||
}
|
||||
|
||||
const stackedCardConfigurations: StackCardConfig[] = [];
|
||||
|
||||
for (let i = 0; i < cards.length; i += columnCount) {
|
||||
@ -44,7 +55,7 @@ export function stackHorizontal(
|
||||
|
||||
for (let i = 0; i < cardConfigurations.length; ) {
|
||||
const currentCard = cardConfigurations[i];
|
||||
const currentType = currentCard.type; // Assuming each card has a 'type' property
|
||||
const currentType = currentCard.type;
|
||||
|
||||
// Start a new sequence
|
||||
const sequence: LovelaceCardConfig[] = [];
|
||||
@ -55,7 +66,7 @@ export function stackHorizontal(
|
||||
i++; // Move to the next card
|
||||
}
|
||||
|
||||
const columnCount = Math.max(columnCounts?.[currentType] || 2, 1);
|
||||
const columnCount = Math.max(columnCounts?.[currentType] || defaultCount, 1);
|
||||
|
||||
// Process the sequence and add the result to the processedConfigurations array
|
||||
processedConfigurations.push(...doStack(sequence, columnCount));
|
||||
|
@ -10,6 +10,7 @@ import { ViewConfig, ViewConstructor } from '../types/strategy/strategy-views';
|
||||
import { sanitizeClassName } from '../utilities/auxiliaries';
|
||||
import { logMessage, lvlFatal } from '../utilities/debug';
|
||||
import RegistryFilter from '../utilities/RegistryFilter';
|
||||
import { stackHorizontal } from '../utilities/cardStacking';
|
||||
|
||||
/**
|
||||
* Abstract View Class.
|
||||
@ -45,7 +46,7 @@ abstract class AbstractView {
|
||||
*/
|
||||
protected constructor() {
|
||||
if (!Registry.initialized) {
|
||||
logMessage(lvlFatal, 'Registry not initialized!');
|
||||
logMessage(lvlFatal, 'Registry is not initialized!');
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,7 +64,7 @@ abstract class AbstractView {
|
||||
|
||||
// Create card configurations for each area.
|
||||
for (const area of Registry.areas) {
|
||||
const areaCards: AbstractCardConfig[] = [];
|
||||
let areaCards: AbstractCardConfig[] = [];
|
||||
|
||||
// Set the target of the Header card to the current area.
|
||||
let target: HassServiceTarget = {
|
||||
@ -85,8 +86,14 @@ abstract class AbstractView {
|
||||
),
|
||||
);
|
||||
|
||||
// Vertically stack the cards of the current area.
|
||||
// Stack the cards of the current area.
|
||||
if (areaCards.length) {
|
||||
areaCards = stackHorizontal(
|
||||
areaCards,
|
||||
Registry.strategyOptions.domains[this.domain as SupportedDomains].stack_count ??
|
||||
Registry.strategyOptions.domains['_'].stack_count,
|
||||
);
|
||||
|
||||
// Create and insert a Header card.
|
||||
const areaHeaderCardOptions = (
|
||||
'headerCardConfiguration' in this.baseConfiguration ? this.baseConfiguration.headerCardConfiguration : {}
|
||||
|
@ -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.
|
||||
*
|
||||
@ -116,7 +116,12 @@ class HomeView extends AbstractView {
|
||||
homeViewCards.push(...Registry.strategyOptions.extra_cards);
|
||||
}
|
||||
|
||||
return homeViewCards;
|
||||
return [
|
||||
{
|
||||
type: 'vertical-stack',
|
||||
cards: homeViewCards,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -210,7 +215,11 @@ class HomeView extends AbstractView {
|
||||
|
||||
return {
|
||||
type: 'vertical-stack',
|
||||
cards: stackHorizontal(cardConfigurations),
|
||||
cards: stackHorizontal(
|
||||
cardConfigurations,
|
||||
Registry.strategyOptions.home_view.stack_count['persons'] ??
|
||||
Registry.strategyOptions.home_view.stack_count['_'],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@ -223,7 +232,6 @@ class HomeView extends AbstractView {
|
||||
private async createAreasSection(): Promise<StackCardConfig | undefined> {
|
||||
if (Registry.strategyOptions.home_view.hidden.includes('areas')) {
|
||||
// Areas section is hidden.
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -257,7 +265,10 @@ class HomeView extends AbstractView {
|
||||
return {
|
||||
type: 'vertical-stack',
|
||||
title: Registry.strategyOptions.home_view.hidden.includes('areasTitle') ? undefined : localize('generic.areas'),
|
||||
cards: stackHorizontal(cardConfigurations, { area: 1, 'custom:mushroom-template-card': 2 }),
|
||||
cards: stackHorizontal(cardConfigurations, Registry.strategyOptions.home_view.stack_count['_'], {
|
||||
'custom:mushroom-template-card': Registry.strategyOptions.home_view.stack_count.areas?.[0],
|
||||
area: Registry.strategyOptions.home_view.stack_count.areas?.[1],
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
56
src/views/ValveView.ts
Normal file
56
src/views/ValveView.ts
Normal file
@ -0,0 +1,56 @@
|
||||
// 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';
|
||||
|
||||
/**
|
||||
* Valve View Class.
|
||||
*
|
||||
* Used to create a view configuration for entities of the valve domain.
|
||||
*/
|
||||
class ValveView extends AbstractView {
|
||||
/** The domain of the entities that the view is representing. */
|
||||
static readonly domain = 'valve' as const;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param {ViewConfig} [customConfiguration] Custom view configuration.
|
||||
*/
|
||||
constructor(customConfiguration?: ViewConfig) {
|
||||
super();
|
||||
|
||||
this.initializeViewConfig(ValveView.getDefaultConfig(), customConfiguration, ValveView.getViewHeaderCardConfig());
|
||||
}
|
||||
|
||||
/** Returns the default configuration object for the view. */
|
||||
static getDefaultConfig(): ViewConfig {
|
||||
return {
|
||||
title: localize('valve.valves'),
|
||||
path: 'valves',
|
||||
icon: 'mdi:valve',
|
||||
subview: false,
|
||||
headerCardConfiguration: {
|
||||
iconOn: 'mdi:valve-open',
|
||||
iconOff: 'mdi:valve-closed',
|
||||
onService: 'valve.open_valve',
|
||||
offService: 'valve.close_valve',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns the default configuration of the view's Header card. */
|
||||
static getViewHeaderCardConfig(): CustomHeaderCardConfig {
|
||||
return {
|
||||
title: localize('valve.all_valves'),
|
||||
subtitle:
|
||||
Registry.getCountTemplate(ValveView.domain, 'in', '[closed]') +
|
||||
` ${localize('valve.valves')} ${localize('valve.unclosed')}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default ValveView;
|
Reference in New Issue
Block a user