node-red-contrib-knx-ultimate 4.3.23 → 5.0.0

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 CHANGED
@@ -6,6 +6,15 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
+ **Version 5.0.0** - June 2026<br/>
10
+
11
+ - KNXUltimate engine: updated to **6.0.1** (see the engine [CHANGELOG](https://github.com/Supergiovane/knxultimate/blob/main/CHANGELOG.md)).<br/>
12
+ - KNX config node: new **"Reveal keyring passwords"** button in the **Utility** tab, enabled only when **KNX Secure** is selected. It shows, in clear text, all the keyring passwords (interfaces, backbone, group addresses and devices keys/passwords) plus the general keyring password, decoded from the loaded keyring file. Localized in EN/IT/DE/FR/ES/zh-CN.<br/>
13
+
14
+ **Version 4.3.24** - June 2026<br/>
15
+
16
+ - Hue **Contact Sensor** and **Motion** nodes: fixed the state on the KNX bus getting stuck on a wrong value (e.g. a contact sensor showing **closed** while the door stayed open) after the Hue event-stream reconnected. The nodes now re-publish the authoritative state at startup and after every (re)connection, and ignore stale/out-of-order reports by checking the `contact_report.changed` / `motion_report.changed` timestamp. Closes [#514](https://github.com/Supergiovane/node-red-contrib-knx-ultimate/issues/514).<br/>
17
+
9
18
  **Version 4.3.23** - June 2026<br/>
10
19
 
11
20
  - Hue nodes: fixed the **help text** not appearing in the editor sidebar when selecting a node. The help is now declared inline in each node's main HTML file (sidebar reads help from there; the `locales/` files keep providing translations). Affected nodes: Light/Outlet, Area Motion, Battery, Button, Contact Sensor, Light Sensor, Motion, Scene, Tap Dial, Temperature Sensor, Zigbee Connectivity and Device Software Update. Also closed an unterminated template `<script>` tag in the Hue Light node.<br/>
@@ -408,6 +408,121 @@ module.exports = (RED) => {
408
408
  }
409
409
  })
410
410
 
411
+ // 2026-06 Reveal all keyring passwords (and the general keyring password) in clear text.
412
+ // Used by the "Utility" tab button, enabled only when KNX Secure is selected.
413
+ RED.httpAdmin.get('/knxUltimateKeyringDump', RED.auth.needsPermission('knxUltimate-config.read'), async (req, res) => {
414
+ try {
415
+ let keyringContent = (req.query.keyring || '').toString()
416
+ let password = (req.query.pwd || '').toString()
417
+ // '__PWRD__' is the Node-RED placeholder for an unchanged password credential: ignore it.
418
+ if (password === '__PWRD__') password = ''
419
+ // Fill any missing field (keyring and/or password) from the existing config node.
420
+ if ((!keyringContent || !password) && req.query.serverId) {
421
+ const cfg = RED.nodes.getNode(req.query.serverId)
422
+ if (cfg) {
423
+ if (!keyringContent) { try { keyringContent = cfg.keyringFileXML || keyringContent } catch (e) { } }
424
+ if (!password) { try { password = (cfg.credentials && cfg.credentials.keyringFilePassword) ? cfg.credentials.keyringFilePassword : password } catch (e) { } }
425
+ }
426
+ }
427
+ if (!keyringContent || !password) {
428
+ return res.json({ ok: false, error: 'MISSING_KEYRING_OR_PASSWORD' })
429
+ }
430
+ let Keyring
431
+ try {
432
+ ({ Keyring } = require('knxultimate/build/secure/keyring'))
433
+ } catch (e) {
434
+ try { RED.log.error(`KNXUltimate: cannot load Keyring module: ${e.message}`) } catch (err) { }
435
+ return res.json({ ok: false, error: 'KEYRING_MODULE_UNAVAILABLE' })
436
+ }
437
+ const kr = new Keyring()
438
+ try {
439
+ await kr.load(keyringContent, password)
440
+ } catch (e) {
441
+ try { RED.log.error(`KNXUltimate: keyring load error: ${e.message}`) } catch (err) { }
442
+ return res.json({ ok: false, error: 'KEYRING_LOAD_FAILED' })
443
+ }
444
+
445
+ const toIAString = (value) => {
446
+ if (!value) return ''
447
+ return typeof value.toString === 'function' ? value.toString() : String(value)
448
+ }
449
+ const toBufferString = (value) => {
450
+ if (!value) return ''
451
+ if (Buffer.isBuffer(value)) return value.toString('hex')
452
+ return String(value)
453
+ }
454
+
455
+ const lines = []
456
+ lines.push('================ KNX Secure keyring passwords ================')
457
+ lines.push(`Created By: ${kr.getCreatedBy?.() || ''}`)
458
+ lines.push(`Created On: ${kr.getCreated?.() || ''}`)
459
+ lines.push(`General keyring password: ${password}`)
460
+ lines.push('')
461
+
462
+ lines.push('Interfaces:')
463
+ const interfaceMap = kr.getInterfaces?.()
464
+ const interfaces = Array.from(interfaceMap ? interfaceMap.values() : [])
465
+ if (interfaces.length === 0) {
466
+ lines.push(' (none)')
467
+ } else {
468
+ interfaces.forEach((iface, idx) => {
469
+ lines.push(` [${idx + 1}] ${toIAString(iface.individualAddress) || '(unknown)'} (${iface.type || ''})`)
470
+ lines.push(` Host: ${toIAString(iface.host) || ''}`)
471
+ lines.push(` User ID: ${typeof iface.userId === 'number' ? iface.userId : ''}`)
472
+ lines.push(` Password: ${iface.decryptedPassword || ''}`)
473
+ lines.push(` Authentication: ${iface.decryptedAuthentication || ''}`)
474
+ })
475
+ }
476
+ lines.push('')
477
+
478
+ lines.push('Backbones:')
479
+ const backbones = kr.getBackbones?.() || []
480
+ if (backbones.length === 0) {
481
+ lines.push(' (none)')
482
+ } else {
483
+ backbones.forEach((backbone, idx) => {
484
+ lines.push(` [${idx + 1}] Multicast: ${backbone.multicastAddress || ''}`)
485
+ lines.push(` Key (hex): ${toBufferString(backbone.decryptedKey)}`)
486
+ })
487
+ }
488
+ lines.push('')
489
+
490
+ lines.push('Group Addresses:')
491
+ const groupAddressMap = kr.getGroupAddresses?.()
492
+ const groupAddresses = Array.from(groupAddressMap ? groupAddressMap.values() : [])
493
+ if (groupAddresses.length === 0) {
494
+ lines.push(' (none)')
495
+ } else {
496
+ groupAddresses.forEach((group, idx) => {
497
+ lines.push(` [${idx + 1}] ${toIAString(group.address) || ''}`)
498
+ lines.push(` Key (hex): ${toBufferString(group.decryptedKey)}`)
499
+ })
500
+ }
501
+ lines.push('')
502
+
503
+ lines.push('Devices:')
504
+ const deviceMap = kr.getDevices?.()
505
+ const devices = Array.from(deviceMap ? deviceMap.values() : [])
506
+ if (devices.length === 0) {
507
+ lines.push(' (none)')
508
+ } else {
509
+ devices.forEach((device, idx) => {
510
+ lines.push(` [${idx + 1}] ${toIAString(device.individualAddress) || ''}`)
511
+ lines.push(` Tool Key (hex): ${toBufferString(device.decryptedToolKey)}`)
512
+ lines.push(` Management Password: ${device.decryptedManagementPassword || ''}`)
513
+ lines.push(` Authentication: ${device.decryptedAuthentication || ''}`)
514
+ lines.push(` Serial Number: ${device.serialNumber || ''}`)
515
+ })
516
+ }
517
+ lines.push('================ End of keyring passwords ================')
518
+
519
+ res.json({ ok: true, dump: lines.join('\n') })
520
+ } catch (error) {
521
+ try { RED.log.error(`KNXUltimate: knxUltimateKeyringDump error: ${error.message}`) } catch (e) { }
522
+ res.json({ ok: false, error: 'UNEXPECTED_ERROR' })
523
+ }
524
+ })
525
+
411
526
  RED.httpAdmin.get('/knxUltimateGetHueColor', (req, res) => {
412
527
  try {
413
528
  const serverId = RED.nodes.getNode(req.query.serverId) // Retrieve node.id of the config node.
@@ -164,13 +164,13 @@
164
164
  active: (node.knxSecureSelected === true || node.knxSecureSelected === 'true') ? 1 : 0,
165
165
  activate: function (event, ui) {
166
166
  node.knxSecureSelected = $(ui.newTab).index() === 1;
167
- try { updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); enforceProtocolFromIP(); } catch (e) { }
167
+ try { updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); enforceProtocolFromIP(); updateRevealKeyringButton(); } catch (e) { }
168
168
  }
169
169
  });
