node-red-contrib-knx-ultimate 6.0.10 → 6.0.12
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/CHANGELOG.md +10 -0
- package/nodes/knxUltimateHueLight.html +1 -5
- package/nodes/knxUltimateMatterBridge.html +67 -2
- package/nodes/knxUltimateMatterControllerDevice.html +329 -44
- package/nodes/locales/de/knxUltimateMatterBridge.html +0 -2
- package/nodes/locales/de/knxUltimateMatterControllerDevice.html +10 -2
- package/nodes/locales/de/knxUltimateMatterControllerDevice.json +17 -1
- package/nodes/locales/en/knxUltimateMatterBridge.html +0 -2
- package/nodes/locales/en/knxUltimateMatterControllerDevice.html +10 -2
- package/nodes/locales/en/knxUltimateMatterControllerDevice.json +17 -1
- package/nodes/locales/es/knxUltimateMatterBridge.html +0 -2
- package/nodes/locales/es/knxUltimateMatterControllerDevice.html +10 -2
- package/nodes/locales/es/knxUltimateMatterControllerDevice.json +17 -1
- package/nodes/locales/fr/knxUltimateMatterBridge.html +0 -2
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +10 -2
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.json +17 -1
- package/nodes/locales/it/knxUltimateMatterBridge.html +0 -2
- package/nodes/locales/it/knxUltimateMatterControllerDevice.html +10 -2
- package/nodes/locales/it/knxUltimateMatterControllerDevice.json +17 -1
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +0 -2
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +10 -2
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.json +17 -1
- package/nodes/matter-config.js +5 -4
- package/nodes/utils/matterControllerProfiles/doorLock.js +19 -2
- package/nodes/utils/matterControllerProfiles/fan.js +20 -0
- package/nodes/utils/matterControllerProfiles/index.js +11 -1
- package/nodes/utils/matterControllerProfiles/mappedEndpoint.js +53 -15
- package/nodes/utils/matterControllerProfiles/switch.js +14 -0
- package/nodes/utils/matterControllerProfiles/thermostat.js +20 -0
- package/nodes/utils/matterControllerProfiles/universal.js +462 -0
- package/nodes/utils/matterControllerProfiles/windowCovering.js +20 -0
- package/nodes/utils/matterEngine.mjs +12 -3
- package/package.json +3 -3
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
<script type="text/markdown" data-help-name="knxUltimateMatterControllerDevice">
|
|
2
|
-
# Control Matter from KNX (BETA)
|
|
3
|
-
|
|
4
2
|
> This node is in **BETA**: behaviour can change while the Matter implementation is refined.
|
|
5
3
|
|
|
6
4
|
This node controls a commissioned Matter endpoint from KNX. Select the Matter device and the editor detects its capabilities, then shows only the KNX mappings that make sense for that endpoint.
|
|
@@ -23,6 +21,16 @@ It replaces the unpublished per-device Matter controller nodes and keeps the ful
|
|
|
23
21
|
| Update local state from KNX write | Updates the local Matter/KNX cache when a telegram is written on a configured KNX GA. |
|
|
24
22
|
| Node Input/Output PINs | Shows Node-RED input/output pins. Input accepts boolean payloads and Matter-style `msg.payload` or `msg.on.on`; output emits state updates. The selection is preserved when the editor is reopened. |
|
|
25
23
|
|
|
24
|
+
## Universal Mode
|
|
25
|
+
|
|
26
|
+
Select **Universal Mode** in the **Mode** selector to receive every attribute report, cluster event and device initialization from this Matter controller. It always has one input and one output and does not use endpoint mappings. A KNX gateway is optional and is used only by the Battery Monitor alarm/text GAs.
|
|
27
|
+
|
|
28
|
+
The **Universal Battery Monitor** scans all endpoints for Power Source, emits an initial snapshot, and caches complete normalized battery state. The optional KNX GAs publish an aggregate alarm as DPT 1.005 and cycle low-battery device names every 2 seconds as DPT 16.001. Send `{payload:{action:"getAllBatteries"}}` to retrieve the cached inventory.
|
|
29
|
+
|
|
30
|
+
Outputs carry the raw value in `msg.payload` and routing details in `msg.matter` (`source`, `nodeId`, `endpointId`, `clusterId`, attribute/event name). Inputs use `msg.nodeId`, `msg.endpointId`, `msg.clusterId` and either `msg.command` with `msg.args`, or `msg.attribute`; add `msg.value` to write, omit it to read. The selectors may also be nested under `msg.matter`.
|
|
31
|
+
|
|
32
|
+
Examples: `{nodeId:"100",endpointId:1,clusterId:6,command:"on",args:{}}` turns On; `{nodeId:"100",endpointId:1,clusterId:47,attribute:"batPercentRemaining"}` reads battery. Battery reports remain in raw Matter half-percent units; divide by two in a Function node when a percentage is needed.
|
|
33
|
+
|
|
26
34
|
## Behaviour
|
|
27
35
|
|
|
28
36
|
The node keeps a local cache from Matter updates and KNX writes, answers KNX read requests from that cache, and can emit/read values at startup. Only the configured group addresses are listened to, so unrelated KNX traffic is ignored.
|
|
@@ -92,7 +92,23 @@
|
|
|
92
92
|
"matter_controller": "Matter controller",
|
|
93
93
|
"matter_device": "Matter device",
|
|
94
94
|
"device_type": "Device type",
|
|
95
|
-
"matter_section": "Matter device"
|
|
95
|
+
"matter_section": "Matter device",
|
|
96
|
+
"controller_mode": "Mode",
|
|
97
|
+
"single_mode": "Single device",
|
|
98
|
+
"universal_mode": "Universal Mode (all Matter devices)",
|
|
99
|
+
"universal_mode_description": "Listen to and control all Matter devices connected to this controller. Universal Mode works without KNX mappings.",
|
|
100
|
+
"universal_service": "Service",
|
|
101
|
+
"universal_service_battery_monitor": "Universal Battery Monitor",
|
|
102
|
+
"universal_service_all_events": "All Matter events",
|
|
103
|
+
"universal_battery_threshold": "Low battery threshold",
|
|
104
|
+
"universal_low_battery_ga": "Low battery alarm GA",
|
|
105
|
+
"universal_low_battery_text_ga": "Low battery device GA",
|
|
106
|
+
"universal_battery_only_low": "Emit only low batteries",
|
|
107
|
+
"universal_emit_initial": "Emit initial battery snapshot",
|
|
108
|
+
"universal_include_raw": "Include raw Matter event",
|
|
109
|
+
"universal_mode_status": "Universal Matter mode",
|
|
110
|
+
"universal_mode_locate_disabled": "Locate is unavailable in Universal Mode.",
|
|
111
|
+
"universal_controller_name": "Matter Universal Controller"
|
|
96
112
|
},
|
|
97
113
|
"common": {
|
|
98
114
|
"ga": "GA",
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
<script type="text/markdown" data-help-name="knxUltimateMatterControllerDevice">
|
|
2
|
-
# Control Matter from KNX (BETA)
|
|
3
|
-
|
|
4
2
|
> Este nodo está en **BETA**: el comportamiento puede cambiar mientras se mejora la implementación Matter.
|
|
5
3
|
|
|
6
4
|
Este nodo controla desde KNX un endpoint Matter ya emparejado. Selecciona el dispositivo Matter y el editor detecta sus capacidades, mostrando solo las asignaciones KNX coherentes con ese endpoint.
|
|
@@ -23,6 +21,16 @@ Sustituye a los nodos Matter separados no publicados y conserva toda la UI de lu
|
|
|
23
21
|
| Update local state from KNX write | Actualiza la caché local Matter/KNX cuando se escribe un telegrama en una GA KNX configurada. |
|
|
24
22
|
| Node Input/Output PINs | Muestra pines de entrada/salida Node-RED. La entrada acepta payloads booleanos y mensajes Matter en `msg.payload` o `msg.on.on`; la salida emite estados. La selección se conserva al volver a abrir el editor. |
|
|
25
23
|
|
|
24
|
+
## Modo universal
|
|
25
|
+
|
|
26
|
+
Selecciona **Modo universal** para supervisar todos los dispositivos. Siempre tiene una entrada y una salida y no usa mapeos de endpoint. El gateway KNX es opcional y solo se utiliza para las GA de alarma/texto del monitor.
|
|
27
|
+
|
|
28
|
+
El **Monitor universal de baterías** conserva el estado completo. Las GA KNX opcionales publican la alarma global como DPT 1.005 y alternan cada 2 segundos los dispositivos como DPT 16.001. `{payload:{action:"getAllBatteries"}}` devuelve el inventario.
|
|
29
|
+
|
|
30
|
+
La salida contiene el valor raw en `msg.payload` y el enrutamiento en `msg.matter` (`source`, `nodeId`, `endpointId`, `clusterId`, nombre de atributo/evento). La entrada usa `msg.nodeId`, `msg.endpointId`, `msg.clusterId` y `msg.command` con `msg.args`, o `msg.attribute`; añade `msg.value` para escribir y omítelo para leer. Los campos también pueden estar bajo `msg.matter`.
|
|
31
|
+
|
|
32
|
+
Ejemplos: `{nodeId:"100",endpointId:1,clusterId:6,command:"on",args:{}}` enciende; `{nodeId:"100",endpointId:1,clusterId:47,attribute:"batPercentRemaining"}` lee la batería. La batería permanece en unidades Matter raw de medio porcentaje; divídela entre dos en un nodo Function.
|
|
33
|
+
|
|
26
34
|
## Comportamiento
|
|
27
35
|
|
|
28
36
|
El nodo mantiene una caché local con actualizaciones Matter y escrituras KNX, responde lecturas KNX desde esa caché y puede emitir/leer valores al inicio. Solo escucha las direcciones de grupo configuradas, por lo que ignora el tráfico KNX no relacionado.
|
|
@@ -92,7 +92,23 @@
|
|
|
92
92
|
"matter_controller": "Controlador Matter",
|
|
93
93
|
"matter_device": "Dispositivo Matter",
|
|
94
94
|
"device_type": "Tipo de dispositivo",
|
|
95
|
-
"matter_section": "Dispositivo Matter"
|
|
95
|
+
"matter_section": "Dispositivo Matter",
|
|
96
|
+
"controller_mode": "Modo",
|
|
97
|
+
"single_mode": "Dispositivo individual",
|
|
98
|
+
"universal_mode": "Modo universal (todos los dispositivos Matter)",
|
|
99
|
+
"universal_mode_description": "Escucha y controla todos los dispositivos Matter conectados a este controlador. El modo universal funciona sin mapeos KNX.",
|
|
100
|
+
"universal_service": "Servicio",
|
|
101
|
+
"universal_service_battery_monitor": "Monitor universal de baterías",
|
|
102
|
+
"universal_service_all_events": "Todos los eventos Matter",
|
|
103
|
+
"universal_battery_threshold": "Umbral de batería baja",
|
|
104
|
+
"universal_low_battery_ga": "GA alarma de batería",
|
|
105
|
+
"universal_low_battery_text_ga": "GA nombre del dispositivo",
|
|
106
|
+
"universal_battery_only_low": "Emitir solo baterías bajas",
|
|
107
|
+
"universal_emit_initial": "Emitir instantánea inicial de baterías",
|
|
108
|
+
"universal_include_raw": "Incluir evento Matter raw",
|
|
109
|
+
"universal_mode_status": "Modo Matter universal",
|
|
110
|
+
"universal_mode_locate_disabled": "Localizar no está disponible en el modo universal.",
|
|
111
|
+
"universal_controller_name": "Controlador Matter universal"
|
|
96
112
|
},
|
|
97
113
|
"common": {
|
|
98
114
|
"ga": "Georgia",
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
<script type="text/markdown" data-help-name="knxUltimateMatterControllerDevice">
|
|
2
|
-
# Control Matter from KNX (BETA)
|
|
3
|
-
|
|
4
2
|
> Ce nœud est en **BETA** : le comportement peut changer pendant l amélioration de l'implémentation Matter.
|
|
5
3
|
|
|
6
4
|
Ce nœud contrôle depuis KNX un endpoint Matter déjà appairé. Sélectionnez l'appareil Matter et l'éditeur détecte ses capacités, puis affiche uniquement les mappings KNX adaptés à cet endpoint.
|
|
@@ -23,6 +21,16 @@ Il remplace les nœuds Matter séparés non publiés et conserve toute l'UI lumi
|
|
|
23
21
|
| Update local state from KNX write | Met à jour le cache local Matter/KNX lorsqu'un télégramme est écrit sur une GA KNX configurée. |
|
|
24
22
|
| Node Input/Output PINs | Affiche les pins entrée/sortie Node-RED. L'entrée accepte les payloads booléens et les messages Matter dans `msg.payload` ou `msg.on.on`; la sortie émet les états. La sélection est conservée à la réouverture de l'éditeur. |
|
|
25
23
|
|
|
24
|
+
## Mode universel
|
|
25
|
+
|
|
26
|
+
Sélectionnez **Mode universel** pour surveiller tous les appareils. Le nœud possède toujours une entrée et une sortie et n'utilise pas les mappings d'un endpoint. La passerelle KNX est optionnelle et sert uniquement aux GA alarme/texte du moniteur.
|
|
27
|
+
|
|
28
|
+
Le **Moniteur universel de batteries** conserve l'état complet. Les GA KNX optionnelles publient l'alarme globale en DPT 1.005 et font défiler les appareils toutes les 2 secondes en DPT 16.001. `{payload:{action:"getAllBatteries"}}` renvoie l'inventaire.
|
|
29
|
+
|
|
30
|
+
La sortie place la valeur brute dans `msg.payload` et le routage dans `msg.matter` (`source`, `nodeId`, `endpointId`, `clusterId`, nom d'attribut/événement). L'entrée utilise `msg.nodeId`, `msg.endpointId`, `msg.clusterId` et soit `msg.command` avec `msg.args`, soit `msg.attribute`; ajoutez `msg.value` pour écrire, omettez-le pour lire. Ces champs peuvent aussi être placés sous `msg.matter`.
|
|
31
|
+
|
|
32
|
+
Exemples : `{nodeId:"100",endpointId:1,clusterId:6,command:"on",args:{}}` allume ; `{nodeId:"100",endpointId:1,clusterId:47,attribute:"batPercentRemaining"}` lit la batterie. La batterie reste dans l'unité Matter brute par demi-pourcent ; divisez par deux dans un nœud Function.
|
|
33
|
+
|
|
26
34
|
## Comportement
|
|
27
35
|
|
|
28
36
|
Le nœud garde un cache local depuis les mises à jour Matter et les écritures KNX, répond aux lectures KNX depuis ce cache et peut émettre/lire les valeurs au démarrage. Il n'écoute que les adresses de groupe configurées, donc le trafic KNX non lié est ignoré.
|
|
@@ -84,7 +84,23 @@
|
|
|
84
84
|
"matter_controller": "Contrôleur Matter",
|
|
85
85
|
"matter_device": "Appareil Matter",
|
|
86
86
|
"device_type": "Type d'appareil",
|
|
87
|
-
"matter_section": "Appareil Matter"
|
|
87
|
+
"matter_section": "Appareil Matter",
|
|
88
|
+
"controller_mode": "Mode",
|
|
89
|
+
"single_mode": "Appareil unique",
|
|
90
|
+
"universal_mode": "Mode universel (tous les appareils Matter)",
|
|
91
|
+
"universal_mode_description": "Écoute et contrôle tous les appareils Matter connectés à ce contrôleur. Le mode universel fonctionne sans mappages KNX.",
|
|
92
|
+
"universal_service": "Service",
|
|
93
|
+
"universal_service_battery_monitor": "Moniteur universel de batteries",
|
|
94
|
+
"universal_service_all_events": "Tous les événements Matter",
|
|
95
|
+
"universal_battery_threshold": "Seuil de batterie faible",
|
|
96
|
+
"universal_low_battery_ga": "GA alarme batterie",
|
|
97
|
+
"universal_low_battery_text_ga": "GA nom appareil",
|
|
98
|
+
"universal_battery_only_low": "Émettre uniquement les batteries faibles",
|
|
99
|
+
"universal_emit_initial": "Émettre l’instantané initial des batteries",
|
|
100
|
+
"universal_include_raw": "Inclure l’événement Matter brut",
|
|
101
|
+
"universal_mode_status": "Mode Matter universel",
|
|
102
|
+
"universal_mode_locate_disabled": "La fonction Localiser n’est pas disponible en mode universel.",
|
|
103
|
+
"universal_controller_name": "Contrôleur Matter universel"
|
|
88
104
|
},
|
|
89
105
|
"common": {
|
|
90
106
|
"ga": "Géorgie",
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
<script type="text/markdown" data-help-name="knxUltimateMatterControllerDevice">
|
|
2
|
-
# Control Matter from KNX (BETA)
|
|
3
|
-
|
|
4
2
|
> Questo nodo è in **BETA**: il comportamento può cambiare mentre l'implementazione Matter viene rifinita.
|
|
5
3
|
|
|
6
4
|
Questo nodo controlla da KNX un endpoint Matter già abbinato. Seleziona il dispositivo Matter e l'editor rileva le sue capability, mostrando solo le mappature KNX coerenti con quell'endpoint.
|
|
@@ -23,6 +21,16 @@ Sostituisce i nodi Matter separati non pubblicati e mantiene tutta la UI luce qu
|
|
|
23
21
|
| Update local state from KNX write | Aggiorna la cache locale Matter/KNX quando arriva una scrittura su un GA KNX configurato. |
|
|
24
22
|
| Node Input/Output PINs | Mostra i pin input/output Node-RED. L'input accetta payload booleani e messaggi stile Matter in `msg.payload` o `msg.on.on`; l'output emette gli aggiornamenti di stato. La selezione viene mantenuta alla riapertura dell'editor. |
|
|
25
23
|
|
|
24
|
+
## Modalità Universale
|
|
25
|
+
|
|
26
|
+
Seleziona **Modalità Universale** nel selettore **Modalità** per ricevere tutti i report attributo, gli eventi cluster e le inizializzazioni dei dispositivi. Ha sempre un input e un output e non usa le mappature del singolo endpoint. Il gateway KNX è opzionale e serve solo alle GA allarme/testo del Monitor batterie.
|
|
27
|
+
|
|
28
|
+
Il **Monitor batterie universale** cerca Power Source in tutti gli endpoint e conserva lo stato normalizzato completo. Le GA KNX opzionali pubblicano l'allarme complessivo come DPT 1.005 e ciclano ogni 2 secondi i nomi dei device scarichi come DPT 16.001. Invia `{payload:{action:"getAllBatteries"}}` per ottenere l'inventario in cache.
|
|
29
|
+
|
|
30
|
+
L'output contiene il valore raw in `msg.payload` e i dati di instradamento in `msg.matter` (`source`, `nodeId`, `endpointId`, `clusterId`, nome attributo/evento). L'input usa `msg.nodeId`, `msg.endpointId`, `msg.clusterId` e `msg.command` con `msg.args`, oppure `msg.attribute`; aggiungi `msg.value` per scrivere, omettilo per leggere. Gli stessi campi possono stare in `msg.matter`.
|
|
31
|
+
|
|
32
|
+
Esempi: `{nodeId:"100",endpointId:1,clusterId:6,command:"on",args:{}}` accende; `{nodeId:"100",endpointId:1,clusterId:47,attribute:"batPercentRemaining"}` legge la batteria. Il valore batteria resta nell'unità raw Matter a mezzi punti percentuali; dividilo per due in un nodo Function.
|
|
33
|
+
|
|
26
34
|
## Comportamento
|
|
27
35
|
|
|
28
36
|
Il nodo mantiene una cache locale da aggiornamenti Matter e scritture KNX, risponde alle letture KNX dalla cache e può emettere/leggere i valori all'avvio. Ascolta solo gli indirizzi di gruppo configurati, quindi ignora il traffico KNX non pertinente.
|
|
@@ -92,7 +92,23 @@
|
|
|
92
92
|
"matter_controller": "Controller Matter",
|
|
93
93
|
"matter_device": "Dispositivo Matter",
|
|
94
94
|
"device_type": "Tipo dispositivo",
|
|
95
|
-
"matter_section": "Dispositivo Matter"
|
|
95
|
+
"matter_section": "Dispositivo Matter",
|
|
96
|
+
"controller_mode": "Modalità",
|
|
97
|
+
"single_mode": "Dispositivo singolo",
|
|
98
|
+
"universal_mode": "Modalità universale (tutti i dispositivi Matter)",
|
|
99
|
+
"universal_mode_description": "Ascolta e controlla tutti i dispositivi Matter collegati a questo controller. La Modalità Universale funziona senza mappature KNX.",
|
|
100
|
+
"universal_service": "Servizio",
|
|
101
|
+
"universal_service_battery_monitor": "Monitor batterie universale",
|
|
102
|
+
"universal_service_all_events": "Tutti gli eventi Matter",
|
|
103
|
+
"universal_battery_threshold": "Soglia batteria scarica",
|
|
104
|
+
"universal_low_battery_ga": "GA allarme batteria",
|
|
105
|
+
"universal_low_battery_text_ga": "GA nome device scarico",
|
|
106
|
+
"universal_battery_only_low": "Emetti solo batterie scariche",
|
|
107
|
+
"universal_emit_initial": "Emetti snapshot iniziale batterie",
|
|
108
|
+
"universal_include_raw": "Includi evento Matter raw",
|
|
109
|
+
"universal_mode_status": "Modalità Matter universale",
|
|
110
|
+
"universal_mode_locate_disabled": "La funzione Trova non è disponibile in Modalità Universale.",
|
|
111
|
+
"universal_controller_name": "Controller Matter Universale"
|
|
96
112
|
},
|
|
97
113
|
"common": {
|
|
98
114
|
"ga": "GA",
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
<script type="text/markdown" data-help-name="knxUltimateMatterControllerDevice">
|
|
2
|
-
# 从 KNX 控制 Matter (BETA)
|
|
3
|
-
|
|
4
2
|
> 此节点为 **BETA**:Matter 实现继续完善时,行为可能会变化。
|
|
5
3
|
|
|
6
4
|
此节点用于从 KNX 控制已经配对的 Matter endpoint。选择 Matter 设备后,编辑器会检测它的能力,并只显示与该 endpoint 匹配的 KNX 映射。
|
|
@@ -23,6 +21,16 @@
|
|
|
23
21
|
| Update local state from KNX write | 当配置的 KNX GA 收到写入 telegram 时,更新本地 Matter/KNX 缓存。 |
|
|
24
22
|
| Node Input/Output PINs | 显示 Node-RED 输入/输出端口。输入接受布尔 payload,以及 `msg.payload` 或 `msg.on.on` 中的 Matter 风格消息;输出发送状态更新。重新打开编辑器时会保留此选择。 |
|
|
25
23
|
|
|
24
|
+
## 通用模式
|
|
25
|
+
|
|
26
|
+
选择 **通用模式** 可监视所有设备。该节点始终有一个输入和一个输出,不使用单个 endpoint 映射。KNX 网关为可选,仅用于电池监视器的报警/文本组地址。
|
|
27
|
+
|
|
28
|
+
**通用电池监视器** 会缓存完整电池状态。可选 KNX 组地址以 DPT 1.005 输出总报警,并以 DPT 16.001 每 2 秒轮播低电量设备名称。发送 `{payload:{action:"getAllBatteries"}}` 可获取缓存清单。
|
|
29
|
+
|
|
30
|
+
输出将原始值放在 `msg.payload`,并在 `msg.matter` 中提供路由信息(`source`、`nodeId`、`endpointId`、`clusterId`、属性/事件名称)。输入使用 `msg.nodeId`、`msg.endpointId`、`msg.clusterId`,以及带 `msg.args` 的 `msg.command` 或 `msg.attribute`;添加 `msg.value` 表示写入,省略则读取。这些字段也可放在 `msg.matter` 下。
|
|
31
|
+
|
|
32
|
+
示例:`{nodeId:"100",endpointId:1,clusterId:6,command:"on",args:{}}` 执行开启;`{nodeId:"100",endpointId:1,clusterId:47,attribute:"batPercentRemaining"}` 读取电池。电池值保留 Matter 原始的半百分比单位,需要百分比时可在 Function 节点中除以二。
|
|
33
|
+
|
|
26
34
|
## 行为
|
|
27
35
|
|
|
28
36
|
节点根据 Matter 更新和 KNX 写入维护本地缓存,用该缓存响应 KNX 读取请求,并可在启动时读取/发送值。节点只监听已配置的组地址,因此会忽略无关的 KNX 流量。
|
|
@@ -84,7 +84,23 @@
|
|
|
84
84
|
"matter_controller": "Matter 控制器",
|
|
85
85
|
"matter_device": "Matter 设备",
|
|
86
86
|
"device_type": "设备类型",
|
|
87
|
-
"matter_section": "Matter 设备"
|
|
87
|
+
"matter_section": "Matter 设备",
|
|
88
|
+
"controller_mode": "模式",
|
|
89
|
+
"single_mode": "单个设备",
|
|
90
|
+
"universal_mode": "通用模式(所有 Matter 设备)",
|
|
91
|
+
"universal_mode_description": "监听并控制连接到此控制器的所有 Matter 设备。通用模式无需 KNX 映射即可工作。",
|
|
92
|
+
"universal_service": "服务",
|
|
93
|
+
"universal_service_battery_monitor": "通用电池监视器",
|
|
94
|
+
"universal_service_all_events": "所有 Matter 事件",
|
|
95
|
+
"universal_battery_threshold": "低电量阈值",
|
|
96
|
+
"universal_low_battery_ga": "低电量报警组地址",
|
|
97
|
+
"universal_low_battery_text_ga": "低电量设备名称组地址",
|
|
98
|
+
"universal_battery_only_low": "仅输出低电量设备",
|
|
99
|
+
"universal_emit_initial": "输出初始电池快照",
|
|
100
|
+
"universal_include_raw": "包含原始 Matter 事件",
|
|
101
|
+
"universal_mode_status": "Matter 通用模式",
|
|
102
|
+
"universal_mode_locate_disabled": "通用模式下无法使用定位功能。",
|
|
103
|
+
"universal_controller_name": "Matter 通用控制器"
|
|
88
104
|
},
|
|
89
105
|
"common": {
|
|
90
106
|
"ga": "组地址",
|
package/nodes/matter-config.js
CHANGED
|
@@ -52,6 +52,7 @@ module.exports = (RED) => {
|
|
|
52
52
|
node.sysLogger?.warn(`Matter client ${label} error: ${error.message}`)
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
const clientMatchesNode = (client, nodeId) => client?.matterNodeId === '__UNIVERSAL__' || String(client?.matterNodeId) === String(nodeId)
|
|
55
56
|
|
|
56
57
|
node.initMatterConnection = async () => {
|
|
57
58
|
try {
|
|
@@ -90,8 +91,8 @@ module.exports = (RED) => {
|
|
|
90
91
|
// A paired node completed the initial sync: let the clients do their initial read
|
|
91
92
|
node.matterManager.on('nodeInitialized', (_nodeId) => {
|
|
92
93
|
node.nodeClients.forEach((_oClient) => {
|
|
93
|
-
if (_oClient
|
|
94
|
-
safeClientCall(_oClient, () => _oClient.handleMatterNodeInitialized(), 'handleMatterNodeInitialized')
|
|
94
|
+
if (clientMatchesNode(_oClient, _nodeId)) {
|
|
95
|
+
safeClientCall(_oClient, () => _oClient.handleMatterNodeInitialized(_nodeId), 'handleMatterNodeInitialized')
|
|
95
96
|
}
|
|
96
97
|
})
|
|
97
98
|
})
|
|
@@ -99,7 +100,7 @@ module.exports = (RED) => {
|
|
|
99
100
|
// Connection state of a single paired device changed
|
|
100
101
|
node.matterManager.on('nodeState', (_nodeId, _state) => {
|
|
101
102
|
node.nodeClients.forEach((_oClient) => {
|
|
102
|
-
if (_oClient
|
|
103
|
+
if (clientMatchesNode(_oClient, _nodeId)) {
|
|
103
104
|
safeClientCall(_oClient, () => _oClient.setNodeStatusMatter({
|
|
104
105
|
fill: _state === 'connected' ? 'green' : 'yellow',
|
|
105
106
|
shape: _state === 'connected' ? 'dot' : 'ring',
|
|
@@ -107,7 +108,7 @@ module.exports = (RED) => {
|
|
|
107
108
|
payload: ''
|
|
108
109
|
}), 'setNodeStatusMatter')
|
|
109
110
|
if (_state === 'connected') {
|
|
110
|
-
safeClientCall(_oClient, () => _oClient.handleMatterNodeInitialized(), 'handleMatterNodeInitialized')
|
|
111
|
+
safeClientCall(_oClient, () => _oClient.handleMatterNodeInitialized(_nodeId), 'handleMatterNodeInitialized')
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
})
|
|
@@ -52,6 +52,7 @@ const setupDoorLockProfile = (RED, node, config) => {
|
|
|
52
52
|
node.inputRBE = 'false'
|
|
53
53
|
node.passthrough = 'no'
|
|
54
54
|
node.currentLockState = undefined
|
|
55
|
+
node.matterConnectionStatus = ''
|
|
55
56
|
node.knxUltimateAcceptedGAs = [config.GALightSwitch, config.GALightState]
|
|
56
57
|
.map((ga) => String(ga || '').trim())
|
|
57
58
|
.filter((ga) => ga !== '')
|
|
@@ -60,7 +61,15 @@ const setupDoorLockProfile = (RED, node, config) => {
|
|
|
60
61
|
// knxUltimate-config invokes this synchronously from addClient(). Profiles return
|
|
61
62
|
// early from the main light constructor, so they must expose the callback before
|
|
62
63
|
// registering with the shared KNX configuration node.
|
|
63
|
-
node.setNodeStatus = ({ fill = 'grey', shape = 'ring', text = '' } = {}) =>
|
|
64
|
+
node.setNodeStatus = ({ fill = 'grey', shape = 'ring', text = '' } = {}) => {
|
|
65
|
+
if (node.currentLockState !== undefined) {
|
|
66
|
+
const state = lockStateToBoolean(node.currentLockState)
|
|
67
|
+
const knxText = text ? ` | KNX: ${text}` : ''
|
|
68
|
+
setStatus(state === undefined ? 'yellow' : 'blue', state === undefined ? 'ring' : 'dot', `Matter: ${lockStateName(node.currentLockState)}${knxText}`)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
setStatus(fill, shape, text)
|
|
72
|
+
}
|
|
64
73
|
|
|
65
74
|
const commandArgs = () => {
|
|
66
75
|
// Matter represents the remote credential as bytes. Keep the PIN in Node-RED's
|
|
@@ -181,7 +190,15 @@ const setupDoorLockProfile = (RED, node, config) => {
|
|
|
181
190
|
}
|
|
182
191
|
}
|
|
183
192
|
node.setNodeStatusMatter = (status) => {
|
|
184
|
-
if (status
|
|
193
|
+
if (!status || !status.text) return
|
|
194
|
+
node.matterConnectionStatus = status.text
|
|
195
|
+
const isAvailable = /^(connected|ready|controller ready)$/i.test(status.text)
|
|
196
|
+
if (isAvailable && node.currentLockState !== undefined) {
|
|
197
|
+
const state = lockStateToBoolean(node.currentLockState)
|
|
198
|
+
setStatus(state === undefined ? 'yellow' : 'blue', state === undefined ? 'ring' : 'dot', `Matter: ${lockStateName(node.currentLockState)}`)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
setStatus(status.fill || 'grey', status.shape || 'ring', status.text)
|
|
185
202
|
}
|
|
186
203
|
|
|
187
204
|
if (node.serverKNX) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { setupMappedEndpointProfile } = require('./mappedEndpoint')
|
|
4
|
+
|
|
5
|
+
const FAN_CONTROL_CLUSTER_ID = 0x0202
|
|
6
|
+
|
|
7
|
+
const setupFanProfile = (RED, node, config) => setupMappedEndpointProfile(RED, node, config, {
|
|
8
|
+
profileName: 'fan',
|
|
9
|
+
inputClusterId: FAN_CONTROL_CLUSTER_ID,
|
|
10
|
+
eventClusterId: FAN_CONTROL_CLUSTER_ID,
|
|
11
|
+
formatAttributeStatus: (event) => {
|
|
12
|
+
if (['percentCurrent', 'percentSetting'].includes(event.attributeName) && event.value !== null && event.value !== undefined) {
|
|
13
|
+
return `Matter fan: ${Math.round(Number(event.value))}%`
|
|
14
|
+
}
|
|
15
|
+
if (event.attributeName === 'fanMode') return `Matter fan mode: ${event.value}`
|
|
16
|
+
return ''
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
module.exports = { FAN_CONTROL_CLUSTER_ID, setupFanProfile }
|
|
@@ -2,13 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
const { setupDoorLockProfile } = require('./doorLock')
|
|
4
4
|
const { setupMappedEndpointProfile } = require('./mappedEndpoint')
|
|
5
|
+
const { setupWindowCoveringProfile } = require('./windowCovering')
|
|
6
|
+
const { setupThermostatProfile } = require('./thermostat')
|
|
7
|
+
const { setupFanProfile } = require('./fan')
|
|
8
|
+
const { setupSwitchProfile } = require('./switch')
|
|
9
|
+
const { setupUniversalProfile } = require('./universal')
|
|
5
10
|
|
|
6
11
|
// Controller-side Matter device profiles live behind this registry. Keep profile
|
|
7
12
|
// selection capability-driven: the editor records the profile only after inspecting
|
|
8
13
|
// the endpoint's actual device types, clusters, attributes and supported commands.
|
|
9
14
|
const PROFILE_SETUPS = Object.freeze({
|
|
10
15
|
doorLock: setupDoorLockProfile,
|
|
11
|
-
|
|
16
|
+
windowCovering: setupWindowCoveringProfile,
|
|
17
|
+
thermostat: setupThermostatProfile,
|
|
18
|
+
fan: setupFanProfile,
|
|
19
|
+
switch: setupSwitchProfile,
|
|
20
|
+
mapped: setupMappedEndpointProfile,
|
|
21
|
+
universal: setupUniversalProfile
|
|
12
22
|
})
|
|
13
23
|
|
|
14
24
|
const setupMatterControllerProfile = (profile, RED, node, config) => {
|
|
@@ -24,7 +24,7 @@ const parseMappings = (value) => {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const setupMappedEndpointProfile = (RED, node, config) => {
|
|
27
|
+
const setupMappedEndpointProfile = (RED, node, config, options = {}) => {
|
|
28
28
|
node.name = config.name || node.matterDeviceName || 'Control Matter from KNX'
|
|
29
29
|
node.topic = node.name
|
|
30
30
|
node.mappings = parseMappings(config.matterMappings)
|
|
@@ -40,6 +40,7 @@ const setupMappedEndpointProfile = (RED, node, config) => {
|
|
|
40
40
|
node.outputRBE = 'false'
|
|
41
41
|
node.inputRBE = 'false'
|
|
42
42
|
node.passthrough = 'no'
|
|
43
|
+
node.matterProfile = options.profileName || 'mapped'
|
|
43
44
|
const enablePins = config.enableNodePINS === 'yes'
|
|
44
45
|
let lastInitialReadTs = 0
|
|
45
46
|
|
|
@@ -111,7 +112,8 @@ const setupMappedEndpointProfile = (RED, node, config) => {
|
|
|
111
112
|
sendKnx(mapping, matterToKnx(event.clusterId, event.attributeName, event.value))
|
|
112
113
|
})
|
|
113
114
|
if (enablePins) node.send({ topic: `${event.clusterId}.${event.attributeName}`, payload: event.value, matter: event })
|
|
114
|
-
|
|
115
|
+
const profileStatus = typeof options.formatAttributeStatus === 'function' ? options.formatAttributeStatus(event) : ''
|
|
116
|
+
status('blue', 'dot', profileStatus || `Matter→KNX: ${event.attributeName}`)
|
|
115
117
|
} catch (error) {
|
|
116
118
|
RED.log.error(`knxUltimateMatterControllerDevice mapped Matter: ${error.message}`)
|
|
117
119
|
status('red', 'ring', error.message)
|
|
@@ -120,8 +122,11 @@ const setupMappedEndpointProfile = (RED, node, config) => {
|
|
|
120
122
|
node.handleMatterClusterEvent = (event) => {
|
|
121
123
|
// Cluster events have no implicit KNX mapping. They are exposed as raw flow output
|
|
122
124
|
// only when the user explicitly enables the node pins.
|
|
123
|
-
|
|
125
|
+
const clusterAccepted = options.eventClusterId === undefined || Number(event?.clusterId) === Number(options.eventClusterId)
|
|
126
|
+
if (enablePins && clusterAccepted && String(event?.nodeId) === String(node.matterNodeId) && Number(event?.endpointId) === Number(node.matterEndpointId)) {
|
|
124
127
|
node.send({ topic: `${event.clusterId}.${event.eventName}`, payload: event.events, matter: event })
|
|
128
|
+
const profileStatus = typeof options.formatEventStatus === 'function' ? options.formatEventStatus(event) : ''
|
|
129
|
+
if (profileStatus) status('blue', 'dot', profileStatus)
|
|
125
130
|
}
|
|
126
131
|
}
|
|
127
132
|
node.handleMatterNodeInitialized = () => {
|
|
@@ -137,20 +142,53 @@ const setupMappedEndpointProfile = (RED, node, config) => {
|
|
|
137
142
|
if (node.serverKNX) { node.serverKNX.removeClient(node); node.serverKNX.addClient(node) } else status('yellow', 'ring', 'No KNX gateway selected')
|
|
138
143
|
if (node.serverMatter) { node.serverMatter.removeClient(node); node.serverMatter.addClient(node) }
|
|
139
144
|
node.on('input', (msg, send, done) => {
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
const complete = typeof done === 'function' ? done : () => {}
|
|
146
|
+
const output = typeof send === 'function' ? send : node.send.bind(node)
|
|
147
|
+
Promise.resolve().then(async () => {
|
|
142
148
|
const mapping = {
|
|
143
|
-
endpointId:
|
|
144
|
-
clusterId:
|
|
145
|
-
targetKind:
|
|
146
|
-
target:
|
|
149
|
+
endpointId: msg.endpointId ?? node.matterEndpointId,
|
|
150
|
+
clusterId: msg.clusterId,
|
|
151
|
+
targetKind: msg.command !== undefined ? 'command' : 'attribute',
|
|
152
|
+
target: msg.command ?? msg.attribute
|
|
147
153
|
}
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
if (mapping.target === undefined || mapping.target === null || mapping.target === '' || mapping.clusterId === undefined || mapping.clusterId === null) {
|
|
155
|
+
throw new Error('Matter input requires clusterId and command or attribute')
|
|
156
|
+
}
|
|
157
|
+
if (options.inputClusterId !== undefined && Number(mapping.clusterId) !== Number(options.inputClusterId)) {
|
|
158
|
+
throw new Error(`${node.matterProfile} input requires clusterId ${options.inputClusterId}`)
|
|
159
|
+
}
|
|
160
|
+
const isAttributeRead = mapping.targetKind === 'attribute' && msg.value === undefined
|
|
161
|
+
if (isAttributeRead) {
|
|
162
|
+
const currentManager = manager()
|
|
163
|
+
if (!currentManager) throw new Error('Matter controller not ready')
|
|
164
|
+
const value = await currentManager.readAttribute(
|
|
165
|
+
node.matterNodeId,
|
|
166
|
+
mapping.endpointId,
|
|
167
|
+
mapping.clusterId,
|
|
168
|
+
mapping.target,
|
|
169
|
+
msg.requestFromRemote === true
|
|
170
|
+
)
|
|
171
|
+
output({
|
|
172
|
+
...msg,
|
|
173
|
+
payload: value,
|
|
174
|
+
matter: {
|
|
175
|
+
source: 'inputRead',
|
|
176
|
+
nodeId: node.matterNodeId,
|
|
177
|
+
endpointId: mapping.endpointId,
|
|
178
|
+
clusterId: mapping.clusterId,
|
|
179
|
+
attribute: mapping.target
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
status('blue', 'dot', `Matter read: ${mapping.target}`)
|
|
183
|
+
complete()
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
enqueue(mapping, mapping.targetKind === 'command' ? msg.args : msg.value)
|
|
187
|
+
complete()
|
|
188
|
+
}).catch((error) => {
|
|
189
|
+
status('red', 'ring', error.message)
|
|
190
|
+
if (typeof done === 'function') done(error); else node.error(error, msg)
|
|
191
|
+
})
|
|
154
192
|
})
|
|
155
193
|
node.on('close', (done) => {
|
|
156
194
|
try { if (node.serverKNX) node.serverKNX.removeClient(node) } catch (error) { /* empty */ }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { setupMappedEndpointProfile } = require('./mappedEndpoint')
|
|
4
|
+
|
|
5
|
+
const SWITCH_CLUSTER_ID = 0x003b
|
|
6
|
+
|
|
7
|
+
const setupSwitchProfile = (RED, node, config) => setupMappedEndpointProfile(RED, node, config, {
|
|
8
|
+
profileName: 'switch',
|
|
9
|
+
inputClusterId: SWITCH_CLUSTER_ID,
|
|
10
|
+
eventClusterId: SWITCH_CLUSTER_ID,
|
|
11
|
+
formatEventStatus: (event) => `Matter switch: ${event.eventName}`
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
module.exports = { SWITCH_CLUSTER_ID, setupSwitchProfile }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { setupMappedEndpointProfile } = require('./mappedEndpoint')
|
|
4
|
+
|
|
5
|
+
const THERMOSTAT_CLUSTER_ID = 0x0201
|
|
6
|
+
|
|
7
|
+
const setupThermostatProfile = (RED, node, config) => setupMappedEndpointProfile(RED, node, config, {
|
|
8
|
+
profileName: 'thermostat',
|
|
9
|
+
inputClusterId: THERMOSTAT_CLUSTER_ID,
|
|
10
|
+
eventClusterId: THERMOSTAT_CLUSTER_ID,
|
|
11
|
+
formatAttributeStatus: (event) => {
|
|
12
|
+
if (['localTemperature', 'occupiedHeatingSetpoint', 'occupiedCoolingSetpoint'].includes(event.attributeName) && event.value !== null && event.value !== undefined) {
|
|
13
|
+
return `Matter thermostat: ${(Number(event.value) / 100).toFixed(1)} °C`
|
|
14
|
+
}
|
|
15
|
+
if (event.attributeName === 'systemMode') return `Matter thermostat mode: ${event.value}`
|
|
16
|
+
return ''
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
module.exports = { THERMOSTAT_CLUSTER_ID, setupThermostatProfile }
|