Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
df9032a
split basic views by label extension
mtinner May 21, 2024
711dae1
add label based views and reuse icon and text of label
mtinner May 22, 2024
eb77434
add labels and enable order of label based tab views
mtinner May 23, 2024
08d5d7f
add floor and sort strategy areas by order first then by floor leven …
mtinner May 23, 2024
1e97674
Merge pull request #1 from mtinner/feature/label-extended-views
mtinner May 23, 2024
b6b0203
label based icon
mtinner May 23, 2024
b9d5d19
Merge pull request #2 from mtinner/feature/label-extended-views
mtinner May 23, 2024
2a880c2
simplify code; pass more logic to AbstractView.ts
mtinner May 24, 2024
e3effec
adjust readme
mtinner May 24, 2024
e6646a1
Merge pull request #3 from mtinner/feature/label-extended-views
mtinner May 24, 2024
e86ce26
enable label based areas
mtinner May 24, 2024
0807d09
Merge pull request #4 from mtinner/feature/label-based-sub-views
mtinner May 24, 2024
773272d
fix title for subview card if there are more than one entry per label
mtinner May 24, 2024
5ab5526
Merge pull request #5 from mtinner/feature/label-based-sub-views
mtinner May 24, 2024
55700b8
add some more documentation
mtinner May 28, 2024
78d814e
revert changes in mushroom-strategy.js
mtinner May 28, 2024
977b8e1
revert changes in mushroom-strategy.js
mtinner May 28, 2024
4fcc215
use entities to control ControllerCard.ts instead of area target
mtinner Aug 1, 2024
64d0971
add binary sensors
mtinner Dec 3, 2024
841e607
map _ in label to space
mtinner Dec 3, 2024
9fdf023
fix naming for binary sensors headline
mtinner Dec 3, 2024
68558c2
map _ in label to space for title
mtinner Dec 6, 2024
67eadfa
fix naming for binary sensors headline
mtinner Dec 6, 2024
07b740e
add lawn mower domain
mtinner Apr 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/node_modules/
.idea
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Visit the [issues](https://github.com/AalianKhan/mushroom-strategy/issues/new/ch

* [Johan Frick](https://github.com/johanfrick)

* [Marcel Tinner](https://github.com/mtinner)

## Credits

* The cards used are from [Mushroom][mushroomUrl] and [Mini graph card][miniGraphUrl].
Expand Down
2 changes: 1 addition & 1 deletion dist/mushroom-strategy.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mushroom-strategy",
"version": "2.1.0",
"version": "2.1.1",
"description": "Automatically create a dashboard using Mushroom cards",
"keywords": [
"strategy",
Expand Down
159 changes: 139 additions & 20 deletions src/Helper.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import {configurationDefaults} from "./configurationDefaults";
import {HassEntities, HassEntity} from "home-assistant-js-websocket";
import {HassEntities, HassEntity, HassServiceTarget} from "home-assistant-js-websocket";
import deepmerge from "deepmerge";
import {EntityRegistryEntry} from "./types/homeassistant/data/entity_registry";
import {DeviceRegistryEntry} from "./types/homeassistant/data/device_registry";
import {AreaRegistryEntry} from "./types/homeassistant/data/area_registry";
import {generic} from "./types/strategy/generic";
import StrategyArea = generic.StrategyArea;
import {LabelRegistryEntry} from "./types/homeassistant/data/label_registry";
import ViewConfig = generic.ViewConfig;
import {FloorRegistryEntry} from "./types/homeassistant/data/floor_registry";
import {isDefined} from "./utils/object";

/**
* Helper Class
Expand Down Expand Up @@ -37,6 +41,22 @@ class Helper {
*/
static #areas: StrategyArea[] = [];

/**
* An array of entities from Home Assistant's label registry.
*
* @type {LabelRegistryEntry[]}
* @private
*/
static #labels: LabelRegistryEntry[] = [];

/**
* An array of entities from Home Assistant's floor registry.
*
* @type {FloorRegistryEntry[]}
* @private
*/
static #floor: any[] = [];

/**
* An array of state entities from Home Assistant's Hass object.
*
Expand All @@ -61,6 +81,11 @@ class Helper {
*/
static #strategyOptions: generic.StrategyConfig;

/**
* map key is floor_id value is level of the floor
* @private
*/
static #floorLevelMap: { [key: string]: number };
/**
* Set to true for more verbose information in the console.
*
Expand Down Expand Up @@ -121,6 +146,16 @@ class Helper {
return this.#entities;
}

/**
* Get the labels from Home Assistant's label registry.
*
* @returns {EntityRegistryEntry[]}
* @static
*/
static get labels(): LabelRegistryEntry[] {
return this.#labels;
}

/**
* Get the current debug mode of the mushroom strategy.
*
Expand Down Expand Up @@ -148,10 +183,12 @@ class Helper {
// Query the registries of Home Assistant.

// noinspection ES6MissingAwait False positive? https://youtrack.jetbrains.com/issue/WEB-63746
[Helper.#entities, Helper.#devices, Helper.#areas] = await Promise.all([
[Helper.#entities, Helper.#devices, Helper.#areas, Helper.#labels, Helper.#floor] = await Promise.all([
info.hass.callWS({type: "config/entity_registry/list"}) as Promise<EntityRegistryEntry[]>,
info.hass.callWS({type: "config/device_registry/list"}) as Promise<DeviceRegistryEntry[]>,
info.hass.callWS({type: "config/area_registry/list"}) as Promise<AreaRegistryEntry[]>,
info.hass.callWS({type: "config/label_registry/list"}) as Promise<any[]>,
info.hass.callWS({type: "config/floor_registry/list"}) as Promise<any[]>,
]);
} catch (e) {
Helper.logError("An error occurred while querying Home assistant's registries!", e);
Expand All @@ -176,18 +213,16 @@ class Helper {
return {...area, ...this.#strategyOptions.areas?.[area.area_id]};
});

// Sort strategy areas by order first and then by name.
this.#floorLevelMap = this.#floor.reduce((acc: { [florName: string]: number }, floor: FloorRegistryEntry) => ({
...acc,
[floor.floor_id]: floor.level
}), {})

// Sort strategy areas by order first then by floor leven and then by name.
this.#areas.sort((a, b) => {
return (a.order ?? Infinity) - (b.order ?? Infinity) || a.name.localeCompare(b.name);
return (a.order ?? Infinity) - (b.order ?? Infinity) || (this.#floorLevelMap[a.floor_id ?? ''] ?? Infinity) - (this.#floorLevelMap[b.floor_id ?? ''] ?? Infinity) || a.name.localeCompare(b.name);
});

// Sort custom and default views of the strategy options by order first and then by title.
this.#strategyOptions.views = Object.fromEntries(
Object.entries(this.#strategyOptions.views).sort(([, a], [, b]) => {
return (a.order ?? Infinity) - (b.order ?? Infinity) || (a.title ?? "undefined").localeCompare(b.title ?? "undefined");
}),
);

// Sort custom and default domains of the strategy options by order first and then by title.
this.#strategyOptions.domains = Object.fromEntries(
Object.entries(this.#strategyOptions.domains).sort(([, a], [, b]) => {
Expand All @@ -198,6 +233,19 @@ class Helper {
this.#initialized = true;
}

/**
* sort views according to order or mushroom strategy default
* @param views
*/
static sortViews(views: ViewConfig[]): ViewConfig[] {
const sortedViews = views.filter(item => !Number.isInteger(item.order));

views.filter(item => Number.isInteger(item.order))
.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
.forEach((item) => sortedViews.splice(item.order ?? Infinity, 0, item));
return sortedViews
}

/**
* Get the initialization status of the Helper class.
*
Expand Down Expand Up @@ -233,7 +281,7 @@ class Helper {
*
* @type {string[]}
*/
const states: string[] = [];
const entities: EntityRegistryEntry[] = [];

if (!this.isInitialized()) {
console.warn("Helper class should be initialized before calling this method!");
Expand All @@ -248,17 +296,39 @@ class Helper {
});

// Get the entities of which all conditions of the callback function are met. @see areaFilterCallback.
const newStates = this.#entities.filter(
this.#entities.filter(
this.#areaFilterCallback, {
area: area,
domain: domain,
areaDeviceIds: areaDeviceIds,
})
.map((entity) => `states['${entity.entity_id}']`);
}).forEach(entity => entities.push(entity))

