mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
feat v1.13.0: universality — floors-import wizard, editable icon rules, tap actions (phase 9)
- 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
This commit is contained in:
+118
-39
@@ -1,53 +1,132 @@
|
||||
/**
|
||||
* Prototype rules — ported 1-to-1 from index.html/build_data.py (see DOCUMENTATION.md §4.2–4.5).
|
||||
* 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 (curation). */
|
||||
/** 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',
|
||||
]);
|
||||
|
||||
const ICON_RULES: Array<[RegExp, string]> = [
|
||||
[/протечк/, 'mdi:water-alert'],
|
||||
[/клапан/, 'mdi:pipe-valve'],
|
||||
[/дым/, 'mdi:smoke-detector'],
|
||||
[/термоголов/, 'mdi:radiator'],
|
||||
[/температ/, 'mdi:thermometer'],
|
||||
[/qingping|air monitor|молекул/, 'mdi:air-filter'],
|
||||
[/штор/, 'mdi:roller-shade'],
|
||||
[/розетк|plug/, 'mdi:power-socket-de'],
|
||||
[/выключат|switch/, 'mdi:light-switch'],
|
||||
[/лампа|лампочк|bulb|gx53|светильник|rgb/, 'mdi:lightbulb'],
|
||||
[/камер|camera/, 'mdi:cctv'],
|
||||
[/замок|ttlock|lock|sn609|sn9161/, 'mdi:lock'],
|
||||
[/ворота|garage/, 'mdi:garage-variant'],
|
||||
[/калитк|door|открыт/, 'mdi:door'],
|
||||
[/счётчик|счетчик|kws|meter/, 'mdi:meter-electric'],
|
||||
[/вводный автомат|breaker|wifimcbn/, 'mdi:electric-switch'],
|
||||
[/myheat|котёл|котел|boiler|отоплен/, 'mdi:water-boiler'],
|
||||
[/холодильник|fridge/, 'mdi:fridge'],
|
||||
[/стиральн|washer/, 'mdi:washing-machine'],
|
||||
[/сушилк|dryer/, 'mdi:tumble-dryer'],
|
||||
[/пылесос|vacuum|dreame/, 'mdi:robot-vacuum'],
|
||||
[/soundbar|колонк|станц/, 'mdi:soundbar'],
|
||||
[/tv|телевизор|hyundaitv|mitv/, 'mdi:television'],
|
||||
[/keenetic|роутер|router/, 'mdi:router-wireless'],
|
||||
[/ибп|ups|kirpich/, 'mdi:battery-charging-high'],
|
||||
[/slzb|координат|zigbee/, 'mdi:zigbee'],
|
||||
];
|
||||
|
||||
/** Pick an MDI icon by device name/model. */
|
||||
export function iconFor(name?: string, model?: string): string {
|
||||
const s = ((name || '') + ' ' + (model || '')).toLowerCase();
|
||||
for (const [pat, icon] of ICON_RULES) {
|
||||
if (pat.test(s)) return icon;
|
||||
}
|
||||
return 'mdi:chip';
|
||||
export interface IconRule {
|
||||
pattern: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
/** Domain priority for picking the device's “primary” entity (more-info). */
|
||||
/**
|
||||
* 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',
|
||||
|
||||
Reference in New Issue
Block a user