170
170
  // Ensure the correct tab is enforced after all UI init
171
171
  try {
172
172
  const initialActive = (node.knxSecureSelected === true || node.knxSecureSelected === 'true') ? 1 : 0;
173
- setTimeout(() => { $("#tabsMain").tabs("option", "active", initialActive); updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); enforceProtocolFromIP(); }, 0);
173
+ setTimeout(() => { $("#tabsMain").tabs("option", "active", initialActive); updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); enforceProtocolFromIP(); updateRevealKeyringButton(); }, 0);
174
174
  } catch (e) { }
175
175
 
176
176
  // Keyring ACE editor setup
@@ -1041,6 +1041,56 @@
1041
1041
  } catch (e) { }
1042
1042
  });
1043
1043
 
1044
+ // Utility: reveal all keyring passwords in clear text.
1045
+ // Enabled only when KNX Secure is selected.
1046
+ function updateRevealKeyringButton() {
1047
+ try {
1048
+ const isSecure = isSecureTabActive();
1049
+ $("#revealkeyringpwd").prop('disabled', !isSecure)
1050
+ .css('opacity', isSecure ? 1 : 0.5)
1051
+ .css('cursor', isSecure ? 'pointer' : 'not-allowed');
1052
+ } catch (e) { }
1053
+ }
1054
+ updateRevealKeyringButton();
1055
+
1056
+ $("#revealkeyringpwd").click(function () {
1057
+ if (!isSecureTabActive()) return;
1058
+ try {
1059
+ const keyring = (node._keyringEditor && typeof node._keyringEditor.getValue === 'function')
1060
+ ? node._keyringEditor.getValue()
1061
+ : $("#node-config-input-keyringFileXML").val();
1062
+ const pwd = $("#node-config-input-keyringFilePassword").val();
1063
+ const params = { serverId: node.id };
1064
+ if (typeof keyring === 'string' && keyring.trim() !== '') params.keyring = keyring.trim();
1065
+ // '__PWRD__' is the Node-RED placeholder for an unchanged password credential:
1066
+ // never send it, so the backend falls back to the real stored credential.
1067
+ if (typeof pwd === 'string' && pwd.trim() !== '' && pwd !== '__PWRD__') params.pwd = pwd.trim();
1068
+ $("#divDebugText").show();
1069
+ $("#debugText").val(node._('knxUltimate-config.utility.reveal_keyring_loading'));
1070
+ const scrollToDebugText = function () {
1071
+ try {
1072
+ const el = document.getElementById('debugText');
1073
+ if (el && typeof el.scrollIntoView === 'function') {
1074
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
1075
+ }
1076
+ } catch (e) { }
1077
+ };
1078
+ scrollToDebugText();
1079
+ $.getJSON('knxUltimateKeyringDump', params, (data) => {
1080
+ if (data && data.ok) {
1081
+ $("#debugText").val(data.dump);
1082
+ } else {
1083
+ const code = (data && data.error) ? data.error : 'UNEXPECTED_ERROR';
1084
+ $("#debugText").val(node._('knxUltimate-config.utility.reveal_keyring_fail') + ' (' + code + ')');
1085
+ }
1086
+ scrollToDebugText();
1087
+ }).fail(function (jqXHR, textStatus, errorThrown) {
1088
+ const err = (errorThrown || textStatus || '').toString();
1089
+ $("#debugText").val(node._('knxUltimate-config.utility.reveal_keyring_fail') + (err ? (': ' + err) : ''));
1090
+ });
1091
+ } catch (e) { }
1092
+ });
1093
+
1044
1094
 
