node-red-contrib-knx-ultimate 6.3.13 → 6.3.15
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 +11 -0
- package/examples/KNX AI - Telegrambot Direct Chat.json +0 -1
- package/nodes/knxUltimateAI.html +1 -7
- package/nodes/knxUltimateAI.js +265 -29
- package/nodes/knxUltimateMatterBridge.html +19 -2
- package/nodes/knxUltimateMatterBridge.js +18 -3
- package/nodes/locales/de/knxUltimateAI.html +6 -6
- package/nodes/locales/de/knxUltimateAI.json +1 -3
- package/nodes/locales/de/knxUltimateMatterBridge.html +3 -1
- package/nodes/locales/de/knxUltimateMatterBridge.json +5 -1
- package/nodes/locales/en/knxUltimateAI.html +6 -6
- package/nodes/locales/en/knxUltimateAI.json +1 -3
- package/nodes/locales/en/knxUltimateMatterBridge.html +3 -1
- package/nodes/locales/en/knxUltimateMatterBridge.json +5 -1
- package/nodes/locales/es/knxUltimateAI.html +6 -6
- package/nodes/locales/es/knxUltimateAI.json +1 -3
- package/nodes/locales/es/knxUltimateMatterBridge.html +3 -1
- package/nodes/locales/es/knxUltimateMatterBridge.json +5 -1
- package/nodes/locales/fr/knxUltimateAI.html +6 -6
- package/nodes/locales/fr/knxUltimateAI.json +1 -3
- package/nodes/locales/fr/knxUltimateMatterBridge.html +3 -1
- package/nodes/locales/fr/knxUltimateMatterBridge.json +5 -1
- package/nodes/locales/it/knxUltimateAI.html +6 -6
- package/nodes/locales/it/knxUltimateAI.json +1 -3
- package/nodes/locales/it/knxUltimateMatterBridge.html +3 -1
- package/nodes/locales/it/knxUltimateMatterBridge.json +5 -1
- package/nodes/locales/zh-CN/knxUltimateAI.html +6 -6
- package/nodes/locales/zh-CN/knxUltimateAI.json +1 -3
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +3 -1
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +5 -1
- package/nodes/utils/knxAiChatContext.js +277 -0
- package/nodes/utils/knxAiHomeMemory.js +2 -2
- package/nodes/utils/matterBridgeDeviceFactory.mjs +130 -65
- package/package.json +1 -1
- package/resources/KNXAIChatAdapterMappings.js +89 -0
|
@@ -28,6 +28,11 @@ import { AirQualitySensorDevice } from '@matter/main/devices/air-quality-sensor'
|
|
|
28
28
|
import { AirQualityServer } from '@matter/main/behaviors/air-quality'
|
|
29
29
|
import { CarbonDioxideConcentrationMeasurementServer } from '@matter/main/behaviors/carbon-dioxide-concentration-measurement'
|
|
30
30
|
import { FanDevice } from '@matter/main/devices/fan'
|
|
31
|
+
import { FanControlServer } from '@matter/main/behaviors/fan-control'
|
|
32
|
+
import { RoomAirConditionerDevice } from '@matter/main/devices/room-air-conditioner'
|
|
33
|
+
import { DoorLockDevice } from '@matter/main/devices/door-lock'
|
|
34
|
+
import { DoorLockServer } from '@matter/main/behaviors/door-lock'
|
|
35
|
+
import { DoorLock } from '@matter/main/clusters/door-lock'
|
|
31
36
|
import { RoboticVacuumCleanerDevice } from '@matter/main/devices/robotic-vacuum-cleaner'
|
|
32
37
|
import { RvcRunModeServer } from '@matter/main/behaviors/rvc-run-mode'
|
|
33
38
|
import { RvcOperationalStateServer } from '@matter/main/behaviors/rvc-operational-state'
|
|
@@ -53,6 +58,8 @@ const BRIDGE_TYPES = {
|
|
|
53
58
|
// Custom construction (dedicated server behaviors), handled in createBridgedEndpoint
|
|
54
59
|
windowcovering: { device: null, functions: ['position'], commandFunctions: [] },
|
|
55
60
|
thermostat: { device: null, functions: ['currenttemp', 'setpoint', 'coolingsetpoint'], commandFunctions: [] },
|
|
61
|
+
roomairconditioner: { device: null, functions: ['onoff', 'currenttemp', 'setpoint', 'coolingsetpoint', 'fanspeed'], commandFunctions: [] },
|
|
62
|
+
doorlock: { device: null, functions: ['lock'], commandFunctions: [] },
|
|
56
63
|
rgblight: { device: null, functions: ['onoff', 'level', 'rgb'], commandFunctions: ['onoff', 'level'] },
|
|
57
64
|
colortemperaturelight: { device: null, functions: ['onoff', 'level', 'colortemp'], commandFunctions: ['onoff', 'level'] },
|
|
58
65
|
smokecoalarm: { device: null, functions: ['smoke', 'co'], commandFunctions: [] },
|
|
@@ -124,6 +131,8 @@ function knxValueToMatterPatch (def, fn, value) {
|
|
|
124
131
|
}
|
|
125
132
|
case 'onoff':
|
|
126
133
|
return { onOff: { onOff: truthy(value) } }
|
|
134
|
+
case 'lock':
|
|
135
|
+
return { doorLock: { lockState: truthy(value) ? DoorLock.LockState.Locked : DoorLock.LockState.Unlocked } }
|
|
127
136
|
case 'level': {
|
|
128
137
|
const percent = Number(value)
|
|
129
138
|
if (Number.isNaN(percent)) return undefined
|
|
@@ -281,6 +290,76 @@ function createRawColorControlServer (def, onCommand) {
|
|
|
281
290
|
return { RawColorControlServer, attachLevelTracker }
|
|
282
291
|
}
|
|
283
292
|
|
|
293
|
+
function configureThermostatState (initialState, def, fallbackMode) {
|
|
294
|
+
let hasHeating = def.hasHeatingSetpoint === true
|
|
295
|
+
let hasCooling = def.hasCoolingSetpoint === true
|
|
296
|
+
// The Thermostat cluster cannot be composed without at least one operating feature.
|
|
297
|
+
// Preserve the historical heating default for the standalone Thermostat; a Room Air
|
|
298
|
+
// Conditioner with no setpoint GA configured defaults to cooling, its primary use case.
|
|
299
|
+
if (!hasHeating && !hasCooling) {
|
|
300
|
+
hasHeating = fallbackMode === 'heating'
|
|
301
|
+
hasCooling = fallbackMode === 'cooling'
|
|
302
|
+
}
|
|
303
|
+
const features = []
|
|
304
|
+
if (hasHeating) features.push('Heating')
|
|
305
|
+
if (hasCooling) features.push('Cooling')
|
|
306
|
+
initialState.thermostat = {
|
|
307
|
+
localTemperature: null,
|
|
308
|
+
// CoolingOnly=0, HeatingOnly=2, CoolingAndHeating=4 (ControlSequenceOfOperation).
|
|
309
|
+
controlSequenceOfOperation: hasHeating && hasCooling ? 4 : (hasCooling ? 0 : 2),
|
|
310
|
+
// Cool=3, Heat=4 (SystemMode). A dual-mode device starts in Heat.
|
|
311
|
+
systemMode: hasCooling && !hasHeating ? 3 : 4
|
|
312
|
+
}
|
|
313
|
+
if (hasHeating) initialState.thermostat.occupiedHeatingSetpoint = 2000 // 20°C
|
|
314
|
+
if (hasCooling) initialState.thermostat.occupiedCoolingSetpoint = 2400 // 24°C
|
|
315
|
+
return { features, hasHeating, hasCooling }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function attachThermostatCommandHandlers (endpoint, def, onCommand, capabilities) {
|
|
319
|
+
// Absolute thermostat targets are Matter attribute writes, not commands. The offline
|
|
320
|
+
// context marks KNX/flow state synchronization and prevents a feedback write to KNX.
|
|
321
|
+
if (capabilities.hasHeating) {
|
|
322
|
+
endpoint.events.thermostat.occupiedHeatingSetpoint$Changed.on((value, oldValue, context) => {
|
|
323
|
+
try {
|
|
324
|
+
if (context?.offline === true || value === null || value === undefined) return
|
|
325
|
+
onCommand({ deviceId: def.id, fn: 'setpoint', value: Math.round(Number(value)) / 100 })
|
|
326
|
+
} catch (error) { /* empty */ }
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
if (capabilities.hasCooling) {
|
|
330
|
+
endpoint.events.thermostat.occupiedCoolingSetpoint$Changed.on((value, oldValue, context) => {
|
|
331
|
+
try {
|
|
332
|
+
if (context?.offline === true || value === null || value === undefined) return
|
|
333
|
+
onCommand({ deviceId: def.id, fn: 'coolingsetpoint', value: Math.round(Number(value)) / 100 })
|
|
334
|
+
} catch (error) { /* empty */ }
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function configureFanState (initialState) {
|
|
340
|
+
initialState.fanControl = {
|
|
341
|
+
fanMode: 0, // Off
|
|
342
|
+
fanModeSequence: 0, // Off/Low/Med/High (Auto requires the AUT feature)
|
|
343
|
+
percentSetting: 0,
|
|
344
|
+
percentCurrent: 0
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function attachFanCommandHandlers (endpoint, def, onCommand) {
|
|
349
|
+
endpoint.events.fanControl.percentSetting$Changed.on((value, oldValue, context) => {
|
|
350
|
+
try {
|
|
351
|
+
if (context?.offline === true || value === null || value === undefined) return
|
|
352
|
+
onCommand({ deviceId: def.id, fn: 'fanspeed', value: clamp(Math.round(Number(value)), 0, 100) })
|
|
353
|
+
} catch (error) { /* empty */ }
|
|
354
|
+
})
|
|
355
|
+
endpoint.events.fanControl.fanMode$Changed.on((value, oldValue, context) => {
|
|
356
|
+
try {
|
|
357
|
+
if (context?.offline === true) return
|
|
358
|
+
if (Number(value) === 0) onCommand({ deviceId: def.id, fn: 'fanspeed', value: 0 })
|
|
359
|
+
} catch (error) { /* empty */ }
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
284
363
|
/**
|
|
285
364
|
* Creates the bridged Matter endpoint for a virtual device definition.
|
|
286
365
|
* @param {object} def - { id, type, name }
|
|
@@ -353,7 +432,9 @@ function createBridgedEndpoint (def, serialPrefix, onCommand) {
|
|
|
353
432
|
return false
|
|
354
433
|
}
|
|
355
434
|
|
|
356
|
-
const RawOnOffBase = def.type === 'onoffplug'
|
|
435
|
+
const RawOnOffBase = def.type === 'onoffplug'
|
|
436
|
+
? OnOffServer
|
|
437
|
+
: def.type === 'roomairconditioner' ? OnOffServer.with('DeadFrontBehavior') : OnOffServer.with('Lighting')
|
|
357
438
|
class RawOnOffServer extends RawOnOffBase {
|
|
358
439
|
async on () {
|
|
359
440
|
await super.on()
|
|
@@ -505,57 +586,58 @@ function createBridgedEndpoint (def, serialPrefix, onCommand) {
|
|
|
505
586
|
// Heating/Cooling capability is auto-detected from which setpoint GA(s) the KNX node
|
|
506
587
|
// has configured (see knxUltimateMatterBridge.js#getMatterDef). Leaving both flags
|
|
507
588
|
// unset keeps the original heating-only behavior, so existing flows are unaffected.
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (hasHeating) features.push('Heating')
|
|
512
|
-
if (hasCooling) features.push('Cooling')
|
|
513
|
-
if (features.length === 0) features.push('Heating') // never build a thermostat with neither mode
|
|
514
|
-
|
|
515
|
-
initialState.thermostat = {
|
|
516
|
-
localTemperature: null,
|
|
517
|
-
// CoolingOnly=0, HeatingOnly=2, CoolingAndHeating=4 (Matter ControlSequenceOfOperation enum)
|
|
518
|
-
controlSequenceOfOperation: hasHeating && hasCooling ? 4 : (hasCooling ? 0 : 2),
|
|
519
|
-
// Cool=3, Heat=4 (Matter SystemMode enum). Dual-mode starts in Heat; the controller can
|
|
520
|
-
// switch it, same as any physical dual thermostat without an auto/deadband mode.
|
|
521
|
-
systemMode: hasCooling && !hasHeating ? 3 : 4
|
|
522
|
-
}
|
|
523
|
-
if (hasHeating) initialState.thermostat.occupiedHeatingSetpoint = 2000 // 20°C
|
|
524
|
-
if (hasCooling) initialState.thermostat.occupiedCoolingSetpoint = 2400 // 24°C
|
|
525
|
-
|
|
526
|
-
endpoint = new Endpoint(ThermostatDevice.with(ThermostatServer.with(...features), BridgedDeviceBasicInformationServer), initialState)
|
|
527
|
-
|
|
528
|
-
// Setpoint changes from a Matter controller. Unlike OnOff/LevelControl/WindowCovering/
|
|
529
|
-
// ColorControl, an absolute setpoint has no cluster COMMAND to intercept - Thermostat
|
|
530
|
-
// only exposes SetpointRaiseLower (a *relative* command) and SetActivePresetRequest;
|
|
531
|
-
// controllers set an absolute target via a plain attribute write. matter.js only fires
|
|
532
|
-
// $Changing/$Changed when the value actually differs (verified in @matter/node's
|
|
533
|
-
// Datasource#preCommit), so a duplicate setpoint (same value sent twice) cannot be
|
|
534
|
-
// forwarded raw here - there is no lower-level hook to capture it at.
|
|
535
|
-
if (hasHeating) {
|
|
536
|
-
endpoint.events.thermostat.occupiedHeatingSetpoint$Changed.on((value, oldValue, context) => {
|
|
537
|
-
try {
|
|
538
|
-
if (context?.offline === true) return
|
|
539
|
-
if (value === null || value === undefined) return
|
|
540
|
-
onCommand({ deviceId: def.id, fn: 'setpoint', value: Math.round(Number(value)) / 100 }) // centi-°C -> °C
|
|
541
|
-
} catch (error) { /* empty */ }
|
|
542
|
-
})
|
|
543
|
-
}
|
|
544
|
-
if (hasCooling) {
|
|
545
|
-
endpoint.events.thermostat.occupiedCoolingSetpoint$Changed.on((value, oldValue, context) => {
|
|
546
|
-
try {
|
|
547
|
-
if (context?.offline === true) return
|
|
548
|
-
if (value === null || value === undefined) return
|
|
549
|
-
onCommand({ deviceId: def.id, fn: 'coolingsetpoint', value: Math.round(Number(value)) / 100 }) // centi-°C -> °C
|
|
550
|
-
} catch (error) { /* empty */ }
|
|
551
|
-
})
|
|
552
|
-
}
|
|
589
|
+
const thermostat = configureThermostatState(initialState, def, 'heating')
|
|
590
|
+
endpoint = new Endpoint(ThermostatDevice.with(ThermostatServer.with(...thermostat.features), BridgedDeviceBasicInformationServer), initialState)
|
|
591
|
+
attachThermostatCommandHandlers(endpoint, def, onCommand, thermostat)
|
|
553
592
|
// NOTE: SystemMode (Heat/Cool/Off/Auto) changes from a controller are accepted by the
|
|
554
593
|
// cluster but not forwarded to KNX yet: there is no single KNX DPT for HVAC mode that
|
|
555
594
|
// fits every actuator. Track it via the node's optional input PIN if you need it today.
|
|
556
595
|
return endpoint
|
|
557
596
|
}
|
|
558
597
|
|
|
598
|
+
if (def.type === 'roomairconditioner') {
|
|
599
|
+
// Matter Room Air Conditioner (0x0072) requires OnOff with DeadFrontBehavior and a
|
|
600
|
+
// feature-selected Thermostat server. FanControl is optional in the specification,
|
|
601
|
+
// but included here so the former on/off + thermostat + fan trio becomes one endpoint.
|
|
602
|
+
const thermostat = configureThermostatState(initialState, def, 'cooling')
|
|
603
|
+
configureFanState(initialState)
|
|
604
|
+
endpoint = new Endpoint(RoomAirConditionerDevice.with(
|
|
605
|
+
RawOnOffServer,
|
|
606
|
+
ThermostatServer.with(...thermostat.features),
|
|
607
|
+
FanControlServer,
|
|
608
|
+
BridgedDeviceBasicInformationServer
|
|
609
|
+
), initialState)
|
|
610
|
+
attachThermostatCommandHandlers(endpoint, def, onCommand, thermostat)
|
|
611
|
+
attachFanCommandHandlers(endpoint, def, onCommand)
|
|
612
|
+
return endpoint
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (def.type === 'doorlock') {
|
|
616
|
+
initialState.doorLock = {
|
|
617
|
+
lockState: DoorLock.LockState.Unlocked,
|
|
618
|
+
lockType: DoorLock.LockType.Other,
|
|
619
|
+
actuatorEnabled: true,
|
|
620
|
+
operatingMode: DoorLock.OperatingMode.Normal,
|
|
621
|
+
// matter.js currently defaults these constrained base attributes to 0, which is
|
|
622
|
+
// outside their Matter-defined 1..255 range and prevents endpoint initialization.
|
|
623
|
+
wrongCodeEntryLimit: 3,
|
|
624
|
+
userCodeTemporaryDisableTime: 10
|
|
625
|
+
}
|
|
626
|
+
class KnxDoorLockServer extends DoorLockServer {
|
|
627
|
+
async lockDoor (request) {
|
|
628
|
+
await super.lockDoor(request)
|
|
629
|
+
onCommand({ deviceId: def.id, fn: 'lock', value: true, matterCommand: { cluster: 'DoorLock', command: 'lockDoor' } })
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async unlockDoor (request) {
|
|
633
|
+
await super.unlockDoor(request)
|
|
634
|
+
onCommand({ deviceId: def.id, fn: 'lock', value: false, matterCommand: { cluster: 'DoorLock', command: 'unlockDoor' } })
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
endpoint = new Endpoint(DoorLockDevice.with(KnxDoorLockServer, BridgedDeviceBasicInformationServer), initialState)
|
|
638
|
+
return endpoint
|
|
639
|
+
}
|
|
640
|
+
|
|
559
641
|
if (def.type === 'occupancysensor') {
|
|
560
642
|
// The OccupancySensing cluster is not added by default: the sensor type must be chosen (we expose it as PIR)
|
|
561
643
|
endpoint = new Endpoint(OccupancySensorDevice.with(OccupancySensingServer.with('PassiveInfrared'), BridgedDeviceBasicInformationServer), initialState)
|
|
@@ -620,30 +702,13 @@ function createBridgedEndpoint (def, serialPrefix, onCommand) {
|
|
|
620
702
|
BridgedDeviceBasicInformationServer
|
|
621
703
|
), initialState)
|
|
622
704
|
} else if (def.type === 'fan') {
|
|
623
|
-
initialState
|
|
624
|
-
fanMode: 0, // Off
|
|
625
|
-
fanModeSequence: 0, // Off/Low/Med/High (Auto requires the AUT feature)
|
|
626
|
-
percentSetting: 0,
|
|
627
|
-
percentCurrent: 0
|
|
628
|
-
}
|
|
705
|
+
configureFanState(initialState)
|
|
629
706
|
endpoint = new Endpoint(FanDevice.with(BridgedDeviceBasicInformationServer), initialState)
|
|
630
707
|
// Speed changes from the controller (percent write or fan mode change). Like the
|
|
631
708
|
// Thermostat setpoints above, FanControlServer exposes no command to intercept for
|
|
632
709
|
// percentSetting (@matter/node's FanControlServer overrides nothing beyond
|
|
633
710
|
// initialize()) - it is attribute-write only, so this cannot be made "raw" either.
|
|
634
|
-
endpoint
|
|
635
|
-
try {
|
|
636
|
-
if (context?.offline === true) return
|
|
637
|
-
if (value === null || value === undefined) return
|
|
638
|
-
onCommand({ deviceId: def.id, fn: 'fanspeed', value: clamp(Math.round(Number(value)), 0, 100) })
|
|
639
|
-
} catch (error) { /* empty */ }
|
|
640
|
-
})
|
|
641
|
-
endpoint.events.fanControl.fanMode$Changed.on((value, oldValue, context) => {
|
|
642
|
-
try {
|
|
643
|
-
if (context?.offline === true) return
|
|
644
|
-
if (Number(value) === 0) onCommand({ deviceId: def.id, fn: 'fanspeed', value: 0 })
|
|
645
|
-
} catch (error) { /* empty */ }
|
|
646
|
-
})
|
|
711
|
+
attachFanCommandHandlers(endpoint, def, onCommand)
|
|
647
712
|
} else if (def.type === 'robotvacuum') {
|
|
648
713
|
// Flow-only device: run mode changes and operational commands are forwarded to the flow
|
|
649
714
|
// (node PINs); the flow reports the state back through the input PIN.
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=20.18.1"
|
|
5
5
|
},
|
|
6
|
-
"version": "6.3.
|
|
6
|
+
"version": "6.3.15",
|
|
7
7
|
"description": "KNX Ultimate is the most advanced KNX integration for Node-RED, providing secure KNX/IP communication, routing, ETS project import, Philips Hue, Matter Controller and Matter Bridge (control matter device via KNX and expose KNX GA via Matter), MQTT, diagnostics with AI, virtual devices, and powerful automation nodes. Build professional, reliable, and scalable smart home and building automation projects with minimal effort.",
|
|
8
8
|
"files": [
|
|
9
9
|
"nodes/",
|
|
@@ -74,6 +74,95 @@ if (confirmation && confirmation.required === true && Array.isArray(confirmation
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
msg.payload = telegramPayload;
|
|
77
|
+
return msg;`
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'redbot-telegram',
|
|
81
|
+
title: 'RedBot / node-red-contrib-chatbot (Telegram)',
|
|
82
|
+
inputCode: `// RedBot Telegram Receiver -> KNX AI
|
|
83
|
+
// RedBot normalizes Telegram text and inline-button postbacks in msg.payload.
|
|
84
|
+
const redbot = msg.payload;
|
|
85
|
+
if (!redbot || typeof redbot !== 'object') return;
|
|
86
|
+
if (redbot.transport && redbot.transport !== 'telegram') return;
|
|
87
|
+
|
|
88
|
+
const chatId = redbot.chatId;
|
|
89
|
+
const content = typeof redbot.content === 'string' ? redbot.content.trim() : '';
|
|
90
|
+
if (chatId === undefined || chatId === null || content === '') return;
|
|
91
|
+
if (redbot.type !== 'message') return;
|
|
92
|
+
|
|
93
|
+
msg.sessionId = String(chatId);
|
|
94
|
+
msg.language =
|
|
95
|
+
(msg.originalMessage && msg.originalMessage.from && msg.originalMessage.from.language_code) ||
|
|
96
|
+
redbot.language ||
|
|
97
|
+
msg.language ||
|
|
98
|
+
'';
|
|
99
|
+
|
|
100
|
+
const action = content.toLowerCase();
|
|
101
|
+
if (action === 'confirm' || action === 'cancel') {
|
|
102
|
+
msg.topic = action;
|
|
103
|
+
msg.knxAi = Object.assign({}, msg.knxAi, {
|
|
104
|
+
sessionId: String(chatId),
|
|
105
|
+
confirm: action === 'confirm'
|
|
106
|
+
});
|
|
107
|
+
return msg;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
msg.topic = 'ask';
|
|
111
|
+
msg.prompt = content;
|
|
112
|
+
return msg;`,
|
|
113
|
+
outputCode: `// KNX AI chat output -> RedBot Telegram Sender
|
|
114
|
+
// Preserves RedBot conversation helpers and emits either a plain message
|
|
115
|
+
// or inline postback buttons for KNX confirmation/cancellation.
|
|
116
|
+
const source = msg.inputMessage && typeof msg.inputMessage === 'object'
|
|
117
|
+
? msg.inputMessage
|
|
118
|
+
: inputMessage;
|
|
119
|
+
const sourcePayload = source && source.payload && typeof source.payload === 'object'
|
|
120
|
+
? source.payload
|
|
121
|
+
: {};
|
|
122
|
+
const originalMessage = source && source.originalMessage && typeof source.originalMessage === 'object'
|
|
123
|
+
? source.originalMessage
|
|
124
|
+
: {};
|
|
125
|
+
const chatId = sourcePayload.chatId !== undefined
|
|
126
|
+
? sourcePayload.chatId
|
|
127
|
+
: originalMessage.chat && originalMessage.chat.id;
|
|
128
|
+
if (chatId === undefined || chatId === null) return;
|
|
129
|
+
|
|
130
|
+
// RedBot's Sender uses these helpers to resolve the active bot and chat context.
|
|
131
|
+
['originalMessage', 'chat', 'api', 'client', 'redBot'].forEach(function (key) {
|
|
132
|
+
if (msg[key] === undefined && source && source[key] !== undefined) {
|
|
133
|
+
msg[key] = source[key];
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
if (typeof msg.chat !== 'function' || typeof msg.client !== 'function') return;
|
|
137
|
+
|
|
138
|
+
let content = msg.payload;
|
|
139
|
+
if (content && typeof content === 'object') {
|
|
140
|
+
content = content.error || content.message || JSON.stringify(content);
|
|
141
|
+
}
|
|
142
|
+
content = String(content === undefined || content === null ? '' : content);
|
|
143
|
+
|
|
144
|
+
const redbotPayload = {
|
|
145
|
+
transport: sourcePayload.transport || 'telegram',
|
|
146
|
+
chatId: chatId,
|
|
147
|
+
type: 'message',
|
|
148
|
+
inbound: false,
|
|
149
|
+
content: content
|
|
150
|
+
};
|
|
151
|
+
if (sourcePayload.userId !== undefined) redbotPayload.userId = sourcePayload.userId;
|
|
152
|
+
|
|
153
|
+
const confirmation = msg.knxAi && msg.knxAi.confirmationRequest;
|
|
154
|
+
if (confirmation && confirmation.required === true && Array.isArray(confirmation.actions)) {
|
|
155
|
+
redbotPayload.type = 'inline-buttons';
|
|
156
|
+
redbotPayload.buttons = confirmation.actions.map(function (action) {
|
|
157
|
+
return {
|
|
158
|
+
type: 'postback',
|
|
159
|
+
label: action.label,
|
|
160
|
+
value: action.callbackData
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
msg.payload = redbotPayload;
|
|
77
166
|
return msg;`
|
|
78
167
|
}
|
|
79
168
|
]
|