node-red-contrib-knx-ultimate 6.3.15 → 6.3.16

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.
@@ -37,6 +37,11 @@ Die enthaltene Vorlage **windkh/node-red-contrib-telegrambot** folgt dem Receive
37
37
 
38
38
  Die enthaltene Vorlage **RedBot / node-red-contrib-chatbot (Telegram)** folgt dem gemeinsamen RedBot-Nachrichtenformat. Verbinden Sie `chatbot-telegram-receive` direkt mit KNX AI und Ausgang 3 direkt mit `chatbot-telegram-send`; ein separater Callback-Node ist nicht erforderlich, da RedBot Postbacks von Inline-Schaltflächen in normale Eingangsnachrichten umwandelt. Die Eingangszuordnung liest `transport`, `chatId`, `type`, `content` und die Telegram-Sprache. Die Ausgangszuordnung bewahrt die RedBot-Trackingdaten `originalMessage`, `chat`, `api` und `client` und erzeugt anschließend entweder einen `message`-Payload oder einen `inline-buttons`-Payload mit `postback`-Aktionen zur Bestätigung. RedBot bleibt eine separate optionale Abhängigkeit.
39
39
 
40
+ ### Automatisch erkannte Kamera-Adapter
41
+ Installierte Kamerapakete können KNX AI zur Laufzeit einen Kamera-Adapter bereitstellen. Es gibt weder eine Auswahl noch einen Kamera-Node, der mit KNX AI verbunden werden muss: verfügbare Adapter, Controller und Kameras werden automatisch erkannt und in den Chat-Kontext aufgenommen. `node-red-contrib-unifi-ultimate` ist der erste unterstützte Anbieter; weitere Pakete wie `hikvision-ultimate` können sich über denselben herstellerneutralen Vertrag registrieren.
42
+
43
+ Der Benutzer kann einen aktuellen Snapshot anfordern oder das Vision-Modell nach dem sichtbaren Inhalt fragen. Die Telegram- und RedBot-Vorlagen senden das Bild als natives Foto mit Bildunterschrift. Außerdem lassen sich dauerhafte Benachrichtigungen für Bewegung, das Überqueren einer intelligenten Linie oder das Betreten einer Einbruchs-/Verweilzone erstellen, optional auf erkannte Personen und eine genau benannte Linie oder Zone begrenzt. Diese Regeln werden in derselben Datei `knxai-chat-context.md` gespeichert und nach einem Neustart von Node-RED wiederhergestellt. UniFi-Ereignisse und Snapshot-Anfragen laufen direkt über den erkannten Anbieter; Ausgang 4 von KNX AI und zusätzliche Flow-Verkabelung sind nicht erforderlich.
44
+
40
45
  ## Proaktive Hausintelligenz und begrenztes Gedächtnis
41
46
  Der Unterbereich **Proaktives Zuhause & Gedächtnis** in **Gespräche & Zuhause** aktiviert proaktive Benachrichtigungen auf Wunsch des Benutzers. Aus ETS-Hierarchie, Namen, Rollen und DPTs erstellt der Node ein deterministisches semantisches Modell für Rollläden, Fenster, Türen, Licht, Temperatur, Klima, Anwesenheit und Alarme mit italienischen, englischen, deutschen, französischen, spanischen und chinesischen Begriffen. Der erste proaktive Detektor überwacht nur zuverlässig erkannte Nicht-Befehlszustände von Rollläden/Fenstern/Türen. Nach der konfigurierten Offenzeit und außerhalb der Ruhezeiten gibt Ausgang 3 eine lokalisierte Nachricht mit `msg.knxAi.type = "proactive_notification"` aus. Ausgang 4 wird niemals proaktiv verwendet und KNX wird nicht selbstständig verändert; eine spätere Benutzeranfrage durchläuft weiterhin die normale Validierung und Bestätigung.
42
47
 