1045
1095
 
1046
1096
  },
@@ -1569,8 +1619,14 @@
1569
1619
  <input type="button" id="clearpersistga" class="ui-button ui-corner-all ui-widget"
1570
1620
  style="background-color:#FFD2D2;width:150px" data-i18n="[value]knxUltimate-config.utility.clear_persist_button">
1571
1621
  </div>
1572
-
1573
- <div class="form-row" id="divDebugText" style="display: none;">
1622
+
1623
+ <div class="form-row">
1624
+ <label><i class="fa fa-key"></i> <span data-i18n="knxUltimate-config.utility.reveal_keyring_label"></span></label>
1625
+ <input type="button" id="revealkeyringpwd" class="ui-button ui-corner-all ui-widget"
1626
+ style="background-color:#FFE9B3;width:150px" data-i18n="[value]knxUltimate-config.utility.reveal_keyring_button">
1627
+ </div>
1628
+
1629
+ <div class="form-row" id="divDebugText" style="display: none;">
1574
1630
  <textarea rows="10" id="debugText" style="width:100%"></textarea>
1575
1631
  </div>
1576
1632
  </p>
@@ -24,6 +24,12 @@ const loggerClass = require('./utils/sysLogger')
24
24
  const payloadRounder = require('./utils/payloadManipulation')
