meteoalarm-card
Version:
Meteoalarm card for Home Assistant Lovelace UI
233 lines (209 loc) • 7.45 kB
text/typescript
import { HassEntity } from 'home-assistant-js-websocket';
import { MeteoalarmData, MeteoalarmEventInfo, MeteoalarmLevelInfo } from './data';
import { localize } from './localize/localize';
import { PredefinedCards } from './predefined-cards';
import {
MeteoalarmAlert,
MeteoalarmAlertKind,
MeteoalarmAlertParsed,
MeteoalarmEventType,
MeteoalarmIntegration,
MeteoalarmIntegrationEntityType,
} from './types';
/**
* This is the class that stands between integration and rendering code.
*/
class EventsParser {
constructor(private integration: MeteoalarmIntegration) {}
/**
* This function parses all of the provided entities and provides their attributes
* to integration specified in constructors. The result is additionally processed and
* verified then, packed to array of MeteoalarmAlertParsed objects
*/
public getEvents(
entities: HassEntity[],
disableSweeper = false,
overrideHeadline = false,
hideCaption = false,
ignoredLevels: string[] = [],
ignoredEvents: string[] = [],
): MeteoalarmAlertParsed[] {
if (this.isAnyEntityUnavailable(entities)) {
return [PredefinedCards.unavailableCard()];
}
this.checkIfIntegrationSupportsEntities(entities);
let alerts = this.sortAlerts(this.graterAllAlerts(entities));
this.validateAlert(alerts);
alerts = this.filterAlerts(alerts, ignoredLevels, ignoredEvents);
const result: MeteoalarmAlertParsed[] = [];
for (const alert of alerts) {
const event = MeteoalarmData.getEvent(alert.event);
const level = MeteoalarmData.getLevel(alert.level);
const headlines = this.generateHeadlines(event, level);
// If there is provided headline, and user wants it, push it to headlines
if (!overrideHeadline && alert.headline) {
headlines.unshift(alert.headline);
}
let caption: string | undefined = undefined;
let captionIcon: string | undefined = undefined;
if (!hideCaption) {
if (alert.kind == MeteoalarmAlertKind.Expected) {
caption = localize('common.expected');
captionIcon = 'clock-outline';
}
}
result.push({
isActive: true,
entity: alert._entity!,
icon: event.icon,
cssClass: level.cssClass,
headlines: headlines,
caption: caption,
captionIcon: captionIcon,
});
}
// If there are no results that mean above loop didn't trigger
// event parsing even once since every sensor was inactive.
if (result.length == 0) {
return [PredefinedCards.noWarningsCard(entities[0])];
}
return disableSweeper ? result.slice(1) : result;
}
/**
* Call integration for each of the entities and put all alerts in array
*/
private graterAllAlerts(entities: HassEntity[]): MeteoalarmAlert[] {
const alerts: MeteoalarmAlert[] = [];
for (const entity of entities) {
const active = this.integration.alertActive(entity);
if (!active) continue;
let entityAlerts = this.integration.getAlerts(entity);
if (!Array.isArray(entityAlerts)) {
entityAlerts = [entityAlerts];
}
if (entityAlerts.length == 0) {
throw new Error('Integration is active but did not return any events');
}
for (const alert of entityAlerts) {
alerts.push({ ...alert, _entity: entity });
}
}
return alerts;
}
private filterAlerts(
alerts: MeteoalarmAlert[],
ignoredLevels: string[],
ignoredEvents: string[],
): MeteoalarmAlert[] {
if (ignoredEvents.length == 0 && ignoredLevels.length == 0) return alerts;
const result: MeteoalarmAlert[] = [];
for (const alert of alerts) {
const eventInfo = MeteoalarmData.events.find((e) => e.type == alert.event)!;
const levelInfo = MeteoalarmData.levels.find((e) => e.type == alert.level)!;
if (
!ignoredEvents.includes(eventInfo.fullName) &&
!ignoredLevels.includes(levelInfo.fullName)
) {
result.push(alert);
}
}
return result;
}
private sortAlerts(alertsInput: MeteoalarmAlert[]): MeteoalarmAlert[] {
let alerts = [...alertsInput];
// Sort by how dangerous events are
alerts = alerts.sort((a, b) => {
const eventsData = MeteoalarmData.events;
const aLevel = eventsData.indexOf(eventsData.find((e) => e.type == a.event)!);
const bLevel = eventsData.indexOf(eventsData.find((e) => e.type == b.event)!);
return bLevel - aLevel;
});
// Sort by level
alerts = alerts.sort((a, b) => b.level - a.level);
// Push expected events to back of the list
alerts = alerts.sort((a, b) => {
if (a.kind === undefined) return 0;
if (a.kind == MeteoalarmAlertKind.Current && b.kind == MeteoalarmAlertKind.Expected)
return -1;
else if (a.kind == MeteoalarmAlertKind.Expected && b.kind == MeteoalarmAlertKind.Current)
return 1;
return 0;
});
return alerts;
}
/**
* Validate if specified alert is up to standards
*/
private validateAlert(alerts: MeteoalarmAlert[]): void {
for (const alert of alerts) {
if (alert.event === undefined || alert.level === undefined) {
throw new Error(
`[Alert QA Error] Invalid event object received: event: ${alert.event} level: ${alert.level}`,
);
}
if (!this.integration.metadata.returnHeadline && alert.headline) {
throw new Error(
'[Alert QA Error] metadata.returnHeadline is false but headline was returned',
);
}
if (
this.integration.metadata.type == MeteoalarmIntegrationEntityType.CurrentExpected &&
alert.kind == undefined
) {
throw new Error('[Alert QA Error] CurrentExpected type is required to provide alert.kind');
}
if (
this.integration.metadata.type != MeteoalarmIntegrationEntityType.CurrentExpected &&
alert.kind != undefined
) {
throw new Error('[Alert QA Error] only CurrentExpected type can return alert.kind');
}
if (!this.integration.metadata.returnMultipleAlerts && alerts.length > 1) {
throw new Error(
'[Alert QA Error] returnMultipleAlerts is false but more than one alert was returned',
);
}
}
}
// Artificially generate headlines from event type and level
private generateHeadlines(event: MeteoalarmEventInfo, level: MeteoalarmLevelInfo): string[] {
if (event.type === MeteoalarmEventType.Unknown) {
return [
localize(level.translationKey + '.generic'),
localize(level.translationKey + '.color'),
];
} else {
const e = localize(event.translationKey);
return [
localize(level.translationKey + '.event').replace(
'{event}',
localize(event.translationKey),
),
e.charAt(0).toUpperCase() + e.slice(1),
];
}
}
private isAnyEntityUnavailable(entities: HassEntity[]): boolean {
return entities.some((e) => {
return (
e == undefined || (e.attributes.status || e.attributes.state || e.state) === 'unavailable'
);
});
}
private checkIfIntegrationSupportsEntities(entities: HassEntity[]): void {
if (!entities.every((e) => this.integration.supports(e))) {
if (entities.length == 1) {
throw new Error(localize('error.entity_invalid.single'));
} else {
const unsupportedEntities = entities.filter((e) => !this.integration.supports(e));
throw new Error(
localize('error.entity_invalid.multiple').replace(
'{entity}',
unsupportedEntities.map((x) => x.entity_id).join(', '),
),
);
}
}
}
}
export default EventsParser;