mirror of
https://github.com/DigiLive/mushroom-strategy.git
synced 2025-06-25 01:21:52 +02:00
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:
@ -9,5 +9,6 @@
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": false
|
||||
}
|
||||
|
@ -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 -->
|
||||
|
||||
|
1
dist/mushroom-strategy.js
vendored
1
dist/mushroom-strategy.js
vendored
File diff suppressed because one or more lines are too long
@ -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
19
package-lock.json
generated
@ -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": {
|
||||
|
@ -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",
|
||||
|
@ -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
13
src/notifications.ts
Normal 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',
|
||||
},
|
||||
];
|
189
src/utilities/PersistentNotification.ts
Normal file
189
src/utilities/PersistentNotification.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user