25
25
  const utils = require('./utils/utils')
26
26
 
27
+ // Versions logged once at startup (node package + KNXUltimate engine)
28
+ let NODE_VERSION = 'unknown'
29
+ try { NODE_VERSION = require('../package.json').version } catch (e) { /* empty */ }
30
+ let KNX_ENGINE_VERSION = 'unknown'
31
+ try { KNX_ENGINE_VERSION = require('knxultimate/package.json').version } catch (e) { /* empty */ }
32
+
27
33
  // DATAPONT MANIPULATION HELPERS
28
34
  // ####################
29
35
  const sortBy = (field) => (a, b) => {
@@ -185,6 +191,11 @@ const findAutoEthernetInterface = (targetIP) => {
185
191
  }
186
192
 
187
193
  module.exports = (RED) => {
194
+ // Log node and KNXUltimate engine versions once, at Node-RED startup.
195
+ try {
196
+ RED.log.info(`KNXUltimate: node-red-contrib-knx-ultimate v${NODE_VERSION} (KNXUltimate engine v${KNX_ENGINE_VERSION})`)
197
+ } catch (e) { /* empty */ }
198
+
188
199
  function knxUltimateConfigNode (config) {
189
200
  RED.nodes.createNode(this, config)
190
201
  const node = this
@@ -23,7 +23,13 @@ module.exports = function (RED) {
23
23
  node.formatnegativevalue = 'leave'
24
24
  node.formatdecimalsvalue = 2
25
25
  node.hueDevice = config.hueDevice
26
- node.initializingAtStart = false
26
+ // Re-publish the authoritative contact state at startup and after every event-stream
27
+ // (re)connection. Without this, any contact_report event missed during an SSE reconnect
28
+ // gap leaves KNX stuck on the last value seen (see issue #514).
29
+ node.initializingAtStart = (config.readStatusAtStartup === undefined || config.readStatusAtStartup === 'yes')
30
+ // Timestamp (ms) of the last contact_report.changed we already applied to KNX. Used to
31
+ // ignore stale/out-of-order reports (e.g. replays after a reconnect).
32
+ node.lastContactChangedTs = undefined
27
33
 
28
34
  const pushStatus = (status) => {
29
35
  if (!status) return
@@ -94,6 +100,18 @@ module.exports = function (RED) {
94
100
  return
95
101
  }
96
102
 
103
+ // contact_report.state is always the real current state (per Hue API v2), and
104
+ // contact_report.changed is when it last changed. Ignore reports that are not newer
105
+ // than the last one applied, so stale/replayed events after an SSE reconnect can't
106
+ // flip KNX to a wrong state (issue #514).
107
+ const changedTs = _event.contact_report.changed !== undefined ? new Date(_event.contact_report.changed).getTime() : NaN
108
+ if (!Number.isNaN(changedTs)) {
109
+ if (node.lastContactChangedTs !== undefined && changedTs <= node.lastContactChangedTs) {
110
+ return
111
+ }
112
+ node.lastContactChangedTs = changedTs
113
+ }
114
+
97
115
  const knxMsgPayload = {}
98
116
  knxMsgPayload.topic = config.GAcontact
99
117
  knxMsgPayload.dpt = config.dptcontact
@@ -23,7 +23,13 @@ module.exports = function (RED) {
23
23
  node.formatnegativevalue = 'leave'
24
24
  node.formatdecimalsvalue = 2
25
25
  node.hueDevice = config.hueDevice
26
- node.initializingAtStart = false
26
+ // Re-publish the authoritative motion state at startup and after every event-stream
27
+ // (re)connection, so events missed during an SSE reconnect gap don't leave KNX stuck
28
+ // on a wrong value (same root cause as the contact sensor, issue #514).
29
+ node.initializingAtStart = (config.readStatusAtStartup === undefined || config.readStatusAtStartup === 'yes')
30
+ // Timestamp (ms) of the last motion_report.changed we already applied to KNX. Used to
31
+ // ignore stale/out-of-order reports (e.g. replays after a reconnect).
32
+ node.lastMotionChangedTs = undefined
27
33
  const pinsSetting = (config.enableNodePINS === undefined || config.enableNodePINS === 'yes' || config.enableNodePINS === true)
28
34
  node.enableNodePINS = pinsSetting ? 'yes' : 'no'
29
35
  node.outputs = pinsSetting ? 1 : 0
@@ -97,6 +103,18 @@ module.exports = function (RED) {
97
103
  try {
98
104
  if (_event.id === config.hueDevice) {
99
105
  if (!_event.hasOwnProperty('motion') || _event.motion.motion === undefined) return
106
+
107
+ // Ignore reports that are not newer than the last one applied, so stale/replayed
108
+ // events after an SSE reconnect can't push a wrong state to KNX (issue #514).
109
+ const motionChanged = _event.motion.motion_report !== undefined ? _event.motion.motion_report.changed : undefined
110
+ const changedTs = motionChanged !== undefined ? new Date(motionChanged).getTime() : NaN
111
+ if (!Number.isNaN(changedTs)) {
112
+ if (node.lastMotionChangedTs !== undefined && changedTs <= node.lastMotionChangedTs) {
113
+ return
114
+ }
115
+ node.lastMotionChangedTs = changedTs
116
+ }
117
+
100
118
  const knxMsgPayload = {}
101
119
  knxMsgPayload.topic = config.GAmotion
102
120
  knxMsgPayload.dpt = config.dptmotion
@@ -144,7 +144,11 @@
144
144
  "clear_persist_ok": "Löschen",
145
145
  "clear_persist_cancel": "Abbrechen",
146
146
  "clear_persist_done": "Persistenter GA-Cache gelöscht.",
147
- "clear_persist_fail": "Persistenter GA-Cache konnte nicht gelöscht werden"
147
+ "clear_persist_fail": "Persistenter GA-Cache konnte nicht gelöscht werden",
148
+ "reveal_keyring_label": "Keyring-Passwörter im Klartext anzeigen (KNX Secure)",
149
+ "reveal_keyring_button": "Anzeigen",
150
+ "reveal_keyring_loading": "Keyring wird dekodiert, bitte warten...",
151
+ "reveal_keyring_fail": "Keyring konnte nicht dekodiert werden. Prüfen Sie die Keyring-Datei und das allgemeine Passwort."
148
152
  }
149
153
  }
150
154
  }
@@ -148,7 +148,11 @@
148
148
  "clear_persist_ok": "Delete",
149
149
  "clear_persist_cancel": "Cancel",
150
150
  "clear_persist_done": "Persisted GA cache deleted.",
151
- "clear_persist_fail": "Unable to delete persisted GA cache"
151
+ "clear_persist_fail": "Unable to delete persisted GA cache",
152
+ "reveal_keyring_label": "Reveal keyring passwords in clear text (KNX Secure)",
153
+ "reveal_keyring_button": "Reveal",
154
+ "reveal_keyring_loading": "Decoding keyring, please wait...",
155
+ "reveal_keyring_fail": "Unable to decode the keyring. Check the keyring file and the general password."
152
156
  }
153
157
  }
154
158
  }
@@ -148,7 +148,11 @@
148
148
  "clear_persist_ok": "Eliminar",
149
149
  "clear_persist_cancel": "Cancelar",
150
150
  "clear_persist_done": "Caché persistente de GA eliminada.",
151
- "clear_persist_fail": "No se puede eliminar la caché persistente de GA"
151
+ "clear_persist_fail": "No se puede eliminar la caché persistente de GA",
152
+ "reveal_keyring_label": "Mostrar las contraseñas del keyring en texto claro (KNX Secure)",
153
+ "reveal_keyring_button": "Mostrar",
154
+ "reveal_keyring_loading": "Decodificando el keyring, espere...",
155
+ "reveal_keyring_fail": "No se puede decodificar el keyring. Comprueba el archivo keyring y la contraseña general."
152
156
  }
153
157
  }
154
158
  }
