Add Notice Manager (#239)

* Adds the ability to create and dismiss persistent HASS notifications.

* Bump Strategy version to v2.3.5
This commit is contained in:
Ferry Cools
2025-06-20 16:40:02 +02:00
committed by GitHub
parent 43d5996453
commit 509b521176
9 changed files with 272 additions and 20 deletions

View File

@ -9,5 +9,6 @@
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false
}

View File

@ -66,11 +66,11 @@ We welcome contributions and feedback!
[hacsBadge]: https://img.shields.io/badge/HACS-Default-blue
[releaseBadge]: https://img.shields.io/github/v/tag/digilive/mushroom-strategy?filter=v2.3.4&label=Release
[releaseBadge]: https://img.shields.io/github/v/tag/digilive/mushroom-strategy?filter=v2.3.5&label=Release
<!-- Repository References -->
[releaseUrl]: https://github.com/DigiLive/mushroom-strategy/releases/tag/v2.3.4
[releaseUrl]: https://github.com/DigiLive/mushroom-strategy/releases/tag/v2.3.5
<!-- Other References -->

File diff suppressed because one or more lines are too long

View File

@ -68,11 +68,11 @@ support helps us grow and improve.
[hacsBadge]: https://img.shields.io/badge/HACS-Default-blue
[releaseBadge]: https://img.shields.io/github/v/tag/digilive/mushroom-strategy?filter=v2.3.4&label=Release
[releaseBadge]: https://img.shields.io/github/v/tag/digilive/mushroom-strategy?filter=v2.3.5&label=Release
<!-- Repository References -->
[releaseUrl]: https://github.com/DigiLive/mushroom-strategy/releases/tag/v2.3.4
[releaseUrl]: https://github.com/DigiLive/mushroom-strategy/releases/tag/v2.3.5
<!-- Other References -->

19
package-lock.json generated
View File