states.push(...newStates);
}

return this.getCountEntityTemplate(entities, operator, value);
}

static getCountEntityTemplate(entities: EntityRegistryEntry[], operator: string, value: string): string {
// noinspection JSMismatchedCollectionQueryUpdate (False positive per 17-04-2023)
/**
* Array of entity state-entries.
*
* Each element contains a template-string which is used to access home assistant's state machine (state object) in
* a template.
* E.g. "states['light.kitchen']"
*
* @type {string[]}
*/
if (!this.isInitialized()) {
console.warn("Helper class should be initialized before calling this method!");
}

// Get the ID of the devices which are linked to the given area.

// Get the entities of which all conditions of the callback function are met. @see areaFilterCallback.
const states = entities
.map((entity) => `states['${entity.entity_id}']`);

return `{% set entities = [${states}] %} {{ entities | selectattr('state','${operator}','${value}') | list | count }}`;
}

Expand Down Expand Up @@ -369,16 +439,52 @@ class Helper {
}

/**
* Get the ids of the views which aren't set to hidden in the strategy options.
* Get a target of entity IDs for the given domain.
*
* @param {string} domain - The target domain to retrieve entity IDs from.
* @return {EntityRegistryEntry[]} - A target for a service call.
*/
static entitiesOfDomain(domain: string) {
return Helper.entities.filter(
entity =>
entity.entity_id.startsWith(domain + ".")
&& !entity.hidden_by
&& !Helper.strategyOptions.card_options?.[entity.entity_id]?.hidden
)
};