@@ -148,7 +148,11 @@
148
148
  "clear_persist_ok": "Supprimer",
149
149
  "clear_persist_cancel": "Annuler",
150
150
  "clear_persist_done": "Cache GA persistant supprimé.",
151
- "clear_persist_fail": "Impossible de supprimer le cache GA persistant"
151
+ "clear_persist_fail": "Impossible de supprimer le cache GA persistant",
152
+ "reveal_keyring_label": "Afficher les mots de passe du keyring en clair (KNX Secure)",
153
+ "reveal_keyring_button": "Afficher",
154
+ "reveal_keyring_loading": "Décodage du keyring en cours, veuillez patienter...",
155
+ "reveal_keyring_fail": "Impossible de décoder le keyring. Vérifiez le fichier keyring et le mot de passe général."
152
156
  }
153
157
  }
154
158
  }
@@ -152,7 +152,11 @@
152
152
  "clear_persist_ok": "Elimina",
153
153
  "clear_persist_cancel": "Annulla",
154
154
  "clear_persist_done": "Cache persistente GA eliminata.",
155
- "clear_persist_fail": "Impossibile eliminare la cache persistente GA"
155
+ "clear_persist_fail": "Impossibile eliminare la cache persistente GA",
156
+ "reveal_keyring_label": "Mostra le password del keyring in chiaro (KNX Secure)",
157
+ "reveal_keyring_button": "Mostra",
158
+ "reveal_keyring_loading": "Decodifica del keyring in corso, attendere...",
159
+ "reveal_keyring_fail": "Impossibile decodificare il keyring. Verifica il file keyring e la password generale."
156
160
  }