@ -1,17 +1,18 @@
{
"name": "mushroom-strategy",
"version": "2.3.4",
"version": "2.3.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mushroom-strategy",
"version": "2.3.2",
"version": "2.3.4",
"license": "MIT",
"dependencies": {
"deepmerge": "^4"
},
"devDependencies": {
"@types/semver": "^7.7.0",
"@typescript-eslint/eslint-plugin": "^8.33.0",
"@typescript-eslint/parser": "^8.32.1",
"eslint": "^9.27.0",
@ -20,6 +21,7 @@
"home-assistant-js-websocket": "^9.5.0",
"markdownlint-cli2": "^0.18.1",
"prettier": "^3.5.3",
"semver": "^7.7.2",
"superstruct": "^2.0.2",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
@ -560,6 +562,13 @@
"undici-types": "~5.26.4"
}
},
"node_modules/@types/semver": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz",
"integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/unist": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
@ -4072,9 +4081,9 @@
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {

View File

@ -1,6 +1,6 @@
{
"name": "mushroom-strategy",
"version": "2.3.4",
"version": "2.3.5",
"description": "Automatically generate a dashboard of Mushroom cards.",
"keywords": [
"dashboard",
@ -30,6 +30,7 @@
"deepmerge": "^4"
},
"devDependencies": {
"@types/semver": "^7.7.0",
"@typescript-eslint/eslint-plugin": "^8.33.0",
"@typescript-eslint/parser": "^8.32.1",
"eslint": "^9.27.0",
@ -38,6 +39,7 @@
"home-assistant-js-websocket": "^9.5.0",
"markdownlint-cli2": "^0.18.1",
"prettier": "^3.5.3",
"semver": "^7.7.2",
"superstruct": "^2.0.2",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",

View File

@ -17,6 +17,10 @@ import { sanitizeClassName } from './utilities/auxiliaries';
import { logMessage, lvlError, lvlInfo } from './utilities/debug';
import RegistryFilter from './utilities/RegistryFilter';
import { stackHorizontal } from './utilities/cardStacking';
import { PersistentNotification } from './utilities/PersistentNotification';
import { HomeAssistant } from './types/homeassistant/types';
import semver from 'semver/preload';
import { NOTIFICATIONS } from './notifications';
/**
* Mushroom Dashboard Strategy.<br>
@ -41,6 +45,8 @@ class MushroomStrategy extends HTMLTemplateElement {
static async generateDashboard(info: DashboardInfo): Promise<LovelaceConfig> {
await Registry.initialize(info);
await MushroomStrategy.handleNotifications(info.hass);
const views: StrategyViewConfig[] = [];
// Parallelize view imports and creation.
@ -90,7 +96,7 @@ class MushroomStrategy extends HTMLTemplateElement {
type: 'custom:mushroom-strategy',
options: { area },
},
})),
}))
);
return { views };
@ -135,7 +141,7 @@ class MushroomStrategy extends HTMLTemplateElement {
{
...Registry.strategyOptions.domains['_'],
...Registry.strategyOptions.domains[domain],
},
}
).createCard();
try {
@ -157,7 +163,7 @@ class MushroomStrategy extends HTMLTemplateElement {
if (domainCards.length) {
domainCards = stackHorizontal(
domainCards,
Registry.strategyOptions.domains[domain].stack_count ?? Registry.strategyOptions.domains['_'].stack_count,
Registry.strategyOptions.domains[domain].stack_count ?? Registry.strategyOptions.domains['_'].stack_count
);
return { type: 'vertical-stack', cards: [headerCard, ...domainCards] };
@ -176,7 +182,7 @@ class MushroomStrategy extends HTMLTemplateElement {
domainCards = stackHorizontal(
domainCards,
Registry.strategyOptions.domains[domain].stack_count ?? Registry.strategyOptions.domains['_'].stack_count,
Registry.strategyOptions.domains[domain].stack_count ?? Registry.strategyOptions.domains['_'].stack_count
);
return domainCards.length ? { type: 'vertical-stack', cards: [headerCard, ...domainCards] } : null;
@ -201,7 +207,7 @@ class MushroomStrategy extends HTMLTemplateElement {
try {
const MiscellaneousCard = (await import('./cards/MiscellaneousCard')).default;
let miscellaneousCards = miscellaneousEntities.map((entity) =>
new MiscellaneousCard(entity, Registry.strategyOptions.card_options?.[entity.entity_id]).getCard(),
new MiscellaneousCard(entity, Registry.strategyOptions.card_options?.[entity.entity_id]).getCard()
);
const headerCard = new HeaderCard(target, {
@ -213,7 +219,7 @@ class MushroomStrategy extends HTMLTemplateElement {
miscellaneousCards = stackHorizontal(
miscellaneousCards,
Registry.strategyOptions.domains['default'].stack_count ??
Registry.strategyOptions.domains['_'].stack_count,
Registry.strategyOptions.domains['_'].stack_count
);
viewCards.push({
@ -229,13 +235,46 @@ class MushroomStrategy extends HTMLTemplateElement {
return { cards: viewCards };
}
/**
* Handle persistent notifications.
*
* @remarks
* Goes through `NOTIFICATIONS` and shows each one whose version range matches the current version.
* If the current version is not applicable, the notification is dismissed.
*
* @param hass The Home Assistant instance.
* @returns A promise that resolves when all notifications have been handled.
*/
private static async handleNotifications(hass: HomeAssistant): Promise<void> {
const notificationManager = new PersistentNotification(hass, 'mushroom_strategy');
const currentVersion = STRATEGY_VERSION.replace(/^v/, '');
const version = semver.coerce(currentVersion) || '0.0.0';
try {
await Promise.all(
NOTIFICATIONS.map(async (notification) => {
if (semver.gte(version, notification.fromVersion) && semver.lte(version, notification.toVersion)) {
return notificationManager.showNotification(notification.storageKey, notification.message, {
title: notification.title,
version: currentVersion,
});
}
return notificationManager.dismissNotification(notification.storageKey);
})
);
} catch (e) {
logMessage(lvlError, 'Error while handling persistent notifications for Mushroom Strategy', e);
}
}
}
customElements.define('ll-strategy-mushroom-strategy', MushroomStrategy);
const version = 'v2.3.4';
const STRATEGY_VERSION = 'v2.3.5';
console.info(
'%c Mushroom Strategy %c '.concat(version, ' '),
'%c Mushroom Strategy %c '.concat(STRATEGY_VERSION, ' '),
'color: white; background: coral; font-weight: 700;',
'color: coral; background: white; font-weight: 700;',
'color: coral; background: white; font-weight: 700;'
);

13
src/notifications.ts Normal file
View File

@ -0,0 +1,13 @@
export const NOTIFICATIONS = [
{
storageKey: 'chips_deprecation',
title: 'Mushroom Strategy',
message:
'## Deprecation Notice\n' +
'As of v3.0.0, chips are replaced by badges.\n' +
'From that version on, you must rename all `chip` or `chips` references and settings in your YAML configuration.\n' +
'The [documentation](https://digilive.github.io/mushroom-strategy/options/home-view-options/) will be updated accordingly.',
fromVersion: '2.3.5',
toVersion: '3.0.0',
},
];

View File

@ -0,0 +1,189 @@
import { HomeAssistant } from '../types/homeassistant/types';
import { logMessage, lvlDebug, lvlError, lvlInfo } from './debug';
/**
* Configuration options for persistent notifications.
*
* @property {string} [title] The title to display in the notification.
* @property {string} [storageKey] The key name for storing the notification state into local storage.
* @property {string} [version] Version string for the notification.
* @property {string} hassId User-defined id of the notification in Home Assistant.
*/
interface NotificationOptions {
title?: string;
storageKey?: string;
version?: string;
hassId?: string;
}
/**
* Represents a notification's state in storage.
*
* @property {boolean} shown Whether the notification has been shown to the user.
* @property {string} timestamp timestamp of when the notification was last shown.
* @property {string} version Version of the notification when it was stored.
* @property {string} hassId Id of the notification in Home Assistant.
*/
interface StoredNotification {
shown: boolean;
timestamp: string;
version: string;
hassId?: string;
}
/**
* A utility class for managing persistent notifications in Home Assistant.
* Handles showing, dismissing and tracking notifications to prevent duplicates.
*
* Notifications are stored in localStorage and can be versioned.
*
* @see https://www.home-assistant.io/integrations/persistent_notification/
*/
export class PersistentNotification {
private static readonly DEFAULT_NAMESPACE = 'mushroom_strategy';
private static readonly DEFAULT_TITLE = 'Mushroom Strategy Notification';
private readonly hass: HomeAssistant;
private readonly namespace: string;
/**
* Constructs a new PersistentNotification instance.
*
* @param hass The Home Assistant instance for interacting with the Home Assistant API.
* @param namespace An optional configuration object.
*/
constructor(hass: HomeAssistant, namespace: string = PersistentNotification.DEFAULT_NAMESPACE) {
this.hass = hass;
this.namespace = namespace;
}
/**
* Shows a persistent notification with the given message and options.
*
* @param storageKey The key name for the notification in the local storage.
* @param message The message to display in the notification.
* @param options Optional configuration options for the notification.
*
* @returns A promise that resolves when the notification is shown or the method has been called before with the same
* storage key.
*/
public async showNotification(storageKey: string, message: string, options: NotificationOptions = {}): Promise<void> {
if (this.hasBeenShown(storageKey)) {
return;
}
const compiledKey = this.compileStorageKey(storageKey, options.storageKey);
const notificationId = options.hassId || compiledKey;
const title = options.title || PersistentNotification.DEFAULT_TITLE;
try {
await this.hass.callService('persistent_notification', 'create', {
title: title,
message: message,
notification_id: notificationId,
});
this.markAsShown(storageKey, options.version || '1.0.0', notificationId);
} catch (error) {
logMessage(lvlError, `Failed to show notification '${storageKey}'!`, error);
logMessage(lvlInfo, `[${title}] ${message}`);
}
}
/**
* Clears a notification from the Home Assistant UI and localStorage.
*
* @param storageKey The key name of the notification in the local storage.
* @param customKey An optional custom key to use for storage.
*/
public async dismissNotification(storageKey: string, customKey?: string): Promise<void> {
storageKey = this.compileStorageKey(storageKey, customKey);
try {
const stored = localStorage.getItem(storageKey);
const notification = stored ? (JSON.parse(stored) as StoredNotification) : null;
// Clear from storage
localStorage.removeItem(storageKey);
// Clear the notification if notificationId is provided
if (notification?.hassId) {
await this.hass.callService('persistent_notification', 'dismiss', {
notification_id: notification.hassId,
});
return;
}
logMessage(lvlDebug, `Notification '${storageKey}' cleared from storage!`);
} catch (error) {
logMessage(lvlError, `Failed to clear notification '${storageKey}'!`, error);
}
}
/**
* Checks if a notification with the given id has been shown to the user.
*
* @param storageKey The key name of the notification in the local storage.
* @param customKey An optional custom key to use for storage.
* @returns True if the notification has been shown before, false otherwise.
*/
public hasBeenShown(storageKey: string, customKey?: string): boolean {
storageKey = this.compileStorageKey(storageKey, customKey);
try {
const stored = localStorage.getItem(storageKey);
if (!stored) {
return false;
}
const notification = JSON.parse(stored) as StoredNotification;
return notification.shown;
} catch {
return false;
}
}
/**
* Compiles a storage key for a given name.
*
* If a customKey is provided, it will be used directly.
* Otherwise, a storage key will be generated by combining the namespace with this given id.
*
* @param name The name of the key.
* @param customKey An optional custom key to use for storage.
* @returns The storage key.
*/
private compileStorageKey(name: string, customKey?: string): string {
if (customKey) {
return customKey;
}
const namespace = this.namespace || PersistentNotification.DEFAULT_NAMESPACE;
return `${namespace}_${name}`;
}
/**
* Marks a notification as shown.
*
* @param storageKey The key of the notification in localStorage.
* @param version The version of the notification.
* @param notificationId Id of the notification in Home Assistant.
*/
private markAsShown(storageKey: string, version: string, notificationId?: string): void {
storageKey = this.compileStorageKey(storageKey);
const notification: StoredNotification = {
shown: true,
timestamp: new Date().toISOString(),
version,
hassId: notificationId,
};
try {
localStorage.setItem(storageKey, JSON.stringify(notification));
} catch (error) {
logMessage(lvlError, 'Failed to save the notification state!', error);
}
}
}