/**
* Get unique labels of domain. Compare name of label for more user flexibility, because the name can be renamed unlike the id.
*
* @param {string} domain - The target domain of entities.
* @return {LabelRegistryEntry[]} - unique labels.
*/
static labelsOfDomain(domain: string): LabelRegistryEntry[] {
const labels: string[] = this.entitiesOfDomain(domain)
.flatMap(entity => entity.labels)
return [...new Set(labels)]
.map(label => this.getLabelById(label))
.filter(isDefined)
.filter((label) => label.name.startsWith(this.getLabelPrefix(domain)))
}


static getLabelById(labelId: string): LabelRegistryEntry | undefined {
return this.#labels.find(label => label.label_id === labelId)
}

static getLabelPrefix = (domain: string) => `ms_${domain}_`

/**
* Get the ids of the views.
*
* @return {string[]} An array of view ids.
*/
static getExposedViewIds(): string[] {
static getViewIds(): string[] {
if (!this.isInitialized()) {
console.warn("Helper class should be initialized before calling this method!");
}

return this.#getObjectKeysByPropertyValue(this.#strategyOptions.views, "hidden", false);
return Object.keys(this.#strategyOptions.views).filter(viewId => configurationDefaults.views[viewId]);
}

/**
Expand Down Expand Up @@ -429,6 +535,19 @@ class Helper {
return (entityUnhidden && domainMatches && entityLinked);
}

/**
* Get a target of entity IDs for the given domain.)
*
* @param {EntityRegistryEntry[]} entities - List of target entries.
* @return {HassServiceTarget} - A target for a service call.
*/
static toTargetEntities(entities: EntityRegistryEntry[]): HassServiceTarget {
return {
entity_id: entities
.map(entity => entity.entity_id)
};
}

/**
* Get the keys of nested objects by its property value.
*
Expand Down
25 changes: 9 additions & 16 deletions src/cards/ControllerCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@ import {HassServiceTarget} from "home-assistant-js-websocket";
* @class
*/
class ControllerCard {
/**
* @type {HassServiceTarget} The target to control the entities of.
* @private
*/
readonly #target: HassServiceTarget;

/**
* Default configuration of the card.
*
Expand All @@ -38,8 +32,7 @@ class ControllerCard {
* @param {HassServiceTarget} target The target to control the entities of.
* @param {cards.ControllerCardOptions} options Controller Card options.
*/
constructor(target: HassServiceTarget, options: cards.ControllerCardOptions = {}) {
this.#target = target;
constructor(private target: HassServiceTarget, options: cards.ControllerCardOptions = {}) {
this.#defaultConfig = {
...this.#defaultConfig,
...options,
Expand All @@ -66,25 +59,25 @@ class ControllerCard {
cards: [
{
type: "custom:mushroom-template-card",
icon: this.#defaultConfig.iconOff,
icon: this.#defaultConfig.iconOn,
layout: "vertical",
icon_color: "red",
icon_color: "amber",
tap_action: {
action: "call-service",
service: this.#defaultConfig.offService,
target: this.#target,
service: this.#defaultConfig.onService,
target: this.target,
data: {},
},
},
{
type: "custom:mushroom-template-card",
icon: this.#defaultConfig.iconOn,
icon: this.#defaultConfig.iconOff,
layout: "vertical",
icon_color: "amber",
icon_color: "red",
tap_action: {
action: "call-service",
service: this.#defaultConfig.onService,
target: this.#target,
service: this.#defaultConfig.offService,
target: this.target,
data: {},
},
},
Expand Down
46 changes: 46 additions & 0 deletions src/cards/LawnMowerCard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {AbstractCard} from "./AbstractCard";
import {cards} from "../types/strategy/cards";
import {EntityRegistryEntry} from "../types/homeassistant/data/entity_registry";
import {LAWN_MOWER_COMMANDS, LawnMowerCardConfig} from "../types/lovelace-mushroom/cards/lawn-mower-card-config";

// noinspection JSUnusedGlobalSymbols Class is dynamically imported.
/**
* LawnMower Card Class
*
* Used to create a card for controlling an entity of the vacuum domain.
*
* @class
* @extends AbstractCard
*/
class LawnMowerCard extends AbstractCard {
/**
* Default configuration of the card.
*
* @type {LawnMowerCardConfig}
* @private
*/
#defaultConfig: LawnMowerCardConfig = {
type: "custom:mushroom-vacuum-card",
icon: undefined,
icon_animation: true,
commands: [...LAWN_MOWER_COMMANDS],
tap_action: {
action: "more-info",
}
};

/**
* Class constructor.
*
* @param {EntityRegistryEntry} entity The hass entity to create a card for.
* @param {cards.LawnMowerCardOptions} [options={}] Options for the card.
* @throws {Error} If the Helper module isn't initialized.
*/
constructor(entity: EntityRegistryEntry, options: cards.LawnMowerCardOptions = {}) {
super(entity);

this.config = Object.assign(this.config, this.#defaultConfig, options);
}
}

export {LawnMowerCard};
Loading