157
161
  }
158
162
  }
@@ -140,7 +140,11 @@
140
140
  "clear_persist_ok": "删除",
141
141
  "clear_persist_cancel": "取消",
142
142
  "clear_persist_done": "已删除持久化 GA 缓存。",
143
- "clear_persist_fail": "无法删除持久化 GA 缓存"
143
+ "clear_persist_fail": "无法删除持久化 GA 缓存",
144
+ "reveal_keyring_label": "以明文显示密钥环密码 (KNX Secure)",
145
+ "reveal_keyring_button": "显示",
146
+ "reveal_keyring_loading": "正在解码密钥环,请稍候...",
147
+ "reveal_keyring_fail": "无法解码密钥环。请检查密钥环文件和通用密码。"
144
148
  }
145
149
  }
146
150
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "engines": {
4
4
  "node": ">=20.18.1"
5
5
  },
6
- "version": "4.3.23",
6
+ "version": "5.0.0",
7
7
  "description": "Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control, ETS group address importer, KNX AI for diagnosticsand KNX routing between interfaces. Easy to use and highly configurable.",
8
8
  "files": [
9
9
  "nodes/",
@@ -19,7 +19,7 @@
19
19
  "dns-sync": "0.2.1",
20
20
  "google-translate-tts": "^0.3.0",
21
21
  "js-yaml": "4.1.1",
22
- "knxultimate": "5.5.9",
22
+ "knxultimate": "6.0.1",
23
23
  "lodash": "4.18.1",
24
24
  "node-color-log": "12.0.1",
25
25
  "ping": "0.4.4",