mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
- floors wizard: spaces from the HA floor registry, step-by-step with mandatory plan upload per floor, skip support, auto-markup after the last one - icon rules as data (settings.icon_rules) + in-card editor with live test, invalid-regex guard, bilingual defaults, device_class fallback chain - tap_action (card) + per-device override; security model: card-wide toggle only for light/switch/fan/humidifier, explicit override for covers, never lock/alarm; 600ms long-press always opens the info card - i18n dictionaries → src/i18n/*.json (+parity tests); light-theme badge fixes - frontend tests 15→28
136 lines
5.6 KiB
TypeScript
Executable File
136 lines
5.6 KiB
TypeScript
Executable File
/**
|
|
* Device curation and icon rules.
|
|
*
|
|
* Icon rules are DATA, not code: the built-in defaults below can be overridden
|
|
* per instance via `config.settings.icon_rules` (edited in the card UI).
|
|
* A rule is `{ pattern, icon }`; patterns are case-insensitive regexes matched
|
|
* against "<device name> <model>", first match wins. Invalid user regexes are
|
|
* skipped silently at compile time (and flagged in the rules editor).
|
|
*/
|
|
|
|
/** Integration domains whose devices are hidden by default (curation). */
|
|
export const EXCLUDED_DOMAINS = new Set([
|
|
'hacs', 'sun', 'backup', 'hassio', 'met', 'telegram_bot', 'mobile_app',
|
|
'systemmonitor', 'better_thermostat', 'adaptive_lighting', 'yandex_pogoda',
|
|
'upnp_serial_number',
|
|
]);
|
|
|
|
export interface IconRule {
|
|
pattern: string;
|
|
icon: string;
|
|
}
|
|
|
|
/**
|
|
* Built-in defaults, bilingual (EN + RU device-name vocabularies).
|
|
* Kept as plain strings so the UI editor can display and clone them.
|
|
*/
|
|
export const DEFAULT_ICON_RULES: IconRule[] = [
|
|
{ pattern: 'протечк|leak|water sensor', icon: 'mdi:water-alert' },
|
|
{ pattern: 'клапан|valve', icon: 'mdi:pipe-valve' },
|
|
{ pattern: 'дым|smoke', icon: 'mdi:smoke-detector' },
|
|
{ pattern: 'термоголов|trv|radiator', icon: 'mdi:radiator' },
|
|
{ pattern: 'температ|temperature|climate sensor', icon: 'mdi:thermometer' },
|
|
{ pattern: 'qingping|air monitor|молекул|air quality', icon: 'mdi:air-filter' },
|
|
{ pattern: 'штор|curtain|blind|shade', icon: 'mdi:roller-shade' },
|
|
{ pattern: 'розетк|plug|socket|outlet', icon: 'mdi:power-socket-de' },
|
|
{ pattern: 'выключат|switch', icon: 'mdi:light-switch' },
|
|
{ pattern: 'лампа|лампочк|bulb|gx53|светильник|rgb|lamp|light strip', icon: 'mdi:lightbulb' },
|
|
{ pattern: 'камер|camera', icon: 'mdi:cctv' },
|
|
{ pattern: 'замок|ttlock|lock|sn609|sn9161', icon: 'mdi:lock' },
|
|
{ pattern: 'ворота|garage|gate', icon: 'mdi:garage-variant' },
|
|
{ pattern: 'калитк|door|открыт|contact', icon: 'mdi:door' },
|
|
{ pattern: 'счётчик|счетчик|kws|meter', icon: 'mdi:meter-electric' },
|
|
{ pattern: 'вводный автомат|breaker|wifimcbn', icon: 'mdi:electric-switch' },
|
|
{ pattern: 'myheat|котёл|котел|boiler|отоплен|heating', icon: 'mdi:water-boiler' },
|
|
{ pattern: 'холодильник|fridge', icon: 'mdi:fridge' },
|
|
{ pattern: 'стиральн|washer|washing', icon: 'mdi:washing-machine' },
|
|
{ pattern: 'сушилк|dryer', icon: 'mdi:tumble-dryer' },
|
|
{ pattern: 'пылесос|vacuum|dreame|roborock', icon: 'mdi:robot-vacuum' },
|
|
{ pattern: 'soundbar|колонк|станц|speaker', icon: 'mdi:soundbar' },
|
|
{ pattern: 'tv|телевизор|hyundaitv|mitv|television', icon: 'mdi:television' },
|
|
{ pattern: 'keenetic|роутер|router|mesh|access point', icon: 'mdi:router-wireless' },
|
|
{ pattern: 'ибп|ups|kirpich', icon: 'mdi:battery-charging-high' },
|
|
{ pattern: 'slzb|координат|zigbee|coordinator', icon: 'mdi:zigbee' },
|
|
{ pattern: 'motion|движен|presence|присутств', icon: 'mdi:motion-sensor' },
|
|
{ pattern: 'humidity|влажн', icon: 'mdi:water-percent' },
|
|
];
|
|
|
|
export interface CompiledIconRule {
|
|
re: RegExp;
|
|
icon: string;
|
|
}
|
|
|
|
/** Compile rules, silently skipping invalid regexes (user input!). */
|
|
export function compileIconRules(rules: IconRule[]): CompiledIconRule[] {
|
|
const res: CompiledIconRule[] = [];
|
|
for (const r of rules) {
|
|
if (!r || typeof r.pattern !== 'string' || !r.icon) continue;
|
|
try {
|
|
res.push({ re: new RegExp(r.pattern, 'i'), icon: r.icon });
|
|
} catch {
|
|
/* invalid user regex — skipped; the rules editor highlights it */
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/** Whether a single rule pattern is a valid regex (for the editor UI). */
|
|
export function isValidPattern(pattern: string): boolean {
|
|
try {
|
|
new RegExp(pattern, 'i');
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const DEFAULT_COMPILED = compileIconRules(DEFAULT_ICON_RULES);
|
|
|
|
/** Fallback by entity device_class / domain when no name rule matched. */
|
|
const DEVICE_CLASS_ICONS: Record<string, string> = {
|
|
temperature: 'mdi:thermometer',
|
|
humidity: 'mdi:water-percent',
|
|
motion: 'mdi:motion-sensor',
|
|
occupancy: 'mdi:motion-sensor',
|
|
door: 'mdi:door',
|
|
window: 'mdi:window-closed',
|
|
garage_door: 'mdi:garage-variant',
|
|
smoke: 'mdi:smoke-detector',
|
|
moisture: 'mdi:water-alert',
|
|
gas: 'mdi:gas-cylinder',
|
|
power: 'mdi:meter-electric',
|
|
energy: 'mdi:meter-electric',
|
|
illuminance: 'mdi:brightness-5',
|
|
co2: 'mdi:molecule-co2',
|
|
pm25: 'mdi:air-filter',
|
|
battery: 'mdi:battery',
|
|
};
|
|
|
|
export const FALLBACK_ICON = 'mdi:chip';
|
|
|
|
/** Pick an MDI icon by device name/model using the given (or default) rules. */
|
|
export function iconFor(name?: string, model?: string, rules?: CompiledIconRule[]): string {
|
|
const s = ((name || '') + ' ' + (model || '')).toLowerCase();
|
|
for (const { re, icon } of rules ?? DEFAULT_COMPILED) {
|
|
if (re.test(s)) return icon;
|
|
}
|
|
return FALLBACK_ICON;
|
|
}
|
|
|
|
/** Icon from entity device_class attributes (used when name rules gave the fallback). */
|
|
export function iconFromDeviceClasses(deviceClasses: string[]): string | null {
|
|
for (const dc of deviceClasses) {
|
|
const icon = DEVICE_CLASS_ICONS[dc];
|
|
if (icon) return icon;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Priority of domains for picking a device's "primary" entity (more-info). */
|
|
export const DOMAIN_PRIORITY = [
|
|
'light', 'switch', 'cover', 'valve', 'lock', 'climate', 'fan',
|
|
'media_player', 'camera', 'vacuum', 'humidifier', 'water_heater',
|
|
'alarm_control_panel', 'sensor', 'binary_sensor', 'event', 'button',
|
|
'number', 'select', 'update',
|
|
];
|