iobroker.assistant 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/admin/assistant.svg +19 -0
- package/admin/custom/.vite/manifest.json +169 -0
- package/admin/custom/assets/Components-DcnuVFM6.js +1 -0
- package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-DogEjv3U.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-Dw5TkU1t.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-BEFjTs9S.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-C4r3udVz.js +1243 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_json_mf_2_config__loadShare__.js-DlOBHPL0.js +1494 -0
- package/admin/custom/assets/hostInit-DKMI-sLp.js +1 -0
- package/admin/custom/assets/index-B8wW0kQx.js +1 -0
- package/admin/custom/assets/lib-DpE8sc4h.js +3 -0
- package/admin/custom/assets/mf-entry-bootstrap-0-e7fa582d.js +36 -0
- package/admin/custom/assets/rolldown-runtime-C0FnF6B9.js +1 -0
- package/admin/custom/assets/virtualExposes-DOTO22UK.js +1 -0
- package/admin/custom/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__ConfigCustomAssistant__customComponents_js-CQa59yVi.js +1 -0
- package/admin/custom/assets/virtual_mf-exposes-ssr___mfe_internal__ConfigCustomAssistant__customComponents_js-9g2Fj8tk.js +2 -0
- package/admin/custom/assets/virtual_mf-exposes___mfe_internal__ConfigCustomAssistant__customComponents_js-BTUQTbYo.js +2 -0
- package/admin/custom/assets/vite-preload-helper-B7qeedMF.js +1 -0
- package/admin/custom/customComponents.js +1 -0
- package/admin/custom/customComponents.ssr.js +1 -0
- package/admin/custom/i18n/de.json +58 -0
- package/admin/custom/i18n/en.json +58 -0
- package/admin/custom/i18n/es.json +34 -0
- package/admin/custom/i18n/fr.json +34 -0
- package/admin/custom/i18n/it.json +34 -0
- package/admin/custom/i18n/nl.json +34 -0
- package/admin/custom/i18n/pl.json +34 -0
- package/admin/custom/i18n/pt.json +34 -0
- package/admin/custom/i18n/ru.json +34 -0
- package/admin/custom/i18n/uk.json +34 -0
- package/admin/custom/i18n/zh-cn.json +34 -0
- package/admin/custom/index.html +32 -0
- package/admin/custom/mf-manifest.json +1 -0
- package/admin/custom/mf-stats.json +1 -0
- package/admin/i18n/de.json +130 -0
- package/admin/i18n/en.json +130 -0
- package/admin/i18n/es.json +106 -0
- package/admin/i18n/fr.json +106 -0
- package/admin/i18n/it.json +106 -0
- package/admin/i18n/nl.json +106 -0
- package/admin/i18n/pl.json +106 -0
- package/admin/i18n/pt.json +106 -0
- package/admin/i18n/ru.json +106 -0
- package/admin/i18n/uk.json +106 -0
- package/admin/i18n/zh-cn.json +106 -0
- package/admin/jsonConfig.json +743 -0
- package/admin/sounds/alarm.wav +0 -0
- package/admin/sounds/timer.wav +0 -0
- package/build/lib/alarms.js +281 -0
- package/build/lib/alarms.js.map +1 -0
- package/build/lib/context.js +52 -0
- package/build/lib/context.js.map +1 -0
- package/build/lib/credentials.js +121 -0
- package/build/lib/credentials.js.map +1 -0
- package/build/lib/llm.js +303 -0
- package/build/lib/llm.js.map +1 -0
- package/build/lib/localLlm.js +221 -0
- package/build/lib/localLlm.js.map +1 -0
- package/build/lib/memory.js +190 -0
- package/build/lib/memory.js.map +1 -0
- package/build/lib/nlu.js +929 -0
- package/build/lib/nlu.js.map +1 -0
- package/build/lib/timers.js +245 -0
- package/build/lib/timers.js.map +1 -0
- package/build/lib/tools.js +299 -0
- package/build/lib/tools.js.map +1 -0
- package/build/lib/voice/aws.js +102 -0
- package/build/lib/voice/aws.js.map +1 -0
- package/build/lib/voice/azure.js +144 -0
- package/build/lib/voice/azure.js.map +1 -0
- package/build/lib/voice/download.js +76 -0
- package/build/lib/voice/download.js.map +1 -0
- package/build/lib/voice/engines.js +89 -0
- package/build/lib/voice/engines.js.map +1 -0
- package/build/lib/voice/lang.js +33 -0
- package/build/lib/voice/lang.js.map +1 -0
- package/build/lib/voice/piper.js +191 -0
- package/build/lib/voice/piper.js.map +1 -0
- package/build/lib/voice/protocol.js +35 -0
- package/build/lib/voice/protocol.js.map +1 -0
- package/build/lib/voice/stt.js +116 -0
- package/build/lib/voice/stt.js.map +1 -0
- package/build/lib/voice/tts.js +36 -0
- package/build/lib/voice/tts.js.map +1 -0
- package/build/lib/voice/voiceServer.js +249 -0
- package/build/lib/voice/voiceServer.js.map +1 -0
- package/build/lib/voice/vosk.js +260 -0
- package/build/lib/voice/vosk.js.map +1 -0
- package/build/lib/voice/wyoming.js +320 -0
- package/build/lib/voice/wyoming.js.map +1 -0
- package/build/lib/weather.js +423 -0
- package/build/lib/weather.js.map +1 -0
- package/build/main.js +3152 -0
- package/build/main.js.map +1 -0
- package/io-package.json +528 -0
- package/package.json +68 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nlu.js","sourceRoot":"","sources":["../../src/lib/nlu.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Tier-0 rule-based NLU (no model, no install, offline) — a TypeScript port of the proven matching\n * approach from the Python assistant \"Hannah\" (`core/hannah/nlu.py`), reduced to the intents this\n * adapter needs: turn on/off, set level, set color, and status queries.\n *\n * It matches a free-text command against the known rooms and devices (room + device + action, longest\n * match wins) and returns a structured {@link NluIntent} the caller executes directly via the ioBroker\n * API — bypassing the LLM entirely for the common commands. Anything it cannot confidently resolve\n * returns `null`, so the caller falls through to the local/cloud LLM.\n */\n\n/** A device as the NLU needs to see it: friendly name, room, type and its controls (controlType → stateId). */\nexport interface NluDevice {\n name: string;\n room: string;\n /** type-detector type, e.g. 'light', 'socket', 'dimmer', 'rgb', 'thermostat'. */\n type: string;\n /** controlType → state id (e.g. `power`, `level`, `brightness`, `actual`, …). */\n controls: Record<string, string>;\n /** controlType → writable flag. */\n writable: Record<string, boolean>;\n /** controlType → ioBroker value type ('boolean'|'number'|'string'|…), to pick the right control. */\n types: Record<string, string>;\n /** controlType → ioBroker role (e.g. 'switch.light', 'level.dimmer'), to find the on/off switch. */\n roles?: Record<string, string>;\n}\n\nexport type NluActionType =\n | 'on'\n | 'off'\n | 'level'\n | 'color'\n | 'query'\n | 'listByState'\n | 'timerSet'\n | 'timerQuery'\n | 'timerCancel'\n | 'alarmSet'\n | 'alarmQuery'\n | 'alarmCancel'\n | 'timeQuery'\n | 'dateQuery';\n\nexport interface NluIntent {\n action: NluActionType;\n /** Single-device actions (on/off/level/color/query). */\n device?: NluDevice;\n /** State id to act on (writable for on/off/level/color, readable for query). */\n stateId?: string;\n /** Value to write (bool for on/off, number for level, hex/temp string for color). */\n value?: boolean | number | string;\n /**\n * Secondary write applied after the primary one — used to also flip a dimmer's on/off switch when\n * setting its level (\"30%\" → level 30 **and** switch on), when the device has a separate switch control.\n */\n also?: { stateId: string; value: boolean };\n /** Matched room display name, if any. */\n room?: string;\n confidence: number;\n // ── Aggregate query (action 'listByState'), e.g. \"which windows are open\" ──\n /** All devices of the queried category (e.g. every window). */\n devices?: NluDevice[];\n /** The queried device category, e.g. 'window'. */\n category?: string;\n /** Which state to report on, e.g. 'open'. */\n stateFilter?: string;\n // ── Timer intents (timerSet/timerQuery/timerCancel) ──\n /** Duration in seconds for `timerSet`. */\n durationSec?: number;\n /** Optional timer/alarm label (e.g. \"die Nudeln\"), or a match hint for `timerCancel`/`alarmCancel`. */\n label?: string;\n // ── Alarm intents (alarmSet/alarmQuery/alarmCancel) ──\n /** Hour 0–23 for `alarmSet`. */\n hour?: number;\n /** Minute 0–59 for `alarmSet`. */\n minute?: number;\n /** Weekdays (0 = Sunday … 6 = Saturday) for `alarmSet`; empty = one-shot next occurrence. */\n weekdays?: number[];\n}\n\nconst TURN_ON = new Set([\n // de/en\n 'an',\n 'ein',\n 'einschalten',\n 'anschalten',\n 'anmachen',\n 'aktiviere',\n 'aktivieren',\n 'starte',\n 'start',\n 'on',\n // ru\n 'включи',\n 'включить',\n 'вруби',\n 'врубить',\n 'зажги',\n 'запусти',\n]);\nconst TURN_OFF = new Set([\n // de/en\n 'aus',\n 'ausschalten',\n 'abschalten',\n 'ausmachen',\n 'deaktiviere',\n 'deaktivieren',\n 'stoppe',\n 'stop',\n 'off',\n // ru\n 'выключи',\n 'выключить',\n 'выруби',\n 'вырубить',\n 'погаси',\n 'потуши',\n 'отключи',\n]);\nconst QUERY_WORDS = new Set([\n // de/en\n 'wie',\n 'was',\n 'ist',\n 'sind',\n 'status',\n 'wieviel',\n 'welche',\n 'welcher',\n 'zeig',\n 'zeige',\n 'laeuft',\n 'how',\n 'what',\n 'is',\n 'are',\n 'show',\n // ru\n 'какая',\n 'какой',\n 'какое',\n 'сколько',\n 'покажи',\n 'что',\n 'статус',\n]);\n/** Filler words removed before matching (so \"mach das Licht an\" → [\"licht\",\"an\"]). */\nconst FILLER = new Set([\n // de/en\n 'bitte',\n 'mal',\n 'doch',\n 'denn',\n 'einfach',\n 'kannst',\n 'du',\n 'koenntest',\n 'mach',\n 'die',\n 'das',\n 'den',\n 'der',\n 'the',\n 'please',\n 'und',\n 'im',\n 'in',\n 'auf',\n 'zum',\n 'zur',\n // ru\n 'пожалуйста',\n 'давай',\n 'ну',\n 'мне',\n 'на',\n 'в',\n 'и',\n]);\n/** Color names (de + ru) → hex, or a special token for color temperature. */\nconst COLORS: Record<string, string> = {\n rot: '#FF0000',\n gruen: '#00FF00',\n blau: '#0000FF',\n gelb: '#FFFF00',\n orange: '#FF8000',\n lila: '#8000FF',\n pink: '#FF69B4',\n magenta: '#FF00FF',\n cyan: '#00FFFF',\n tuerkis: '#00CED1',\n weiss: '#FFFFFF',\n warmweiss: 'warm',\n kaltweiss: 'cold',\n красный: '#FF0000',\n зеленый: '#00FF00',\n синий: '#0000FF',\n голубой: '#00BFFF',\n желтый: '#FFFF00',\n оранжевый: '#FF8000',\n фиолетовый: '#8000FF',\n розовый: '#FF69B4',\n белый: '#FFFFFF',\n};\n/** Device types that accept a color. */\nconst COLOR_TYPES = new Set(['rgb', 'rgbSingle', 'rgbwSingle', 'hue', 'cie', 'ct']);\n\n/** type-detector types that count as \"windows\" for the \"which windows are open\" aggregate query. */\nconst WINDOW_TYPES = new Set(['window', 'windowTilt']);\n/** Words (de/en/ru, stemmed) that name a window. */\nconst WINDOW_WORDS = ['fenster', 'window', 'windows', 'окно', 'окна', 'окон'];\n/** Words (de/en/ru, stemmed) that mean \"open\". */\nconst OPEN_WORDS = ['offen', 'geoeffnet', 'geoffnet', 'auf', 'open', 'открыт', 'открыта', 'открыты', 'открытые'];\n\n/** Words (de/en/ru, stemmed) that make a query an on/off question (\"is the light on?\"). */\nconst ON_OFF_QUERY_WORDS = [\n 'an',\n 'ein',\n 'aus',\n 'eingeschaltet',\n 'ausgeschaltet',\n 'on',\n 'off',\n 'включен',\n 'включена',\n 'включено',\n 'выключен',\n 'выключена',\n 'работает',\n];\n\n/**\n * State-id suffixes of pure measurements/indicators — never the on/off switch or level (but note ACTUAL\n * is NOT here: it is a valid read-back for \"is it on\"). Used to keep pickControl off metering states.\n */\nconst METER_SUFFIX =\n /(CONSUMPTION|ELECTRIC_POWER|CURRENT|VOLTAGE|ENERGY|POWER|LOWBAT|BATTERY|UNREACH|RSSI|TEMPERATURE)$/i;\n\n// ── Timers / reminders (de/en/ru) ───────────────────────────────────────────\n/**\n * Marks a command as being about a countdown timer. Never fires on the bare word \"time\" (\"what time is\n * it\") — it needs the full \"timer\". \"timers?\" carries no leading word boundary so German compounds\n * (\"Küchentimer\", \"Eieruhr\"-style) and the plural still match; the prefix forms keep a boundary.\n */\nconst TIMER_RE = /(?:(?<![\\p{L}])(reminder|remind|erinner\\p{L}*|таймер\\p{L}*|напомн\\p{L}*)|timers?)(?![\\p{L}])/iu;\n/** Words that turn a timer command into \"cancel/stop the timer\" (de/en/ru). */\nconst TIMER_CANCEL_RE =\n /(?<![\\p{L}])(abbrech\\p{L}*|beend\\p{L}*|stopp?\\p{L}*|loesch\\p{L}*|cancel|delete|remove|clear|отмен\\p{L}*|удал\\p{L}*|сброс\\p{L}*|стоп)(?![\\p{L}])/iu;\n/** Number words → digits (normalized: umlauts expanded), de/en/ru, plus common fractions. */\nconst NUMBER_WORDS: Record<string, string> = {\n ein: '1',\n eine: '1',\n einen: '1',\n eins: '1',\n zwei: '2',\n drei: '3',\n vier: '4',\n fuenf: '5',\n sechs: '6',\n sieben: '7',\n acht: '8',\n neun: '9',\n zehn: '10',\n elf: '11',\n zwoelf: '12',\n anderthalb: '1.5',\n eineinhalb: '1.5',\n halbe: '0.5',\n halb: '0.5',\n viertel: '0.25',\n dreiviertel: '0.75',\n one: '1',\n two: '2',\n three: '3',\n four: '4',\n five: '5',\n six: '6',\n seven: '7',\n eight: '8',\n nine: '9',\n ten: '10',\n eleven: '11',\n twelve: '12',\n half: '0.5',\n quarter: '0.25',\n один: '1',\n одну: '1',\n два: '2',\n две: '2',\n три: '3',\n четыре: '4',\n пять: '5',\n шесть: '6',\n семь: '7',\n восемь: '8',\n девять: '9',\n десять: '10',\n};\n/** Fixed phrases replaced up front so their fractions don't collide with the general number-word pass. */\nconst DURATION_PHRASES: [RegExp, string][] = [\n [/(?<![\\p{L}])dreiviertelstunde(?![\\p{L}])/iu, '45 minuten'],\n [/(?<![\\p{L}])viertelstunde(?![\\p{L}])/iu, '15 minuten'],\n [/(?<![\\p{L}])eine? halbe stunde(?![\\p{L}])/iu, '30 minuten'],\n [/(?<![\\p{L}])halbe stunde(?![\\p{L}])/iu, '30 minuten'],\n [/half an hour/iu, '30 minutes'],\n [/a half hour/iu, '30 minutes'],\n [/quarter of an hour/iu, '15 minutes'],\n [/quarter hour/iu, '15 minutes'],\n [/(?<![\\p{L}])полтора часа(?![\\p{L}])/iu, '90 минут'],\n [/(?<![\\p{L}])полчаса(?![\\p{L}])/iu, '30 минут'],\n];\nconst HOUR_RE = /(\\d+(?:[.,]\\d+)?)\\s*(?:stunden?|hours?|hrs?|h|час(?:а|ов)?)(?![\\p{L}])/giu;\nconst MIN_RE = /(\\d+(?:[.,]\\d+)?)\\s*(?:minuten?|mins?|minutes?|минут[уы]?)(?![\\p{L}])/giu;\nconst SEC_RE = /(\\d+(?:[.,]\\d+)?)\\s*(?:sekunden?|secs?|seconds?|секунд[уы]?)(?![\\p{L}])/giu;\n/** Duration/number tokens stripped from a timer label (\"timer für 5 minuten für die Nudeln\" → \"die Nudeln\"). */\nconst LABEL_STOP_WORDS = new Set([\n 'in',\n 'im',\n 'mich',\n 'me',\n 'my',\n 'den',\n 'die',\n 'das',\n 'der',\n 'the',\n 'a',\n 'an',\n 'auf',\n 'noch',\n 'ein',\n 'eine',\n 'einen',\n 'timer',\n 'reminder',\n 'remind',\n 'таймер',\n 'на',\n 'через',\n 'про',\n 'about',\n 'to',\n 'for',\n 'fuer',\n 'stunde',\n 'stunden',\n 'minute',\n 'minuten',\n 'min',\n 'sekunde',\n 'sekunden',\n 'sec',\n 'hour',\n 'hours',\n 'minutes',\n 'seconds',\n 'second',\n 'час',\n 'часа',\n 'часов',\n 'минут',\n 'минуту',\n 'минуты',\n 'секунд',\n 'секунду',\n 'секунды',\n]);\n/** Marker words after which a timer label follows (\"… an die Nudeln\", \"remind me to call mom\"). */\nconst LABEL_MARKERS = new Set(['an', 'fuer', 'for', 'to', 'about', 'про']);\n\n// ── Alarms / wake-ups at a fixed clock time (de/en/ru) ──────────────────────\n/**\n * Marks a command as being about an alarm / wake-up (\"Wecker\", \"weck mich\", \"alarm\", \"будильник\"). Like\n * {@link TIMER_RE}, \"alarms?\"/\"wecker\" carry no leading boundary so compounds (\"Küchenwecker\") and the\n * plural still match; the verb-prefix forms (\"weck…\", \"разбуд…\") keep a boundary.\n */\nconst ALARM_RE = /(?:(?<![\\p{L}])(weck\\p{L}*|wake|буд\\p{L}*|разбуд\\p{L}*)|alarms?|wecker)(?![\\p{L}])/iu;\n/** Hour number words → digits (no fractions — those are handled by the clock regexes). de/en/ru, 1–12. */\nconst HOUR_WORDS: Record<string, string> = {\n ein: '1',\n eine: '1',\n einen: '1',\n eins: '1',\n zwei: '2',\n drei: '3',\n vier: '4',\n fuenf: '5',\n sechs: '6',\n sieben: '7',\n acht: '8',\n neun: '9',\n zehn: '10',\n elf: '11',\n zwoelf: '12',\n one: '1',\n two: '2',\n three: '3',\n four: '4',\n five: '5',\n six: '6',\n seven: '7',\n eight: '8',\n nine: '9',\n ten: '10',\n eleven: '11',\n twelve: '12',\n один: '1',\n два: '2',\n три: '3',\n четыре: '4',\n пять: '5',\n шесть: '6',\n семь: '7',\n восемь: '8',\n девять: '9',\n десять: '10',\n одиннадцать: '11',\n двенадцать: '12',\n};\n/** Weekday word (normalized, prefix-matched for inflection) → 0 (Sun) … 6 (Sat). */\nconst WEEKDAYS: [RegExp, number][] = [\n [/(?<![\\p{L}])(montag|monday|понедельник)/iu, 1],\n [/(?<![\\p{L}])(dienstag|tuesday|вторник)/iu, 2],\n [/(?<![\\p{L}])(mittwoch|wednesday|сред)/iu, 3],\n [/(?<![\\p{L}])(donnerstag|thursday|четверг)/iu, 4],\n [/(?<![\\p{L}])(freitag|friday|пятниц)/iu, 5],\n [/(?<![\\p{L}])(samstag|sonnabend|saturday|суббот)/iu, 6],\n [/(?<![\\p{L}])(sonntag|sunday|воскресень)/iu, 0],\n];\n\n/**\n * Parse a clock time from free text (de/en/ru): \"7:30\", \"7 Uhr\", \"7 Uhr 30\", \"halb acht\" (7:30),\n * \"viertel nach sieben\" (7:15), \"viertel vor acht\"/\"dreiviertel acht\" (7:45), \"half past seven\",\n * \"quarter past/to\", \"в 7 часов\", plus am/pm. Returns {hour 0-23, minute 0-59} or null.\n */\nexport function parseClockTime(text: string): { hour: number; minute: number } | null {\n let t = normalize(text);\n // Substitute hour number words → digits (\"halb acht\" → \"halb 8\"), leaving fraction words intact.\n t = t\n .split(/\\s+/)\n .map(w => (Object.prototype.hasOwnProperty.call(HOUR_WORDS, w) ? HOUR_WORDS[w] : w))\n .join(' ');\n\n const pm = /(?<![\\p{L}])(pm|nachmittags|abends|вечера)(?![\\p{L}])/u.test(t);\n const am = /(?<![\\p{L}])(am|morgens|vormittags|utra|утра|nachts)(?![\\p{L}])/u.test(t);\n const applyMeridiem = (h: number): number => {\n if (pm && h < 12) {\n return h + 12;\n }\n if (am && h === 12) {\n return 0;\n }\n return h;\n };\n const ok = (h: number, m: number): { hour: number; minute: number } | null =>\n h >= 0 && h <= 23 && m >= 0 && m <= 59 ? { hour: h, minute: m } : null;\n\n let m: RegExpMatchArray | null;\n // HH:MM\n if ((m = t.match(/(?<!\\d)(\\d{1,2}):(\\d{2})(?!\\d)/))) {\n return ok(applyMeridiem(+m[1]), +m[2]);\n }\n // \"viertel nach H\" → H:15 ; \"viertel vor H\" / \"dreiviertel H\" → (H-1):45\n if ((m = t.match(/viertel\\s+nach\\s+(\\d{1,2})/))) {\n return ok(applyMeridiem(+m[1]), 15);\n }\n if ((m = t.match(/(?:viertel\\s+vor|dreiviertel)\\s+(\\d{1,2})/))) {\n return ok(applyMeridiem((+m[1] + 23) % 24), 45);\n }\n if ((m = t.match(/quarter\\s+past\\s+(\\d{1,2})/))) {\n return ok(applyMeridiem(+m[1]), 15);\n }\n if ((m = t.match(/quarter\\s+to\\s+(\\d{1,2})/))) {\n return ok(applyMeridiem((+m[1] + 23) % 24), 45);\n }\n // \"halb H\" (German) → (H-1):30 ; \"half past H\" (English) → H:30\n if ((m = t.match(/half\\s+past\\s+(\\d{1,2})/))) {\n return ok(applyMeridiem(+m[1]), 30);\n }\n if ((m = t.match(/(?<![\\p{L}])halb\\s+(\\d{1,2})/u))) {\n return ok(applyMeridiem((+m[1] + 23) % 24), 30);\n }\n // \"H Uhr MM\" / \"H Uhr\" / \"H часов MM\"\n if ((m = t.match(/(\\d{1,2})\\s*(?:uhr|часов|часа|час)\\s*(\\d{1,2})(?![\\p{L}])/u))) {\n return ok(applyMeridiem(+m[1]), +m[2]);\n }\n if ((m = t.match(/(\\d{1,2})\\s*(?:uhr|o.?clock|часов|часа|час)(?![\\p{L}])/u))) {\n return ok(applyMeridiem(+m[1]), 0);\n }\n // \"um/at/в H\" (bare hour after an explicit time preposition) — but not when a duration unit follows,\n // so \"Timer um 5 Minuten\" is not misread as 5 o'clock.\n if (\n (m = t.match(\n /(?<![\\p{L}])(?:um|at|в|во)\\s+(\\d{1,2})(?![\\d:])(?!\\s*(?:minut|stund|sekund|min|sek|hour|second))/u,\n ))\n ) {\n return ok(applyMeridiem(+m[1]), 0);\n }\n // Bare hour qualified only by am/pm (\"7 pm\", \"8 am\").\n if ((pm || am) && (m = t.match(/(?<![\\d.])(\\d{1,2})(?![\\d.:])/))) {\n return ok(applyMeridiem(+m[1]), 0);\n }\n return null;\n}\n\n/**\n * Parse recurrence from free text → weekday numbers (0 = Sun … 6 = Sat). \"täglich\"/\"jeden Tag\"/\"daily\"\n * → all 7; \"wochentags\"/\"werktags\"/\"weekdays\" → Mon–Fri; \"wochenende\"/\"weekend\" → Sat+Sun; otherwise the\n * union of the named weekdays. Empty array = one-shot.\n */\nexport function parseWeekdays(text: string): number[] {\n const t = normalize(text);\n if (/(?<![\\p{L}])(taeglich|jeden tag|daily|every day|ежедневно|каждый день)(?![\\p{L}])/u.test(t)) {\n return [0, 1, 2, 3, 4, 5, 6];\n }\n if (/(?<![\\p{L}])(wochentags|werktags|weekdays|по будням|будни)(?![\\p{L}])/u.test(t)) {\n return [1, 2, 3, 4, 5];\n }\n if (/(?<![\\p{L}])(wochenende|weekend|выходн\\p{L}*)(?![\\p{L}])/u.test(t)) {\n return [0, 6];\n }\n const days = new Set<number>();\n for (const [re, day] of WEEKDAYS) {\n if (re.test(t)) {\n days.add(day);\n }\n }\n return [...days].sort();\n}\n\n/**\n * Parse a free-text duration (\"1 Stunde 30 Minuten\", \"in 5 minuten\", \"полтора часа\") into seconds; null\n * if none is found. With `assumeMinutes`, a bare number with no unit (\"timer 5\") counts as minutes.\n */\nexport function parseDurationSeconds(text: string, assumeMinutes = false): number | null {\n let t = normalize(text);\n for (const [re, rep] of DURATION_PHRASES) {\n t = t.replace(re, rep);\n }\n // Replace standalone number words with digits so \"fünf minuten\" → \"5 minuten\".\n t = t\n .split(/\\s+/)\n .map(w => (Object.prototype.hasOwnProperty.call(NUMBER_WORDS, w) ? NUMBER_WORDS[w] : w))\n .join(' ');\n\n let total = 0;\n let matched = false;\n const sum = (re: RegExp, factor: number): void => {\n re.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(t))) {\n total += parseFloat(m[1].replace(',', '.')) * factor;\n matched = true;\n }\n };\n sum(HOUR_RE, 3600);\n sum(MIN_RE, 60);\n sum(SEC_RE, 1);\n\n if (!matched && assumeMinutes) {\n const bare = t.match(/(?<![\\d.])(\\d+(?:[.,]\\d+)?)(?![\\d.])/);\n if (bare) {\n total = parseFloat(bare[1].replace(',', '.')) * 60;\n matched = true;\n }\n }\n return matched && total > 0 ? Math.round(total) : null;\n}\n\n/**\n * \"Make it stop\" words (de/en/ru), used to silence a ringing timer/alarm. Kept separate from the device\n * TURN_OFF set (no \"aus\") so it only reacts to an explicit stop/quiet phrase, not \"turn the light off\".\n */\nconst STOP_RE =\n /(?<![\\p{L}])(stopp?\\p{L}*|halt|aufhoer\\p{L}*|ruhe|ruhig|genug|schluss|abbrech\\p{L}*|beend\\p{L}*|ende|still|cancel|enough|quiet|dismiss|silence|стоп|хватит|тихо|довольно|прекрат\\p{L}*|отмен\\p{L}*|замолч\\p{L}*|тишин\\p{L}*)(?![\\p{L}])/iu;\n\n/** True if the text is a \"stop / be quiet\" command (used to silence a ringing timer/alarm). */\nexport function isStopCommand(text: string): boolean {\n return STOP_RE.test(normalize(text));\n}\n\n/** Normalize for matching: lowercase, German umlauts → ascii, ß → ss, Russian ё → е. */\nexport function normalize(s: string): string {\n return s\n .toLowerCase()\n .replace(/ä/g, 'ae')\n .replace(/ö/g, 'oe')\n .replace(/ü/g, 'ue')\n .replace(/ß/g, 'ss')\n .replace(/ё/g, 'е');\n}\n\nconst STRIP = /[.,!?;:()[\\]]/g;\nconst escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/**\n * Does a name word appear in the text? Matches on a Unicode word boundary and allows a differing\n * suffix (crude stemming) so inflected forms match too — essential for Russian (\"подсветку\" for\n * \"подсветка\", \"кокпита\" for \"кокпит\"). Longer words drop more trailing characters.\n */\nfunction wordInText(word: string, text: string): boolean {\n const stem =\n word.length >= 6 ? word.slice(0, word.length - 2) : word.length >= 5 ? word.slice(0, word.length - 1) : word;\n try {\n return new RegExp(`(?<![\\\\p{L}\\\\p{N}])${escapeRe(stem)}`, 'u').test(text);\n } catch {\n return text.includes(stem);\n }\n}\n\nexport class Nlu {\n private readonly rooms: string[];\n private readonly devices: NluDevice[];\n /** Synonym dictionary as normalized [from, to] pairs, applied to the input before matching. */\n private readonly aliases: [string, string][];\n\n constructor(rooms: string[], devices: NluDevice[], aliases: { from: string; to: string }[] = []) {\n this.rooms = rooms.filter(Boolean);\n this.devices = devices;\n this.aliases = aliases\n .map(a => [normalize(a.from || ''), normalize(a.to || '')] as [string, string])\n .filter(([from]) => from);\n }\n\n /**\n * Rewrite synonyms to canonical terms on the (normalized) input, matching whole words/phrases only\n * (Unicode boundaries). E.g. \"tv\" → \"fernseher\", \"deckenlicht\" → \"licht wohnzimmer\". No-op when the\n * dictionary is empty. Applied once up front so all downstream matching sees the canonical wording.\n */\n private applyAliases(norm: string): string {\n let out = norm;\n for (const [from, to] of this.aliases) {\n try {\n out = out.replace(new RegExp(`(?<![\\\\p{L}\\\\p{N}])${escapeRe(from)}(?![\\\\p{L}\\\\p{N}])`, 'gu'), to);\n } catch {\n // Defensive: a malformed alias must never break parsing.\n }\n }\n return out;\n }\n\n /** Parse a command; returns a structured intent or null if nothing could be resolved confidently. */\n parse(text: string): NluIntent | null {\n const raw = text;\n const norm = this.applyAliases(normalize(text.replace(STRIP, ' ')));\n const tokens = norm.split(/\\s+/).filter(t => t && !FILLER.has(t));\n if (!tokens.length) {\n return null;\n }\n const joined = tokens.join(' ');\n const tokenSet = new Set(tokens);\n\n // Timer / alarm intents (\"timer 5 minuten\", \"weck mich um 7 Uhr\", \"wie lange noch\") — before\n // window/device matching, and device-independent.\n const schedule = this.parseSchedule(raw, norm, joined);\n if (schedule) {\n return schedule;\n }\n\n // Time / date query (\"what time is it\", \"which day is today\") — device-independent, answered from\n // the host clock. After the schedule parse so a clock phrase with a timer/alarm keyword still wins.\n const timeQuery = this.parseTimeQuery(joined);\n if (timeQuery) {\n return timeQuery;\n }\n\n // Aggregate query first (\"which windows are open\") — before single-device matching.\n const windowsQuery = this.parseWindowsOpen(joined);\n if (windowsQuery) {\n return windowsQuery;\n }\n\n const roomName = this.findRoom(joined);\n const device = this.findDevice(joined, roomName);\n if (!device) {\n return null; // no device → let the LLM handle it\n }\n\n const action = this.findAction(tokenSet); // 'on' | 'off' | null\n const level = this.findLevel(norm); // number | null\n const color = this.findColor(joined);\n const isQuery = tokens.some(t => QUERY_WORDS.has(t)) || raw.trim().endsWith('?');\n\n // A question (\"ist das Licht an?\") must never trigger a write — only the query branch below.\n // 1. Set level (dimmer/blind …) — needs a numeric level-like control.\n if (level !== null && !isQuery) {\n const stateId = this.pickControl(device, ['level', 'brightness', 'dimmer'], true, 'number');\n if (stateId) {\n const intent: NluIntent = {\n action: 'level',\n device,\n stateId,\n value: level,\n room: roomName,\n confidence: 0.9,\n };\n // Many dimmers have a separate on/off switch that setting the level does not flip — so also\n // turn it on (level > 0) / off (0%) if the device has one. Devices without a switch are\n // unaffected. The switch is found by role/type, not by name (not every device names it ON_SET).\n const sw = this.findSwitch(device, stateId);\n if (sw) {\n intent.also = { stateId: sw, value: level > 0 };\n }\n return intent;\n }\n }\n\n // 2. Set color — only a hex color on a color-capable device (warm/cold color-temp is out of scope for v1).\n if (color && color.startsWith('#') && COLOR_TYPES.has(device.type) && !isQuery) {\n const stateId = this.pickControl(device, ['rgb', 'color', 'hue', 'cie', 'ct'], true);\n if (stateId) {\n return { action: 'color', device, stateId, value: color, room: roomName, confidence: 0.85 };\n }\n }\n\n // 3. Turn on / off — prefer a writable boolean switch (never a metering state like CONSUMPTION).\n if (action && !isQuery) {\n const stateId = this.pickControl(device, ['power', 'switch', 'on', 'level'], true, 'boolean');\n if (stateId) {\n // If only a numeric (dimmer/level) control is available, on/off means full/zero level, not a\n // boolean 1/0 (writing `true` to a 0–100 state would land on ~1%).\n const numeric = this.valueTypeOf(device, stateId) === 'number';\n return {\n action,\n device,\n stateId,\n value: numeric ? (action === 'on' ? 100 : 0) : action === 'on',\n room: roomName,\n confidence: 0.9,\n };\n }\n }\n\n // 4. Status query — read the right control: an on/off question (\"is the light on?\") reads the\n // boolean switch/feedback; a value question (\"what temperature?\") reads the actual value.\n if (isQuery) {\n const onOff = ON_OFF_QUERY_WORDS.some(w => wordInText(w, joined));\n const order = onOff ? ['power', 'switch', 'on', 'actual', 'level'] : ['actual', 'value', 'level', 'power'];\n const stateId = this.pickControl(device, order, false, onOff ? 'boolean' : undefined);\n if (stateId) {\n return { action: 'query', device, stateId, room: roomName, confidence: 0.7 };\n }\n }\n\n return null;\n }\n\n /**\n * Timer (countdown) / alarm (fixed clock time) command (de/en/ru). Requires a timer or alarm keyword,\n * then classifies in priority order:\n * - a cancel word → `timerCancel` / `alarmCancel`,\n * - a parseable clock time → `alarmSet` (with weekdays / label / room),\n * - an explicit duration with a unit (\"5 Minuten\") → `timerSet` (so \"weck mich in 5 Minuten\" is a\n * countdown, not a clock time),\n * - a bare number: an hour in the alarm domain (\"Wecker auf 8\"), else minutes → `timerSet`,\n * - otherwise → `timerQuery` / `alarmQuery` (list what is running).\n */\n private parseSchedule(raw: string, norm: string, joined: string): NluIntent | null {\n const isTimer = TIMER_RE.test(norm);\n const isAlarm = ALARM_RE.test(norm);\n if (!isTimer && !isAlarm) {\n return null;\n }\n // Prefer the alarm reading only when it is the sole keyword (so \"erinnere mich um 7\" → alarm, but\n // \"timer …\" stays a timer even if it also mentions \"wecker\").\n const alarmDomain = isAlarm && !isTimer;\n const room = this.findRoom(joined) || undefined;\n const label = (): string | undefined => this.findTimerLabel(norm) || undefined;\n const alarm = (hour: number, minute: number, confidence: number): NluIntent => ({\n action: 'alarmSet',\n hour,\n minute,\n weekdays: parseWeekdays(norm),\n label: label(),\n room,\n confidence,\n });\n\n if (TIMER_CANCEL_RE.test(norm)) {\n return { action: alarmDomain ? 'alarmCancel' : 'timerCancel', label: label(), room, confidence: 0.9 };\n }\n\n // Clock time first (parsed from the raw text — the normalized `norm` has ':' stripped, which would\n // turn \"6:30\" into \"6 30\"). The \"um H\" rule already refuses a following duration unit, and the ru\n // \"H часов\" o'clock form is resolved here before it could be mistaken for \"H hours\".\n const clock = parseClockTime(raw);\n if (clock) {\n return alarm(clock.hour, clock.minute, 0.9);\n }\n\n // Explicit duration with a unit (\"5 Minuten\", \"1 Stunde\") → a countdown timer, even for \"Wecker in …\".\n const explicit = parseDurationSeconds(norm, false);\n if (explicit) {\n return { action: 'timerSet', durationSec: explicit, label: label(), room, confidence: 0.9 };\n }\n\n // A bare number: an hour for an alarm (\"Wecker auf 8\"), otherwise minutes for a timer (\"Timer 5\").\n const bare = parseDurationSeconds(norm, true);\n if (bare) {\n const hour = Math.round(bare / 60);\n if (alarmDomain && hour >= 0 && hour <= 23) {\n return alarm(hour, 0, 0.8);\n }\n return { action: 'timerSet', durationSec: bare, label: label(), room, confidence: 0.9 };\n }\n return { action: alarmDomain ? 'alarmQuery' : 'timerQuery', room, confidence: 0.7 };\n }\n\n /**\n * Extract a timer label: the text after the last marker word (\"an\", \"for\", \"to\", \"про\", …), stripped\n * of duration/number/filler tokens. \"erinnere mich in 5 minuten an den ofen\" → \"ofen\"; \"timer für 5\n * minuten\" → \"\" (no label). Operates on the normalized text (lowercased) — fine for a spoken reply.\n */\n private findTimerLabel(norm: string): string {\n const tokens = norm.split(/\\s+/).filter(Boolean);\n let start = -1;\n for (let i = 0; i < tokens.length; i++) {\n if (LABEL_MARKERS.has(tokens[i])) {\n start = i + 1;\n }\n }\n if (start < 0 || start >= tokens.length) {\n return '';\n }\n const words = tokens.slice(start).filter(w => !LABEL_STOP_WORDS.has(w) && !/^\\d+([.,]\\d+)?$/.test(w));\n return words.join(' ').trim();\n }\n\n /**\n * \"What time is it?\" / \"What's the date/day today?\" (de/en/ru) → a device-independent time or date\n * query, answered directly by the adapter from the host clock (no LLM). The date branch is checked\n * first so \"which day is it\" is not swallowed by the time branch. Matching is stem-tolerant via\n * {@link wordInText}; \"сейчас\"/\"today\"/\"monday\" don't false-match \"час\"/\"day\" (word-boundary guarded).\n */\n private parseTimeQuery(text: string): NluIntent | null {\n const has = (w: string): boolean => wordInText(w, text);\n const qualifier = (): boolean =>\n has('heute') ||\n has('welch') ||\n has('what') ||\n has('which') ||\n has('today') ||\n has('сегодня') ||\n has('какой') ||\n has('какое');\n // Date / weekday question.\n const isDate =\n has('datum') ||\n has('wochentag') ||\n has('date') ||\n has('число') ||\n has('дата') ||\n ((has('tag') || has('day') || has('день')) && qualifier());\n if (isDate) {\n return { action: 'dateQuery', confidence: 0.9 };\n }\n // Time-of-day question. \"wie viel Uhr\" needs both words so a bare \"Uhr\" (rare here) doesn't trigger.\n const isTime =\n has('spaet') ||\n has('uhrzeit') ||\n has('time') ||\n has('clock') ||\n has('час') ||\n has('время') ||\n (has('uhr') && has('viel'));\n if (isTime) {\n return { action: 'timeQuery', confidence: 0.9 };\n }\n return null;\n }\n\n /**\n * \"Which windows are open?\" (de/en/ru) → an aggregate query over all window devices. Matches when the\n * text mentions a window word AND an \"open\" word; optionally restricted to a matched room.\n */\n private parseWindowsOpen(text: string): NluIntent | null {\n if (!WINDOW_WORDS.some(w => wordInText(w, text)) || !OPEN_WORDS.some(w => wordInText(w, text))) {\n return null;\n }\n const roomName = this.findRoom(text);\n const windows = this.devices.filter(d => WINDOW_TYPES.has(d.type) && (!roomName || d.room === roomName));\n if (!windows.length) {\n return null; // no window devices known → let the LLM handle it\n }\n return {\n action: 'listByState',\n category: 'window',\n stateFilter: 'open',\n devices: windows,\n room: roomName || undefined,\n confidence: 0.9,\n };\n }\n\n /** The room whose significant words all appear in the text (stem-tolerant); longest name wins. */\n private findRoom(text: string): string {\n let best = '';\n let bestLen = 0;\n for (const name of this.rooms) {\n const n = normalize(name);\n const words = n.split(/\\s+/).filter(w => w.length >= 3);\n if (!words.length) {\n continue;\n }\n if (words.every(w => wordInText(w, text)) && n.length > bestLen) {\n best = name;\n bestLen = n.length;\n }\n }\n return best;\n }\n\n /**\n * Device whose significant name words all appear in the text (stem-tolerant for inflection).\n * Prefers devices in the matched room; the longest matching name wins.\n */\n private findDevice(text: string, roomName: string): NluDevice | null {\n const normRoom = roomName ? normalize(roomName) : '';\n const spaces: NluDevice[][] = [];\n if (roomName) {\n spaces.push(this.devices.filter(d => d.room === roomName));\n }\n spaces.push(this.devices.filter(d => !roomName || d.room !== roomName));\n\n for (const space of spaces) {\n let best: NluDevice | null = null;\n let bestLen = 0;\n for (const dev of space) {\n const nk = normalize(dev.name);\n if (!nk || (normRoom && normRoom.includes(nk))) {\n continue; // skip a device whose name is part of the room name\n }\n const words = nk.split(/\\s+/).filter(w => w.length >= 3);\n if (!words.length) {\n continue;\n }\n if (words.every(w => wordInText(w, text)) && nk.length > bestLen) {\n best = dev;\n bestLen = nk.length;\n }\n }\n if (best) {\n return best;\n }\n }\n return null;\n }\n\n private findAction(tokens: Set<string>): 'on' | 'off' | null {\n for (const t of tokens) {\n if (TURN_OFF.has(t)) {\n return 'off';\n }\n if (TURN_ON.has(t)) {\n return 'on';\n }\n }\n return null;\n }\n\n private findLevel(text: string): number | null {\n const m = text.match(/(\\d+(?:[.,]\\d+)?)\\s*(?:prozent|процент\\w*|%)/u);\n return m ? parseFloat(m[1].replace(',', '.')) : null;\n }\n\n private findColor(text: string): string | null {\n for (const [name, hex] of Object.entries(COLORS)) {\n if (wordInText(name, text)) {\n return hex;\n }\n }\n return null;\n }\n\n /**\n * Pick a control state id. Tries the ordered controlType keys (preferring `preferType`, then any type),\n * then falls back to any usable **non-metering** control — so a query/command never lands on a\n * CONSUMPTION/CURRENT/… measurement. `mustWrite` additionally requires the control to be writable.\n */\n private pickControl(device: NluDevice, order: string[], mustWrite: boolean, preferType?: string): string {\n const types = device.types || {};\n const usable = (key: string): boolean => !!device.controls[key] && (!mustWrite || !!device.writable[key]);\n if (preferType) {\n // 1. an ordered key of the preferred value type (e.g. a boolean switch for on/off).\n for (const key of order) {\n if (usable(key) && types[key] === preferType) {\n return device.controls[key];\n }\n }\n // 2. ANY control of the preferred type (non-metering). Catches a boolean switch stored under a\n // non-standard control key (e.g. `ON_SET`), so on/off never lands on a numeric level/dimmer\n // control when a real boolean switch exists on the device.\n for (const [key, id] of Object.entries(device.controls)) {\n if (usable(key) && types[key] === preferType && !METER_SUFFIX.test(id)) {\n return id;\n }\n }\n }\n // 3. an ordered key of any type.\n for (const key of order) {\n if (usable(key)) {\n return device.controls[key];\n }\n }\n // 4. fallback: any usable non-metering control.\n for (const [key, id] of Object.entries(device.controls)) {\n if (usable(key) && !METER_SUFFIX.test(id)) {\n return id;\n }\n }\n return '';\n }\n\n /** The ioBroker value type ('boolean'|'number'|…) of a device control by its state id, '' if unknown. */\n private valueTypeOf(device: NluDevice, stateId: string): string {\n for (const [key, id] of Object.entries(device.controls)) {\n if (id === stateId) {\n return device.types[key] || '';\n }\n }\n return '';\n }\n\n /**\n * The device's on/off switch control (state id) other than `excludeStateId`, so a level command can also\n * power the device on. Role-based: a writable control with a `switch…` role wins; otherwise any writable\n * boolean, non-metering control; '' if the device has no separate switch.\n */\n private findSwitch(device: NluDevice, excludeStateId: string): string {\n const roles = device.roles || {};\n let fallback = '';\n for (const [key, id] of Object.entries(device.controls)) {\n if (id === excludeStateId || !device.writable[key] || METER_SUFFIX.test(id)) {\n continue;\n }\n const role = roles[key] || '';\n if (/^switch(\\.|$)/i.test(role)) {\n return id; // a role-confirmed switch is the best match\n }\n if (!fallback && device.types[key] === 'boolean') {\n fallback = id; // any writable boolean control as a fallback\n }\n }\n return fallback;\n }\n}\n"]}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Countdown timers / reminders — the model-free, self-contained timer engine (roadmap #2).
|
|
4
|
+
*
|
|
5
|
+
* Unlike the Python assistant "Hannah" (which keeps timers only in SQLite and signals satellites over
|
|
6
|
+
* gRPC/MQTT, creating no ioBroker objects), this manager is deliberately ioBroker-first: it holds the
|
|
7
|
+
* active timers in memory and fires each one with its own `setTimeout`, while `main.ts` mirrors every
|
|
8
|
+
* timer into `assistant.0.timers.*` states so they can be visualised or reacted to from JavaScript.
|
|
9
|
+
*
|
|
10
|
+
* States are written only on change events (add/cancel/fire/restore) — never on a periodic tick. The
|
|
11
|
+
* live countdown is derived from the absolute `fireAt` timestamp by the consumer (vis widget / script),
|
|
12
|
+
* so nothing hammers the states/history every second.
|
|
13
|
+
*
|
|
14
|
+
* The manager itself is transport-agnostic and testable: it schedules, cancels and restores timers and
|
|
15
|
+
* emits callbacks (`onFire`, `onChange`); the adapter owns the ioBroker representation and the spoken
|
|
16
|
+
* announcement.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.TimerManager = void 0;
|
|
20
|
+
exports.formatDuration = formatDuration;
|
|
21
|
+
/** Largest delay `setTimeout` accepts (~24.8 days); longer timers are re-armed in chunks. */
|
|
22
|
+
const MAX_TIMEOUT = 2 ** 31 - 1;
|
|
23
|
+
/**
|
|
24
|
+
* In-memory countdown timer scheduler. Each timer fires from its own `setTimeout` (no polling loop), and
|
|
25
|
+
* the manager never touches any state on a periodic schedule — only on add/cancel/fire/restore.
|
|
26
|
+
*/
|
|
27
|
+
class TimerManager {
|
|
28
|
+
opts;
|
|
29
|
+
timers = new Map();
|
|
30
|
+
handles = new Map();
|
|
31
|
+
seq = 0;
|
|
32
|
+
now;
|
|
33
|
+
constructor(opts) {
|
|
34
|
+
this.opts = opts;
|
|
35
|
+
this.now = opts.now || Date.now;
|
|
36
|
+
}
|
|
37
|
+
/** Add a timer, schedule it and notify. Returns the created {@link TimerInfo}. */
|
|
38
|
+
add(o) {
|
|
39
|
+
const now = this.now();
|
|
40
|
+
const duration = Math.max(1, Math.round(o.duration));
|
|
41
|
+
const info = {
|
|
42
|
+
id: this.genId(now),
|
|
43
|
+
label: (o.label || '').trim(),
|
|
44
|
+
room: (o.room || '').trim(),
|
|
45
|
+
source: o.source || '',
|
|
46
|
+
createdAt: now,
|
|
47
|
+
duration,
|
|
48
|
+
fireAt: now + duration * 1000,
|
|
49
|
+
};
|
|
50
|
+
this.timers.set(info.id, info);
|
|
51
|
+
this.opts.log?.debug(`timer added: ${info.id} "${info.label}" in ${duration}s`);
|
|
52
|
+
this.arm(info);
|
|
53
|
+
this.emitChange();
|
|
54
|
+
return info;
|
|
55
|
+
}
|
|
56
|
+
/** Cancel one timer by id. Returns true if it existed. */
|
|
57
|
+
cancel(id) {
|
|
58
|
+
this.clearHandle(id);
|
|
59
|
+
if (!this.timers.delete(id)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
this.emitChange();
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/** Cancel all active timers (optionally only those in a room). Returns the number cancelled. */
|
|
66
|
+
cancelAll(room) {
|
|
67
|
+
const victims = this.list().filter(t => !room || t.room === room);
|
|
68
|
+
for (const t of victims) {
|
|
69
|
+
this.clearHandle(t.id);
|
|
70
|
+
this.timers.delete(t.id);
|
|
71
|
+
}
|
|
72
|
+
if (victims.length) {
|
|
73
|
+
this.emitChange();
|
|
74
|
+
}
|
|
75
|
+
return victims.length;
|
|
76
|
+
}
|
|
77
|
+
/** The active timers, soonest-to-fire first. */
|
|
78
|
+
list() {
|
|
79
|
+
return [...this.timers.values()].sort((a, b) => a.fireAt - b.fireAt);
|
|
80
|
+
}
|
|
81
|
+
get(id) {
|
|
82
|
+
return this.timers.get(id);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Restore persisted timers after a restart: timers still in the future are rescheduled, ones that
|
|
86
|
+
* already expired while the adapter was down are dropped (announcing them late would be surprising).
|
|
87
|
+
* Returns how many were restored / dropped.
|
|
88
|
+
*/
|
|
89
|
+
restore(list) {
|
|
90
|
+
const now = this.now();
|
|
91
|
+
let restored = 0;
|
|
92
|
+
let dropped = 0;
|
|
93
|
+
for (const t of list) {
|
|
94
|
+
if (!t || typeof t.fireAt !== 'number' || typeof t.id !== 'string') {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (t.fireAt > now) {
|
|
98
|
+
const info = {
|
|
99
|
+
id: t.id,
|
|
100
|
+
label: t.label || '',
|
|
101
|
+
room: t.room || '',
|
|
102
|
+
source: t.source || '',
|
|
103
|
+
createdAt: t.createdAt || now,
|
|
104
|
+
duration: t.duration || Math.round((t.fireAt - now) / 1000),
|
|
105
|
+
fireAt: t.fireAt,
|
|
106
|
+
};
|
|
107
|
+
this.timers.set(info.id, info);
|
|
108
|
+
this.arm(info);
|
|
109
|
+
restored++;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
dropped++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
this.emitChange();
|
|
116
|
+
return { restored, dropped };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Fire every timer whose `fireAt` has passed — public so tests can drive expiry with a mocked clock
|
|
120
|
+
* (production fires each timer from its own `setTimeout`, so this is a no-op there).
|
|
121
|
+
*/
|
|
122
|
+
tick() {
|
|
123
|
+
const now = this.now();
|
|
124
|
+
const due = this.list().filter(t => t.fireAt <= now);
|
|
125
|
+
for (const t of due) {
|
|
126
|
+
this.fire(t.id, false);
|
|
127
|
+
}
|
|
128
|
+
if (due.length) {
|
|
129
|
+
this.emitChange();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Clear all pending timeouts and drop the in-memory timers (no callbacks). Call on unload. */
|
|
133
|
+
dispose() {
|
|
134
|
+
for (const h of this.handles.values()) {
|
|
135
|
+
clearTimeout(h);
|
|
136
|
+
}
|
|
137
|
+
this.handles.clear();
|
|
138
|
+
this.timers.clear();
|
|
139
|
+
}
|
|
140
|
+
/** Fire one timer: remove it, then invoke `onFire`. `emit` controls whether to notify immediately. */
|
|
141
|
+
fire(id, emit) {
|
|
142
|
+
const info = this.timers.get(id);
|
|
143
|
+
if (!info) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.clearHandle(id);
|
|
147
|
+
this.timers.delete(id);
|
|
148
|
+
try {
|
|
149
|
+
this.opts.onFire(info);
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
this.opts.log?.warn(`timer onFire failed: ${e.message}`);
|
|
153
|
+
}
|
|
154
|
+
if (emit) {
|
|
155
|
+
this.emitChange();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Arm the `setTimeout` for a timer (chunked for delays beyond the setTimeout limit). */
|
|
159
|
+
arm(info) {
|
|
160
|
+
if (this.opts.manualTick) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.clearHandle(info.id);
|
|
164
|
+
const delay = Math.max(0, info.fireAt - this.now());
|
|
165
|
+
const handle = setTimeout(() => {
|
|
166
|
+
// Re-arm if this was only a chunk of a very long delay; otherwise fire.
|
|
167
|
+
if (this.timers.has(info.id) && info.fireAt - this.now() > 0) {
|
|
168
|
+
this.arm(info);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
this.fire(info.id, true);
|
|
172
|
+
}
|
|
173
|
+
}, Math.min(delay, MAX_TIMEOUT));
|
|
174
|
+
handle.unref?.(); // don't keep the process alive for a timer
|
|
175
|
+
this.handles.set(info.id, handle);
|
|
176
|
+
}
|
|
177
|
+
clearHandle(id) {
|
|
178
|
+
const h = this.handles.get(id);
|
|
179
|
+
if (h) {
|
|
180
|
+
clearTimeout(h);
|
|
181
|
+
this.handles.delete(id);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
genId(now) {
|
|
185
|
+
return `t${now.toString(36)}${(this.seq++).toString(36)}`;
|
|
186
|
+
}
|
|
187
|
+
emitChange() {
|
|
188
|
+
try {
|
|
189
|
+
this.opts.onChange(this.list());
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
this.opts.log?.warn(`timer onChange failed: ${e.message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
exports.TimerManager = TimerManager;
|
|
197
|
+
/** Simple Russian plural selector (1 → one, 2–4 → few, else many; with the 11–14 exception). */
|
|
198
|
+
function ruPlural(n, one, few, many) {
|
|
199
|
+
const m10 = n % 10;
|
|
200
|
+
const m100 = n % 100;
|
|
201
|
+
if (m10 === 1 && m100 !== 11) {
|
|
202
|
+
return one;
|
|
203
|
+
}
|
|
204
|
+
if (m10 >= 2 && m10 <= 4 && (m100 < 10 || m100 >= 20)) {
|
|
205
|
+
return few;
|
|
206
|
+
}
|
|
207
|
+
return many;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Human-readable, spoken-friendly duration ("1 Stunde 30 Minuten", "5 minutes", "2 минуты"). Seconds are
|
|
211
|
+
* only shown when there is no hour part (so a 90-minute timer reads "1 Stunde 30 Minuten", not with a
|
|
212
|
+
* dangling "0 Sekunden"). Falls back to English for languages other than de/en/ru.
|
|
213
|
+
*/
|
|
214
|
+
function formatDuration(totalSec, lang) {
|
|
215
|
+
const l = lang === 'ru' ? 'ru' : lang === 'de' ? 'de' : 'en';
|
|
216
|
+
let sec = Math.max(0, Math.round(totalSec));
|
|
217
|
+
const h = Math.floor(sec / 3600);
|
|
218
|
+
sec -= h * 3600;
|
|
219
|
+
const m = Math.floor(sec / 60);
|
|
220
|
+
const s = sec - m * 60;
|
|
221
|
+
const parts = [];
|
|
222
|
+
const add = (n, ru, de, en) => {
|
|
223
|
+
if (l === 'ru') {
|
|
224
|
+
parts.push(`${n} ${ruPlural(n, ru[0], ru[1], ru[2])}`);
|
|
225
|
+
}
|
|
226
|
+
else if (l === 'de') {
|
|
227
|
+
parts.push(`${n} ${n === 1 ? de[0] : de[1]}`);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
parts.push(`${n} ${n === 1 ? en[0] : en[1]}`);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
if (h) {
|
|
234
|
+
add(h, ['час', 'часа', 'часов'], ['Stunde', 'Stunden'], ['hour', 'hours']);
|
|
235
|
+
}
|
|
236
|
+
if (m) {
|
|
237
|
+
add(m, ['минута', 'минуты', 'минут'], ['Minute', 'Minuten'], ['minute', 'minutes']);
|
|
238
|
+
}
|
|
239
|
+
// Show seconds when there is no hour part, or when nothing else was added (e.g. a 10-second timer).
|
|
240
|
+
if ((s && !h) || !parts.length) {
|
|
241
|
+
add(s, ['секунда', 'секунды', 'секунд'], ['Sekunde', 'Sekunden'], ['second', 'seconds']);
|
|
242
|
+
}
|
|
243
|
+
return parts.join(' ');
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=timers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timers.js","sourceRoot":"","sources":["../../src/lib/timers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Countdown timers / reminders — the model-free, self-contained timer engine (roadmap #2).\n *\n * Unlike the Python assistant \"Hannah\" (which keeps timers only in SQLite and signals satellites over\n * gRPC/MQTT, creating no ioBroker objects), this manager is deliberately ioBroker-first: it holds the\n * active timers in memory and fires each one with its own `setTimeout`, while `main.ts` mirrors every\n * timer into `assistant.0.timers.*` states so they can be visualised or reacted to from JavaScript.\n *\n * States are written only on change events (add/cancel/fire/restore) — never on a periodic tick. The\n * live countdown is derived from the absolute `fireAt` timestamp by the consumer (vis widget / script),\n * so nothing hammers the states/history every second.\n *\n * The manager itself is transport-agnostic and testable: it schedules, cancels and restores timers and\n * emits callbacks (`onFire`, `onChange`); the adapter owns the ioBroker representation and the spoken\n * announcement.\n */\n\n/** One active countdown timer. Plain data so it round-trips through JSON for persistence. */\nexport interface TimerInfo {\n /** Short unique id (also the `timers.items.<id>` state-object segment). */\n id: string;\n /** Optional label (\"die Nudeln\"); '' when the user only gave a duration. */\n label: string;\n /** Room the command came from / was scoped to ('' when unknown). */\n room: string;\n /** Origin of the request (satellite device name / 'chat' / '') — used to target the announcement. */\n source: string;\n /** Epoch ms the timer was created. */\n createdAt: number;\n /** Original duration in seconds. */\n duration: number;\n /** Epoch ms the timer fires at. */\n fireAt: number;\n}\n\n/** Options for creating a new timer. */\nexport interface AddTimerOptions {\n label?: string;\n room?: string;\n source?: string;\n /** Duration in seconds (> 0). */\n duration: number;\n}\n\nexport interface TimerManagerOptions {\n /** Called once per timer when it expires (already removed from the active list). */\n onFire: (timer: TimerInfo) => void;\n /** Called whenever the active list changes (add/cancel/fire/restore) with the new sorted list. */\n onChange: (timers: TimerInfo[]) => void;\n log?: Pick<ioBroker.Log, 'debug' | 'info' | 'warn'>;\n /** Injectable clock (tests). Defaults to `Date.now`. */\n now?: () => number;\n /** Don't arm real `setTimeout`s — the caller drives {@link TimerManager.tick} (tests). */\n manualTick?: boolean;\n}\n\n/** Largest delay `setTimeout` accepts (~24.8 days); longer timers are re-armed in chunks. */\nconst MAX_TIMEOUT = 2 ** 31 - 1;\n\n/**\n * In-memory countdown timer scheduler. Each timer fires from its own `setTimeout` (no polling loop), and\n * the manager never touches any state on a periodic schedule — only on add/cancel/fire/restore.\n */\nexport class TimerManager {\n private readonly timers = new Map<string, TimerInfo>();\n private readonly handles = new Map<string, ReturnType<typeof setTimeout>>();\n private seq = 0;\n private readonly now: () => number;\n\n constructor(private readonly opts: TimerManagerOptions) {\n this.now = opts.now || Date.now;\n }\n\n /** Add a timer, schedule it and notify. Returns the created {@link TimerInfo}. */\n add(o: AddTimerOptions): TimerInfo {\n const now = this.now();\n const duration = Math.max(1, Math.round(o.duration));\n const info: TimerInfo = {\n id: this.genId(now),\n label: (o.label || '').trim(),\n room: (o.room || '').trim(),\n source: o.source || '',\n createdAt: now,\n duration,\n fireAt: now + duration * 1000,\n };\n this.timers.set(info.id, info);\n this.opts.log?.debug(`timer added: ${info.id} \"${info.label}\" in ${duration}s`);\n this.arm(info);\n this.emitChange();\n return info;\n }\n\n /** Cancel one timer by id. Returns true if it existed. */\n cancel(id: string): boolean {\n this.clearHandle(id);\n if (!this.timers.delete(id)) {\n return false;\n }\n this.emitChange();\n return true;\n }\n\n /** Cancel all active timers (optionally only those in a room). Returns the number cancelled. */\n cancelAll(room?: string): number {\n const victims = this.list().filter(t => !room || t.room === room);\n for (const t of victims) {\n this.clearHandle(t.id);\n this.timers.delete(t.id);\n }\n if (victims.length) {\n this.emitChange();\n }\n return victims.length;\n }\n\n /** The active timers, soonest-to-fire first. */\n list(): TimerInfo[] {\n return [...this.timers.values()].sort((a, b) => a.fireAt - b.fireAt);\n }\n\n get(id: string): TimerInfo | undefined {\n return this.timers.get(id);\n }\n\n /**\n * Restore persisted timers after a restart: timers still in the future are rescheduled, ones that\n * already expired while the adapter was down are dropped (announcing them late would be surprising).\n * Returns how many were restored / dropped.\n */\n restore(list: TimerInfo[]): { restored: number; dropped: number } {\n const now = this.now();\n let restored = 0;\n let dropped = 0;\n for (const t of list) {\n if (!t || typeof t.fireAt !== 'number' || typeof t.id !== 'string') {\n continue;\n }\n if (t.fireAt > now) {\n const info: TimerInfo = {\n id: t.id,\n label: t.label || '',\n room: t.room || '',\n source: t.source || '',\n createdAt: t.createdAt || now,\n duration: t.duration || Math.round((t.fireAt - now) / 1000),\n fireAt: t.fireAt,\n };\n this.timers.set(info.id, info);\n this.arm(info);\n restored++;\n } else {\n dropped++;\n }\n }\n this.emitChange();\n return { restored, dropped };\n }\n\n /**\n * Fire every timer whose `fireAt` has passed — public so tests can drive expiry with a mocked clock\n * (production fires each timer from its own `setTimeout`, so this is a no-op there).\n */\n tick(): void {\n const now = this.now();\n const due = this.list().filter(t => t.fireAt <= now);\n for (const t of due) {\n this.fire(t.id, false);\n }\n if (due.length) {\n this.emitChange();\n }\n }\n\n /** Clear all pending timeouts and drop the in-memory timers (no callbacks). Call on unload. */\n dispose(): void {\n for (const h of this.handles.values()) {\n clearTimeout(h);\n }\n this.handles.clear();\n this.timers.clear();\n }\n\n /** Fire one timer: remove it, then invoke `onFire`. `emit` controls whether to notify immediately. */\n private fire(id: string, emit: boolean): void {\n const info = this.timers.get(id);\n if (!info) {\n return;\n }\n this.clearHandle(id);\n this.timers.delete(id);\n try {\n this.opts.onFire(info);\n } catch (e) {\n this.opts.log?.warn(`timer onFire failed: ${(e as Error).message}`);\n }\n if (emit) {\n this.emitChange();\n }\n }\n\n /** Arm the `setTimeout` for a timer (chunked for delays beyond the setTimeout limit). */\n private arm(info: TimerInfo): void {\n if (this.opts.manualTick) {\n return;\n }\n this.clearHandle(info.id);\n const delay = Math.max(0, info.fireAt - this.now());\n const handle = setTimeout(\n () => {\n // Re-arm if this was only a chunk of a very long delay; otherwise fire.\n if (this.timers.has(info.id) && info.fireAt - this.now() > 0) {\n this.arm(info);\n } else {\n this.fire(info.id, true);\n }\n },\n Math.min(delay, MAX_TIMEOUT),\n );\n (handle as { unref?: () => void }).unref?.(); // don't keep the process alive for a timer\n this.handles.set(info.id, handle);\n }\n\n private clearHandle(id: string): void {\n const h = this.handles.get(id);\n if (h) {\n clearTimeout(h);\n this.handles.delete(id);\n }\n }\n\n private genId(now: number): string {\n return `t${now.toString(36)}${(this.seq++).toString(36)}`;\n }\n\n private emitChange(): void {\n try {\n this.opts.onChange(this.list());\n } catch (e) {\n this.opts.log?.warn(`timer onChange failed: ${(e as Error).message}`);\n }\n }\n}\n\n/** Simple Russian plural selector (1 → one, 2–4 → few, else many; with the 11–14 exception). */\nfunction ruPlural(n: number, one: string, few: string, many: string): string {\n const m10 = n % 10;\n const m100 = n % 100;\n if (m10 === 1 && m100 !== 11) {\n return one;\n }\n if (m10 >= 2 && m10 <= 4 && (m100 < 10 || m100 >= 20)) {\n return few;\n }\n return many;\n}\n\n/**\n * Human-readable, spoken-friendly duration (\"1 Stunde 30 Minuten\", \"5 minutes\", \"2 минуты\"). Seconds are\n * only shown when there is no hour part (so a 90-minute timer reads \"1 Stunde 30 Minuten\", not with a\n * dangling \"0 Sekunden\"). Falls back to English for languages other than de/en/ru.\n */\nexport function formatDuration(totalSec: number, lang: string): string {\n const l = lang === 'ru' ? 'ru' : lang === 'de' ? 'de' : 'en';\n let sec = Math.max(0, Math.round(totalSec));\n const h = Math.floor(sec / 3600);\n sec -= h * 3600;\n const m = Math.floor(sec / 60);\n const s = sec - m * 60;\n\n const parts: string[] = [];\n const add = (n: number, ru: [string, string, string], de: [string, string], en: [string, string]): void => {\n if (l === 'ru') {\n parts.push(`${n} ${ruPlural(n, ru[0], ru[1], ru[2])}`);\n } else if (l === 'de') {\n parts.push(`${n} ${n === 1 ? de[0] : de[1]}`);\n } else {\n parts.push(`${n} ${n === 1 ? en[0] : en[1]}`);\n }\n };\n\n if (h) {\n add(h, ['час', 'часа', 'часов'], ['Stunde', 'Stunden'], ['hour', 'hours']);\n }\n if (m) {\n add(m, ['минута', 'минуты', 'минут'], ['Minute', 'Minuten'], ['minute', 'minutes']);\n }\n // Show seconds when there is no hour part, or when nothing else was added (e.g. a 10-second timer).\n if ((s && !h) || !parts.length) {\n add(s, ['секунда', 'секунды', 'секунд'], ['Sekunde', 'Sekunden'], ['second', 'seconds']);\n }\n return parts.join(' ');\n}\n"]}
|