@@ -4,6 +4,7 @@
4
4
  "sections": {
5
5
  "groupAssistant": "KI-Assistent",
6
6
  "groupChatHome": "Gespräche & Zuhause",
7
+ "detectedAdapters": "Automatisch erkannte Adapter",
7
8
  "groupKnxAnalysis": "KNX-Verkehrsanalyse",
8
9
  "quickSetup": "Assistent einrichten",
9
10
  "capture": "Bus-Telegrammeingang",
@@ -93,6 +94,12 @@
93
94
  "installOllamaModelFailed": "Failed to install Ollama model",
94
95
  "ollamaInstallSteps": "1) Open the model library and copy the model name (for example llama3.1). 2) Put the name in the Model field and click Install it.",
95
96
  "ollamaStartedAuto": "Ollama server started automatically.",
97
+ "detectedAdaptersLoading": "Installierte Adapter werden erkannt…",
98
+ "detectedAdaptersNone": "Kein automatischer Adapter erkannt.",
99
+ "detectedAdaptersUnavailable": "Die automatische Adaptererkennung ist vorübergehend nicht verfügbar.",
100
+ "detectedAdapterDetected": "Erkannt",
101
+ "detectedAdapterControllers": "Controller",
102
+ "detectedAdapterCameras": "Kameras",
96
103
  "chatAdapterIntro": "Wählen Sie eine Vorlage, um den Code für Ein- und Ausgang einzufügen. Die Liste wird aus der mitgelieferten Chat-Adapter-Datei geladen; der erzeugte Code bleibt bearbeitbar.",
97
104
  "chatAdapterCodeHelp": "Zuordnungen laufen synchron. Geben Sie msg zurück, um fortzufahren, oder keinen Wert, um die Nachricht zu verwerfen. Fehler werden abgefangen und gemeldet, ohne Node-RED anzuhalten.",
98
105
  "homeIntelligenceIntro": "Der Node erstellt ein mehrsprachiges semantisches ETS-Modell und kann den Chat benachrichtigen, wenn ein zuverlässig erkannter Rollladen, ein Fenster oder eine Tür offen bleibt. Er sendet niemals selbstständig einen KNX-Befehl.",
@@ -37,6 +37,11 @@ The included **windkh/node-red-contrib-telegrambot** preset follows the package'
37
37
 
38
38
  The included **RedBot / node-red-contrib-chatbot (Telegram)** preset follows RedBot's common message contract. Connect `chatbot-telegram-receive` directly to KNX AI and output 3 directly to `chatbot-telegram-send`; no separate callback node is needed because RedBot converts inline-button postbacks into normal inbound messages. The input mapping reads `transport`, `chatId`, `type`, `content`, and the Telegram language. The output mapping preserves RedBot's `originalMessage`, `chat`, `api`, and `client` tracking data, then emits either a `message` payload or an `inline-buttons` payload containing `postback` actions for confirmation. RedBot remains a separate optional dependency.
39
39
 
40
+ ### Automatically detected camera adapters
41
+ Installed camera packages can publish a camera adapter to KNX AI at runtime. There is no selector and no camera node to wire to KNX AI: available adapters, controllers and cameras are detected automatically and included in the chat context. `node-red-contrib-unifi-ultimate` is the first supported provider; other packages, such as `hikvision-ultimate`, can register through the same vendor-neutral contract.
42
+
43
+ The user can ask for a current snapshot or ask the vision model what is visible. Telegram and RedBot presets emit the returned image as a native photo with a caption. The user can also create persistent notifications for motion, a smart line crossing or entry into an intrusion/loiter zone, optionally limited to detected people and to an exact named line or zone. These rules are stored in the same `knxai-chat-context.md` file and are restored after Node-RED restarts. UniFi event subscriptions and snapshot requests are made directly through the detected provider; KNX AI output 4 is not involved and no intermediate flow wiring is required.
44
+
40
45
  ## Proactive home intelligence and bounded memory
41
46
  The **Proactive home & memory** subsection inside **Conversations & home** enables opt-in proactive notifications. From ETS hierarchy, names, roles and DPTs, the node builds a deterministic semantic model for covers, windows, doors, lights, temperature, climate, occupancy and alarms using Italian, English, German, French, Spanish and Chinese terms. The first proactive detector watches only reliably recognized non-command cover/window/door states. After the configured open duration and outside quiet hours, output 3 emits a localized message with `msg.knxAi.type = "proactive_notification"`. It never emits output 4 or changes KNX autonomously; a subsequent user request still uses the normal validation and confirmation workflow.
42
47
 
@@ -4,6 +4,7 @@
4
4
  "sections": {
5
5
  "groupAssistant": "AI assistant",
6
6
  "groupChatHome": "Conversations & home",
7
+ "detectedAdapters": "Automatically detected adapters",
7
8
  "groupKnxAnalysis": "KNX traffic analysis",
8
9
  "quickSetup": "Assistant setup",
9
10
  "capture": "Bus telegram input",
@@ -93,6 +94,12 @@
93
94
  "installOllamaModelFailed": "Failed to install Ollama model",
94
95
  "ollamaInstallSteps": "1) Open the model library and copy the model name (for example llama3.1). 2) Put the name in the Model field and click Install it.",
95
96
  "ollamaStartedAuto": "Ollama server started automatically.",
97
+ "detectedAdaptersLoading": "Detecting installed adapters…",
98
+ "detectedAdaptersNone": "No automatic adapter detected.",
99
+ "detectedAdaptersUnavailable": "Automatic adapter detection is temporarily unavailable.",
100
+ "detectedAdapterDetected": "Detected",
101
+ "detectedAdapterControllers": "Controllers",
102
+ "detectedAdapterCameras": "Cameras",
96
103
  "chatAdapterIntro": "Choose a mapping preset to insert its input and output code. The list is loaded from the packaged chat-adapter mappings file; the generated code remains editable.",
97
104
  "chatAdapterCodeHelp": "Mappings run synchronously. Return msg to continue or return no value to discard it. Errors are caught and reported without stopping Node-RED.",
98
105
  "homeIntelligenceIntro": "The node builds a multilingual semantic ETS model and can notify the chat when a reliably recognized cover, window, or door remains open. It never sends a KNX command proactively.",
@@ -37,6 +37,11 @@ El preajuste incluido **windkh/node-red-contrib-telegrambot** sigue el contrato
37
37
 
38
38
  El preajuste incluido **RedBot / node-red-contrib-chatbot (Telegram)** sigue el formato común de mensajes de RedBot. Conecta directamente `chatbot-telegram-receive` a KNX AI y la salida 3 a `chatbot-telegram-send`; no hace falta un nodo callback separado porque RedBot convierte los postbacks de los botones inline en mensajes de entrada normales. El mapeo de entrada lee `transport`, `chatId`, `type`, `content` y el idioma de Telegram. El mapeo de salida conserva los datos de seguimiento RedBot `originalMessage`, `chat`, `api` y `client`, y después emite un payload `message` o un payload `inline-buttons` con acciones `postback` de confirmación. RedBot sigue siendo una dependencia opcional separada.
39
39
 
40
+ ### Adaptadores de cámara detectados automáticamente
41
+ Los paquetes de cámaras instalados pueden publicar en tiempo de ejecución un adaptador para KNX AI. No hay selector ni nodo de cámara que conectar a KNX AI: los adaptadores, controladores y cámaras disponibles se detectan automáticamente y se incorporan al contexto del chat. `node-red-contrib-unifi-ultimate` es el primer proveedor compatible; otros paquetes, como `hikvision-ultimate`, pueden registrarse mediante el mismo contrato independiente del fabricante.
42
+
43
+ El usuario puede pedir una captura actual o preguntar al modelo de visión qué se ve. Los preajustes de Telegram y RedBot envían la imagen como foto nativa con pie. También se pueden crear notificaciones persistentes por movimiento, cruce de una línea inteligente o entrada en una zona de intrusión/merodeo, limitadas opcionalmente a personas detectadas y a una línea o zona concreta por nombre. Estas reglas se guardan en el mismo archivo `knxai-chat-context.md` y se restauran después de reiniciar Node-RED. Las suscripciones a eventos UniFi y las solicitudes de captura se realizan directamente a través del proveedor detectado; no interviene la salida 4 de KNX AI ni hace falta cableado intermedio en el flujo.
44
+
40
45
  ## Inteligencia doméstica proactiva y memoria limitada
41
46
  La subsección **Hogar proactivo y memoria** dentro de **Conversaciones y hogar** activa las notificaciones proactivas de forma opcional. A partir de la jerarquía ETS, nombres, roles y DPT, el nodo crea un modelo semántico determinista para persianas, ventanas, puertas, luces, temperatura, clima, presencia y alarmas usando términos italianos, ingleses, alemanes, franceses, españoles y chinos. El primer detector proactivo vigila únicamente estados que no sean de comando de persianas/ventanas/puertas reconocidos con suficiente fiabilidad. Tras el tiempo abierto configurado y fuera de las horas silenciosas, la salida 3 emite un mensaje localizado con `msg.knxAi.type = "proactive_notification"`. Nunca emite por la salida 4 ni modifica KNX de manera autónoma; una solicitud posterior del usuario sigue pasando por la validación y confirmación normales.
42
47
 
@@ -4,6 +4,7 @@
4
4
  "sections": {
5
5
  "groupAssistant": "Asistente IA",
6
6
  "groupChatHome": "Conversaciones y hogar",
7
+ "detectedAdapters": "Adaptadores detectados automáticamente",
7
8
  "groupKnxAnalysis": "Análisis del tráfico KNX",
8
9
  "quickSetup": "Configurar el asistente",
9
10
  "capture": "Telegramas del bus",
@@ -85,6 +86,12 @@
85
86
  "installOllamaModelFailed": "Failed to install Ollama model",
86
87
  "ollamaInstallSteps": "1) Open the model library and copy the model name (for example llama3.1). 2) Put the name in the Model field and click Install it.",
87
88
  "ollamaStartedAuto": "Ollama server started automatically.",
89
+ "detectedAdaptersLoading": "Detectando adaptadores instalados…",
90
+ "detectedAdaptersNone": "No se detectó ningún adaptador automático.",
91
+ "detectedAdaptersUnavailable": "La detección automática de adaptadores no está disponible temporalmente.",
92
+ "detectedAdapterDetected": "Detectado",
93
+ "detectedAdapterControllers": "Controladores",
94
+ "detectedAdapterCameras": "Cámaras",
88
95
  "chatAdapterIntro": "Elige un preajuste para insertar su código de mapeo de entrada y salida. La lista se carga desde el archivo de adaptadores de chat incluido; el código generado sigue siendo editable.",
89
96
  "chatAdapterCodeHelp": "Los mapeos se ejecutan de forma síncrona. Devuelve msg para continuar o ningún valor para descartarlo. Los errores se capturan y notifican sin detener Node-RED.",
90
97
  "homeIntelligenceIntro": "El nodo crea un modelo ETS semántico multilingüe y puede avisar al chat cuando una persiana, ventana o puerta reconocida con suficiente fiabilidad permanece abierta. Nunca envía por sí solo un comando KNX.",
@@ -37,6 +37,11 @@ Le préréglage inclus **windkh/node-red-contrib-telegrambot** suit le contrat r
37
37
 
38
38
  Le préréglage inclus **RedBot / node-red-contrib-chatbot (Telegram)** suit le format de message commun de RedBot. Connectez directement `chatbot-telegram-receive` à KNX AI et la sortie 3 à `chatbot-telegram-send` ; aucun nœud de callback séparé n’est nécessaire, car RedBot convertit les postbacks des boutons inline en messages entrants ordinaires. Le mappage d’entrée lit `transport`, `chatId`, `type`, `content` et la langue Telegram. Le mappage de sortie conserve les données de suivi RedBot `originalMessage`, `chat`, `api` et `client`, puis émet soit un payload `message`, soit un payload `inline-buttons` avec des actions `postback` de confirmation. RedBot reste une dépendance optionnelle distincte.
39
39
 
40
+ ### Adaptateurs de caméra détectés automatiquement
41
+ Les paquets de caméra installés peuvent publier à l’exécution un adaptateur pour KNX AI. Il n’existe aucun sélecteur ni nœud caméra à relier à KNX AI : les adaptateurs, contrôleurs et caméras disponibles sont détectés automatiquement et ajoutés au contexte du chat. `node-red-contrib-unifi-ultimate` est le premier fournisseur pris en charge ; d’autres paquets, tels que `hikvision-ultimate`, peuvent s’enregistrer avec le même contrat indépendant du fabricant.
42
+
43
+ L’utilisateur peut demander une capture actuelle ou demander au modèle de vision ce qui est visible. Les préréglages Telegram et RedBot envoient l’image comme photo native avec une légende. L’utilisateur peut aussi créer des notifications persistantes pour un mouvement, le franchissement d’une ligne intelligente ou l’entrée dans une zone d’intrusion/de stationnement, avec une limitation facultative aux personnes détectées et à une ligne ou zone nommée précise. Ces règles sont stockées dans le même fichier `knxai-chat-context.md` et restaurées après les redémarrages de Node-RED. Les abonnements aux événements UniFi et les demandes de capture passent directement par le fournisseur détecté ; la sortie 4 de KNX AI et un câblage intermédiaire ne sont pas nécessaires.
44
+
40
45
  ## Intelligence domestique proactive et mémoire limitée
41
46
  La sous-section **Maison proactive et mémoire** de **Conversations et maison** active les notifications proactives sur choix de l’utilisateur. À partir de la hiérarchie ETS, des noms, rôles et DPT, le nœud crée un modèle sémantique déterministe pour les volets, fenêtres, portes, éclairages, températures, climat, présence et alarmes avec des termes italiens, anglais, allemands, français, espagnols et chinois. Le premier détecteur proactif surveille uniquement les états hors commande de volets/fenêtres/portes reconnus avec une fiabilité suffisante. Après la durée d’ouverture configurée et hors heures silencieuses, la sortie 3 émet un message localisé avec `msg.knxAi.type = "proactive_notification"`. Il n’émet jamais sur la sortie 4 et ne modifie jamais KNX de façon autonome ; une demande ultérieure de l’utilisateur passe toujours par la validation et la confirmation normales.
42
47
 
@@ -4,6 +4,7 @@
4
4
  "sections": {
5
5
  "groupAssistant": "Assistant IA",
6
6
  "groupChatHome": "Conversations et maison",
7
+ "detectedAdapters": "Adaptateurs détectés automatiquement",
7
8
  "groupKnxAnalysis": "Analyse du trafic KNX",
8
9
  "quickSetup": "Configurer l'assistant",
9
10
  "capture": "Télégrammes du bus",
@@ -85,6 +86,12 @@
85
86
  "installOllamaModelFailed": "Failed to install Ollama model",
86
87
  "ollamaInstallSteps": "1) Open the model library and copy the model name (for example llama3.1). 2) Put the name in the Model field and click Install it.",
87
88
  "ollamaStartedAuto": "Ollama server started automatically.",
89
+ "detectedAdaptersLoading": "Détection des adaptateurs installés…",
90
+ "detectedAdaptersNone": "Aucun adaptateur automatique détecté.",
91
+ "detectedAdaptersUnavailable": "La détection automatique des adaptateurs est temporairement indisponible.",
92
+ "detectedAdapterDetected": "Détecté",
93
+ "detectedAdapterControllers": "Contrôleurs",
94
+ "detectedAdapterCameras": "Caméras",
88
95
  "chatAdapterIntro": "Choisissez un préréglage pour insérer son code de mappage d’entrée et de sortie. La liste est chargée depuis le fichier d’adaptateurs de chat fourni ; le code généré reste modifiable.",
89
96
  "chatAdapterCodeHelp": "Les mappages s’exécutent de façon synchrone. Renvoyez msg pour continuer ou aucune valeur pour l’écarter. Les erreurs sont interceptées et signalées sans arrêter Node-RED.",
90
97
  "homeIntelligenceIntro": "Le nœud crée un modèle ETS sémantique multilingue et peut avertir le chat lorsqu'un volet, une fenêtre ou une porte reconnu avec suffisamment de fiabilité reste ouvert. Il n'envoie jamais de commande KNX de manière autonome.",
@@ -37,6 +37,11 @@ Il preset incluso **windkh/node-red-contrib-telegrambot** segue il contratto rec
37
37
 
38
38
  Il preset incluso **RedBot / node-red-contrib-chatbot (Telegram)** segue il formato comune dei messaggi RedBot. Collega direttamente `chatbot-telegram-receive` a KNX AI e l'uscita 3 direttamente a `chatbot-telegram-send`; non serve un nodo callback separato perché RedBot converte i postback dei pulsanti inline in normali messaggi in ingresso. La mappatura d'ingresso legge `transport`, `chatId`, `type`, `content` e la lingua Telegram. Quella d'uscita conserva i dati di tracciamento RedBot `originalMessage`, `chat`, `api` e `client`, quindi emette un payload `message` oppure un payload `inline-buttons` con azioni `postback` per la conferma. RedBot resta una dipendenza opzionale separata.
39
39
 
40
+ ### Adapter telecamera rilevati automaticamente
41
+ I pacchetti di telecamere installati possono pubblicare a runtime un adapter per KNX AI. Non esistono selettori né nodi telecamera da collegare a KNX AI: adapter, controller e telecamere disponibili vengono rilevati automaticamente e inseriti nel contesto della chat. `node-red-contrib-unifi-ultimate` è il primo provider supportato; altri pacchetti, come `hikvision-ultimate`, possono registrarsi tramite lo stesso contratto indipendente dal produttore.
42
+
43
+ L'utente può chiedere uno snapshot aggiornato oppure domandare al modello vision che cosa è visibile. I preset Telegram e RedBot inviano l'immagine come foto nativa con didascalia. L'utente può anche creare notifiche persistenti per movimento, attraversamento di una linea intelligente o ingresso in una zona di intrusione/stazionamento, limitandole facoltativamente alle persone rilevate e a una linea o zona nominata esatta. Le regole vengono salvate nello stesso file `knxai-chat-context.md` e ripristinate dopo i riavvii di Node-RED. Le sottoscrizioni agli eventi UniFi e le richieste snapshot avvengono direttamente tramite il provider rilevato: l'uscita 4 di KNX AI non è coinvolta e non servono collegamenti intermedi nel flow.
44
+
40
45
  ## Intelligenza domestica proattiva e memoria limitata
41
46
  La sottosezione **Casa proattiva e memoria** dentro **Conversazioni e casa** abilita le notifiche proattive su scelta dell'utente. Da gerarchia ETS, nomi, ruoli e DPT, il nodo crea un modello semantico deterministico per persiane, finestre, porte, luci, temperatura, clima, presenza e allarmi usando termini italiani, inglesi, tedeschi, francesi, spagnoli e cinesi. Il primo rilevatore proattivo osserva soltanto stati non di comando di persiane/finestre/porte riconosciuti con sufficiente affidabilità. Dopo il tempo di apertura configurato e fuori dalle ore silenziose, l'uscita 3 emette un messaggio localizzato con `msg.knxAi.type = "proactive_notification"`. Non emette mai l'uscita 4 e non modifica autonomamente KNX; un'eventuale richiesta successiva dell'utente passa sempre dalla normale validazione e conferma.
42
47
 
@@ -4,6 +4,7 @@
4
4
  "sections": {
5
5
  "groupAssistant": "Assistente AI",
6
6
  "groupChatHome": "Conversazioni e casa",
7
+ "detectedAdapters": "Adapter rilevati automaticamente",
7
8
  "groupKnxAnalysis": "Analisi traffico KNX",
8
9
  "quickSetup": "Configurazione assistente",
9
10
  "capture": "Telegrammi dal bus",
@@ -93,6 +94,12 @@
93
94
  "installOllamaModelFailed": "Installazione modello Ollama non riuscita",
94
95
  "ollamaInstallSteps": "1) Apri la libreria, scegli un modello e copiane il nome (es. llama3.1). 2) Inserisci il nome nel campo Modello e clicca Installalo.",
95
96
  "ollamaStartedAuto": "Server Ollama avviato automaticamente.",
97
+ "detectedAdaptersLoading": "Rilevamento degli adapter installati…",
98
+ "detectedAdaptersNone": "Nessun adapter automatico rilevato.",
99
+ "detectedAdaptersUnavailable": "Il rilevamento automatico degli adapter non è momentaneamente disponibile.",
100
+ "detectedAdapterDetected": "Rilevato",
101
+ "detectedAdapterControllers": "Controller",
102
+ "detectedAdapterCameras": "Telecamere",
96
103
  "chatAdapterIntro": "Scegli un preset per inserire il codice di mappatura in ingresso e in uscita. La lista viene caricata dal file degli adattatori chat incluso nel pacchetto; il codice generato resta modificabile.",
97
104
  "chatAdapterCodeHelp": "Le mappature sono sincrone. Restituisci msg per continuare oppure nessun valore per scartarlo. Gli errori vengono intercettati e segnalati senza arrestare Node-RED.",
98
105
  "homeIntelligenceIntro": "Il nodo crea un modello ETS semantico multilingue e può avvisare la chat quando una persiana, finestra o porta riconosciuta con sufficiente affidabilità rimane aperta. Non invia mai autonomamente comandi KNX.",
@@ -37,6 +37,11 @@
37
37
 
38
38
  随附的 **RedBot / node-red-contrib-chatbot (Telegram)** 预设遵循 RedBot 的通用消息格式。将 `chatbot-telegram-receive` 直接连接到 KNX AI,并将输出 3 直接连接到 `chatbot-telegram-send`;无需单独的 callback 节点,因为 RedBot 会把内联按钮的 postback 转换成普通入站消息。输入映射读取 `transport`、`chatId`、`type`、`content` 和 Telegram 语言。输出映射保留 RedBot 的 `originalMessage`、`chat`、`api` 和 `client` 跟踪数据,然后发送 `message` payload,或发送带有确认 `postback` 操作的 `inline-buttons` payload。RedBot 仍是独立的可选依赖项。
39
39
 
40
+ ### 自动检测的摄像机适配器
41
+ 已安装的摄像机软件包可以在运行时向 KNX AI 发布适配器。无需选择器,也无需将摄像机节点连接到 KNX AI:可用的适配器、控制器和摄像机会被自动检测并加入聊天上下文。`node-red-contrib-unifi-ultimate` 是首个受支持的提供方;`hikvision-ultimate` 等其他软件包可通过同一套厂商无关协议注册。
42
+
43
+ 用户可以请求当前快照,或询问视觉模型画面中可见的内容。Telegram 和 RedBot 预设会把图像作为带说明文字的原生照片发送。用户还可以为移动、智能越线或进入入侵/徘徊区域创建持久通知,并可按检测到的人员以及指定名称的线或区域进行限制。这些规则保存在同一个 `knxai-chat-context.md` 文件中,并在 Node-RED 重启后恢复。UniFi 事件订阅和快照请求直接通过检测到的提供方完成;不会使用 KNX AI 输出 4,也不需要中间 Flow 连线。
44
+
40
45
  ## 主动家庭智能与有限记忆
41
46
  **对话与家庭**中的**主动家庭与记忆**子部分以可选方式启用主动通知。节点会根据 ETS 层级、名称、角色和 DPT,使用意大利语、英语、德语、法语、西班牙语和中文术语,为卷帘、窗户、门、照明、温度、气候、占用和报警建立确定性的语义模型。首个主动检测器只监视可靠识别且非命令的卷帘/窗户/门状态。超过配置的打开时间且当前不在静默时段时,输出 3 会发送本地化消息,并设置 `msg.knxAi.type = "proactive_notification"`。节点绝不会主动使用输出 4,也不会自行修改 KNX;用户之后提出的任何操作仍须经过正常的校验与确认。
42
47
 
@@ -4,6 +4,7 @@
4
4
  "sections": {
5
5
  "groupAssistant": "AI 助手",
6
6
  "groupChatHome": "对话与家庭",
7
+ "detectedAdapters": "自动检测到的适配器",
7
8
  "groupKnxAnalysis": "KNX 总线流量分析",
8
9
  "quickSetup": "助手配置",
9
10
  "capture": "总线报文输入",
@@ -85,6 +86,12 @@
85
86
  "installOllamaModelFailed": "Failed to install Ollama model",
86
87
  "ollamaInstallSteps": "1) Open the model library and copy the model name (for example llama3.1). 2) Put the name in the Model field and click Install it.",
87
88
  "ollamaStartedAuto": "Ollama server started automatically.",
89
+ "detectedAdaptersLoading": "正在检测已安装的适配器…",
90
+ "detectedAdaptersNone": "未检测到自动适配器。",
91
+ "detectedAdaptersUnavailable": "自动适配器检测暂时不可用。",
92
+ "detectedAdapterDetected": "已检测",
93
+ "detectedAdapterControllers": "控制器",
94
+ "detectedAdapterCameras": "摄像机",
88
95
  "chatAdapterIntro": "选择预设即可插入输入和输出映射代码。列表从随包提供的聊天适配器文件加载;生成的代码仍可编辑。",
89
96
  "chatAdapterCodeHelp": "映射同步运行。返回 msg 以继续,或不返回值以丢弃消息。错误会被捕获并报告,不会停止 Node-RED。",
90
97
  "homeIntelligenceIntro": "节点会创建多语言 ETS 语义模型,并在可靠识别的卷帘、窗户或门长时间保持打开时主动通知聊天。它绝不会自行发送 KNX 命令。",
@@ -0,0 +1,302 @@
1
+ const KNX_AI_CAMERA_REGISTRY_KEY = Symbol.for('node-red.knx-ai.camera-adapters.v1')
2
+ const KNX_AI_CAMERA_IMAGE_MAX_BYTES = 6 * 1024 * 1024
3
+ const KNX_AI_CAMERA_MAX_ACTIONS = 8
4
+
5
+ const clampText = (value, maxChars = 240) => String(value === undefined || value === null ? '' : value)
6
+ .trim()
7
+ .slice(0, Math.max(0, Number(maxChars) || 0))
8
+
9
+ const normalizeSearchText = value => clampText(value, 300)
10
+ .normalize('NFKD')
11
+ .replace(/[\u0300-\u036f]/g, '')
12
+ .toLocaleLowerCase()
13
+ .replace(/[^a-z0-9]+/g, ' ')
14
+ .trim()
15
+
16
+ const uniqueTexts = values => Array.from(new Set((Array.isArray(values) ? values : [])
17
+ .map(value => clampText(value, 240))
18
+ .filter(Boolean)))
19
+
20
+ // Runtime contract for optional camera suites. A package registers one adapter
21
+ // manifest and one provider per configured controller. Providers expose:
22
+ // listCameras({ force }) -> camera[]
23
+ // takeSnapshot({ cameraId, cameraName, highQuality }) -> { data, mediaType, camera }
24
+ // subscribe(listener) -> unsubscribe(), with normalized camera event objects.
25
+ // The global Symbol lets separately installed Node-RED packages share the same
26
+ // in-process registry without either package importing the other one.
27
+ const getKnxAiCameraAdapterRegistry = () => {
28
+ const existing = globalThis[KNX_AI_CAMERA_REGISTRY_KEY]
29
+ if (existing && existing.version === 1 && existing.adapters instanceof Map && existing.providers instanceof Map) return existing
30
+ const registry = {
31
+ version: 1,
32
+ adapters: new Map(),
33
+ providers: new Map(),
34
+ listeners: new Set(),
35
+ registerAdapter (adapter) {
36
+ if (!adapter || !adapter.id) return
37
+ this.adapters.set(String(adapter.id), Object.freeze(Object.assign({}, adapter)))
38
+ this.listeners.forEach(listener => {
39
+ try { listener({ type: 'adapter_registered', adapter: this.adapters.get(String(adapter.id)) }) } catch (error) { /* ignore */ }
40
+ })
41
+ },
42
+ registerProvider (provider) {
43
+ if (!provider || !provider.id) return
44
+ this.providers.set(String(provider.id), provider)
45
+ this.listeners.forEach(listener => {
46
+ try { listener({ type: 'provider_registered', provider }) } catch (error) { /* ignore */ }
47
+ })
48
+ },
49
+ unregisterProvider (providerId) {
50
+ const id = String(providerId || '')
51
+ const provider = this.providers.get(id)
52
+ if (!provider) return
53
+ this.providers.delete(id)
54
+ this.listeners.forEach(listener => {
55
+ try { listener({ type: 'provider_unregistered', provider }) } catch (error) { /* ignore */ }
56
+ })
57
+ },
58
+ subscribe (listener) {
59
+ if (typeof listener !== 'function') return () => {}
60
+ this.listeners.add(listener)
61
+ return () => this.listeners.delete(listener)
62
+ }
63
+ }
64
+ globalThis[KNX_AI_CAMERA_REGISTRY_KEY] = registry
65
+ return registry
66
+ }
67
+
68
+ const normalizeKnxAiCameraRegistration = (value = {}) => {
69
+ const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {}
70
+ const id = clampText(source.cameraId || source.id, 160)
71
+ const name = clampText(source.cameraName || source.name || id, 240)
72
+ const state = clampText(source.state, 80).toUpperCase()
73
+ const online = typeof source.online === 'boolean'
74
+ ? source.online
75
+ : state
76
+ ? ['CONNECTED', 'ONLINE'].includes(state)
77
+ : null
78
+ if (!id && !name) return null
79
+ return {
80
+ id,
81
+ name,
82
+ aliases: uniqueTexts([name, id].concat(Array.isArray(source.aliases) ? source.aliases : [])),
83
+ source: clampText(source.source || 'camera-adapter', 80),
84
+ adapterId: clampText(source.adapterId, 120),
85
+ adapterTitle: clampText(source.adapterTitle, 240),
86
+ providerId: clampText(source.providerId, 200),
87
+ controllerId: clampText(source.controllerId, 160),
88
+ controllerName: clampText(source.controllerName, 240),
89
+ nativeCameraId: clampText(source.nativeCameraId, 160),
90
+ state,
91
+ online,
92
+ objectTypes: uniqueTexts(source.objectTypes).map(normalizeCameraObjectType).filter(Boolean).slice(0, 24),
93
+ lines: (Array.isArray(source.lines) ? source.lines : []).slice(0, 80),
94
+ zones: (Array.isArray(source.zones) ? source.zones : []).slice(0, 80),
95
+ lastSeenAt: clampText(source.lastSeenAt || new Date().toISOString(), 64)
96
+ }
97
+ }
98
+
99
+ const resolveKnxAiCamera = ({ target, cameras } = {}) => {
100
+ const requested = normalizeSearchText(target)
101
+ const list = (Array.isArray(cameras) ? cameras : [])
102
+ .map(normalizeKnxAiCameraRegistration)
103
+ .filter(Boolean)
104
+ if (!requested) return { camera: null, ambiguous: false }
105
+
106
+ const exact = list.filter(camera => [camera.id, camera.name].concat(camera.aliases)
107
+ .some(alias => normalizeSearchText(alias) === requested))
108
+ if (exact.length === 1) return { camera: exact[0], ambiguous: false }
109
+ if (exact.length > 1) return { camera: null, ambiguous: true }
110
+
111
+ const partial = list.filter(camera => [camera.id, camera.name].concat(camera.aliases)
112
+ .some(alias => {
113
+ const normalizedAlias = normalizeSearchText(alias)
114
+ return normalizedAlias && (normalizedAlias.includes(requested) || requested.includes(normalizedAlias))
115
+ }))
116
+ if (partial.length === 1) return { camera: partial[0], ambiguous: false }
117
+ return { camera: null, ambiguous: partial.length > 1 }
118
+ }
119
+
120
+ const normalizeCameraEventType = value => {
121
+ const raw = normalizeSearchText(value).replace(/\s+/g, '')
122
+ if (['smartdetect', 'objectdetect', 'objectdetection', 'smartdetection'].includes(raw)) return 'smartDetect'
123
+ if (['smartdetectline', 'line', 'linecrossing', 'crossline'].includes(raw)) return 'smartDetectLine'
124
+ if (['smartdetectzone', 'zone', 'intrusionzone', 'intrusion'].includes(raw)) return 'smartDetectZone'
125
+ if (['smartdetectloiterzone', 'loiter', 'loiterzone'].includes(raw)) return 'smartDetectLoiterZone'
126
+ if (['motion', 'movement'].includes(raw)) return 'motion'
127
+ if (['ring', 'doorbell'].includes(raw)) return 'ring'
128
+ if (['smartaudiodetect', 'audio'].includes(raw)) return 'smartAudioDetect'
129
+ return clampText(value, 80)
130
+ }
131
+
132
+ const normalizeCameraObjectType = value => {
133
+ const raw = normalizeSearchText(value).replace(/\s+/g, '')
134
+ if (['person', 'people', 'persona', 'persone', 'human', 'someone', 'qualcuno'].includes(raw)) return 'person'
135
+ if (['animal', 'animale', 'animali', 'pet', 'pets', 'dog', 'cat', 'cane', 'gatto'].includes(raw)) return 'animal'
136
+ if (['vehicle', 'vehicles', 'veicolo', 'veicoli', 'car', 'auto', 'automobile'].includes(raw)) return 'vehicle'
137
+ if (['face', 'volto', 'faccia'].includes(raw)) return 'face'
138
+ if (['licenseplate', 'numberplate', 'targa'].includes(raw)) return 'licenseplate'
139
+ if (['package', 'parcel', 'pacco'].includes(raw)) return 'package'
140
+ return normalizeSearchText(value).replace(/\s+/g, '')
141
+ }
142
+
143
+ const normalizeCameraActionType = value => {
144
+ const raw = normalizeSearchText(value).replace(/\s+/g, '_')
145
+ if (['snapshot', 'get_snapshot', 'send_snapshot', 'camera_snapshot'].includes(raw)) return 'snapshot'
146
+ if (['analyze', 'analyse', 'analyze_snapshot', 'describe_snapshot', 'vision'].includes(raw)) return 'analyze'
147
+ if (['watch', 'subscribe', 'notify', 'create_watch'].includes(raw)) return 'watch'
148
+ if (['unwatch', 'unsubscribe', 'stop', 'remove_watch', 'delete_watch'].includes(raw)) return 'unwatch'
149
+ if (['list', 'list_watches', 'show_watches'].includes(raw)) return 'list_watches'
150
+ return ''
151
+ }
152
+
153
+ const normalizeKnxAiCameraAction = (value, cameras = []) => {
154
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null
155
+ const type = normalizeCameraActionType(value.type || value.action || value.operation)
156
+ if (!type) return null
157
+ const target = clampText(value.cameraId || value.cameraName || value.camera || value.target, 240)
158
+ const resolved = resolveKnxAiCamera({ target, cameras })
159
+ const camera = resolved.camera
160
+ let eventType = normalizeCameraEventType(value.eventType || value.event)
161
+ const requestedScope = clampText(value.scopeId || value.scopeName || value.zoneId || value.lineId || value.zone || value.line, 240)
162
+ const availableScopes = camera
163
+ ? eventType === 'smartDetectLine'
164
+ ? camera.lines
165
+ : ['smartDetectZone', 'smartDetectLoiterZone'].includes(eventType)
166
+ ? camera.zones
167
+ : []
168
+ : []
169
+ const normalizedScopeTarget = normalizeSearchText(requestedScope)
170
+ const matchingScopes = normalizedScopeTarget
171
+ ? (Array.isArray(availableScopes) ? availableScopes : []).filter(scope => [scope && scope.id, scope && scope.name]
172
+ .some(candidate => normalizeSearchText(candidate) === normalizedScopeTarget))
173
+ : []
174
+ const resolvedScope = matchingScopes.length === 1 ? matchingScopes[0] : null
175
+ const objectTypes = uniqueTexts(Array.isArray(value.objectTypes)
176
+ ? value.objectTypes
177
+ : [value.objectType || value.smartDetectType]).map(normalizeCameraObjectType).filter(Boolean).slice(0, 12)
178
+ // A plain motion event has no classified object metadata. Convert requests
179
+ // such as "motion caused by a person/animal" to the generic smart-detection
180
+ // family so they can match Protect zone, line, or loiter detections.
181
+ if (type === 'watch' && eventType === 'motion' && objectTypes.length > 0) eventType = 'smartDetect'
182
+ return {
183
+ type,
184
+ cameraId: camera ? camera.id : clampText(value.cameraId, 160),
185
+ cameraName: camera ? camera.name : clampText(value.cameraName || value.camera || value.target, 240),
186
+ unresolvedTarget: camera ? '' : target,
187
+ unresolved: Boolean(target && !camera),
188
+ ambiguous: resolved.ambiguous,
189
+ eventType,
190
+ scopeId: resolvedScope ? clampText(resolvedScope.id, 160) : clampText(value.scopeId || value.zoneId || value.lineId, 160),
191
+ scopeName: resolvedScope ? clampText(resolvedScope.name || resolvedScope.id, 240) : clampText(value.scopeName || value.zone || value.line, 240),
192
+ unresolvedScope: Boolean(requestedScope && camera && !resolvedScope),
193
+ ambiguousScope: matchingScopes.length > 1,
194
+ objectTypes,
195
+ cooldownSeconds: Math.max(10, Math.min(86400, Number(value.cooldownSeconds) || 60)),
196
+ sendSnapshot: value.sendSnapshot !== false,
197
+ reason: clampText(value.reason, 500)
198
+ }
199
+ }
200
+
201
+ const normalizeKnxAiCameraActions = ({ actions, cameras } = {}) => (Array.isArray(actions) ? actions : [])
202
+ .slice(0, KNX_AI_CAMERA_MAX_ACTIONS)
203
+ .map(action => normalizeKnxAiCameraAction(action, cameras))
204
+ .filter(Boolean)
205
+
206
+ const normalizeKnxAiCameraEvent = value => {
207
+ const meta = value && typeof value === 'object' && !Array.isArray(value) && value.eventType
208
+ ? value
209
+ : null
210
+ if (!meta) return null
211
+ return {
212
+ cameraId: clampText(meta.cameraId, 160),
213
+ cameraName: clampText(meta.cameraName, 240),
214
+ eventType: normalizeCameraEventType(meta.eventType),
215
+ scopeId: clampText(meta.scopeId, 160),
216
+ scopeName: clampText(meta.scopeName, 240),
217
+ objectTypes: uniqueTexts(Array.isArray(meta.objectTypes) ? meta.objectTypes : []).map(normalizeSearchText).filter(Boolean),
218
+ eventId: clampText(meta.eventId, 160),
219
+ active: meta.active !== false,
220
+ at: clampText(meta.at || new Date().toISOString(), 64),
221
+ raw: meta.raw
222
+ }
223
+ }
224
+
225
+ const cameraWatchMatchesEvent = (watch, event) => {
226
+ if (!watch || !event) return false
227
+ const watchCameraId = clampText(watch.cameraId, 160)
228
+ const eventCameraId = clampText(event.cameraId, 160)
229
+ if (watchCameraId && eventCameraId && watchCameraId !== eventCameraId) return false
230
+ if ((!watchCameraId || !eventCameraId) && normalizeSearchText(watch.cameraName) !== normalizeSearchText(event.cameraName)) return false
231
+ const wantedObjects = (Array.isArray(watch.objectTypes) ? watch.objectTypes : []).map(normalizeCameraObjectType).filter(Boolean)
232
+ const watchEventType = normalizeCameraEventType(watch.eventType)
233
+ const eventType = normalizeCameraEventType(event.eventType)
234
+ const genericSmartDetection = watchEventType === 'smartDetect'
235
+ || (watchEventType === 'motion' && wantedObjects.length > 0)
236
+ if (genericSmartDetection) {
237
+ if (!['smartDetectZone', 'smartDetectLine', 'smartDetectLoiterZone'].includes(eventType)) return false
238
+ } else if (watchEventType && watchEventType !== eventType) return false
239
+ const watchScope = normalizeSearchText(watch.scopeId || watch.scopeName)
240
+ const eventScopeCandidates = [event.scopeId, event.scopeName].map(normalizeSearchText).filter(Boolean)
241
+ if (watchScope && !eventScopeCandidates.some(candidate => candidate === watchScope || candidate.includes(watchScope) || watchScope.includes(candidate))) return false
242
+ if (wantedObjects.length) {
243
+ const detectedObjects = (Array.isArray(event.objectTypes) ? event.objectTypes : []).map(normalizeCameraObjectType).filter(Boolean)
244
+ if (!detectedObjects.some(type => wantedObjects.includes(type))) return false
245
+ }
246
+ return true
247
+ }
248
+
249
+ const normalizeKnxAiCameraImage = ({ data, mediaType } = {}) => {
250
+ if (!Buffer.isBuffer(data)) throw new Error('The camera snapshot is not binary image data')
251
+ if (data.length === 0) throw new Error('The camera snapshot is empty')
252
+ if (data.length > KNX_AI_CAMERA_IMAGE_MAX_BYTES) throw new Error(`The camera snapshot exceeds ${KNX_AI_CAMERA_IMAGE_MAX_BYTES} bytes`)
253
+ const type = clampText(mediaType || 'image/jpeg', 80).toLocaleLowerCase().split(';')[0]
254
+ if (!['image/jpeg', 'image/png', 'image/webp', 'image/gif'].includes(type)) throw new Error(`Unsupported camera image type: ${type}`)
255
+ return { data, mediaType: type, bytes: data.length }
256
+ }
257
+
258
+ const buildKnxAiCameraNotificationText = ({ language, event } = {}) => {
259
+ const lang = clampText(language || 'en', 8).toLocaleLowerCase()
260
+ const camera = clampText(event && event.cameraName, 240) || clampText(event && event.cameraId, 160) || 'camera'
261
+ const eventType = normalizeCameraEventType(event && event.eventType)
262
+ const scope = clampText(event && (event.scopeName || event.scopeId), 240)
263
+ const objectTypes = uniqueTexts(event && event.objectTypes).join(', ')
264
+ const details = [scope, objectTypes].filter(Boolean).join(' — ')
265
+ const suffix = details ? ` (${details})` : ''
266
+ const labels = {
267
+ it: { smartDetectLine: 'un attraversamento di linea', smartDetectZone: 'un ingresso in zona', smartDetectLoiterZone: 'una permanenza in zona', motion: 'un movimento', ring: 'un suono del campanello', smartAudioDetect: 'un evento audio intelligente' },
268
+ de: { smartDetectLine: 'eine Linienüberquerung', smartDetectZone: 'einen Zoneneintritt', smartDetectLoiterZone: 'einen längeren Zonenaufenthalt', motion: 'eine Bewegung', ring: 'ein Klingeln', smartAudioDetect: 'ein intelligentes Audioereignis' },
269
+ fr: { smartDetectLine: 'un franchissement de ligne', smartDetectZone: 'une entrée dans une zone', smartDetectLoiterZone: 'une présence prolongée dans une zone', motion: 'un mouvement', ring: 'une sonnerie', smartAudioDetect: 'un événement audio intelligent' },
270
+ es: { smartDetectLine: 'un cruce de línea', smartDetectZone: 'una entrada en zona', smartDetectLoiterZone: 'una permanencia en zona', motion: 'un movimiento', ring: 'una llamada al timbre', smartAudioDetect: 'un evento de audio inteligente' },
271
+ zh: { smartDetectLine: '越线事件', smartDetectZone: '进入区域事件', smartDetectLoiterZone: '区域徘徊事件', motion: '移动事件', ring: '门铃事件', smartAudioDetect: '智能音频事件' },
272
+ en: { smartDetectLine: 'a line crossing', smartDetectZone: 'a zone entry', smartDetectLoiterZone: 'loitering in a zone', motion: 'motion', ring: 'a doorbell ring', smartAudioDetect: 'a smart audio event' }
273
+ }
274
+ const label = (labels[lang] || labels.en)[eventType] || (lang === 'zh' ? '摄像机事件' : lang === 'it' ? 'un evento' : lang === 'de' ? 'ein Ereignis' : lang === 'fr' ? 'un événement' : lang === 'es' ? 'un evento' : 'an event')
275
+ const copy = {
276
+ it: `La telecamera ${camera} ha rilevato ${label}${suffix}.`,
277
+ de: `Die Kamera ${camera} hat ${label} erkannt${suffix}.`,
278
+ fr: `La caméra ${camera} a détecté ${label}${suffix}.`,
279
+ es: `La cámara ${camera} ha detectado ${label}${suffix}.`,
280
+ zh: `摄像机 ${camera} 检测到${label}${suffix}。`,
281
+ en: `Camera ${camera} detected ${label}${suffix}.`
282
+ }
283
+ return copy[lang] || copy.en
284
+ }
285
+
286
+ module.exports = {
287
+ KNX_AI_CAMERA_IMAGE_MAX_BYTES,
288
+ KNX_AI_CAMERA_MAX_ACTIONS,
289
+ KNX_AI_CAMERA_REGISTRY_KEY,
290
+ buildKnxAiCameraNotificationText,
291
+ cameraWatchMatchesEvent,
292
+ getKnxAiCameraAdapterRegistry,
293
+ normalizeCameraEventType,
294
+ normalizeCameraObjectType,
295
+ normalizeKnxAiCameraAction,
296
+ normalizeKnxAiCameraActions,
297
+ normalizeKnxAiCameraEvent,
298
+ normalizeKnxAiCameraImage,
299
+ normalizeKnxAiCameraRegistration,
300
+ normalizeSearchText,
301
+ resolveKnxAiCamera
302
+ }