meross-cli 0.1.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +110 -0
  4. package/cli/commands/control/execute.js +23 -0
  5. package/cli/commands/control/index.js +12 -0
  6. package/cli/commands/control/menu.js +193 -0
  7. package/cli/commands/control/params/generic.js +229 -0
  8. package/cli/commands/control/params/index.js +56 -0
  9. package/cli/commands/control/params/light.js +188 -0
  10. package/cli/commands/control/params/thermostat.js +166 -0
  11. package/cli/commands/control/params/timer.js +242 -0
  12. package/cli/commands/control/params/trigger.js +206 -0
  13. package/cli/commands/dump.js +35 -0
  14. package/cli/commands/index.js +34 -0
  15. package/cli/commands/info.js +221 -0
  16. package/cli/commands/list.js +112 -0
  17. package/cli/commands/mqtt.js +187 -0
  18. package/cli/commands/sniffer/device-sniffer.js +217 -0
  19. package/cli/commands/sniffer/fake-app.js +233 -0
  20. package/cli/commands/sniffer/index.js +7 -0
  21. package/cli/commands/sniffer/message-queue.js +65 -0
  22. package/cli/commands/sniffer/sniffer-menu.js +676 -0
  23. package/cli/commands/stats.js +90 -0
  24. package/cli/commands/status/device-status.js +1403 -0
  25. package/cli/commands/status/hub-status.js +72 -0
  26. package/cli/commands/status/index.js +50 -0
  27. package/cli/commands/status/subdevices/hub-smoke-detector.js +82 -0
  28. package/cli/commands/status/subdevices/hub-temp-hum-sensor.js +43 -0
  29. package/cli/commands/status/subdevices/hub-thermostat-valve.js +83 -0
  30. package/cli/commands/status/subdevices/hub-water-leak-sensor.js +27 -0
  31. package/cli/commands/status/subdevices/index.js +23 -0
  32. package/cli/commands/test/index.js +185 -0
  33. package/cli/config/users.js +108 -0
  34. package/cli/control-registry.js +875 -0
  35. package/cli/helpers/client.js +89 -0
  36. package/cli/helpers/meross.js +106 -0
  37. package/cli/menu/index.js +10 -0
  38. package/cli/menu/main.js +648 -0
  39. package/cli/menu/settings.js +789 -0
  40. package/cli/meross-cli.js +547 -0
  41. package/cli/tests/README.md +365 -0
  42. package/cli/tests/test-alarm.js +144 -0
  43. package/cli/tests/test-child-lock.js +248 -0
  44. package/cli/tests/test-config.js +133 -0
  45. package/cli/tests/test-control.js +189 -0
  46. package/cli/tests/test-diffuser.js +505 -0
  47. package/cli/tests/test-dnd.js +246 -0
  48. package/cli/tests/test-electricity.js +209 -0
  49. package/cli/tests/test-encryption.js +281 -0
  50. package/cli/tests/test-garage.js +259 -0
  51. package/cli/tests/test-helper.js +313 -0
  52. package/cli/tests/test-hub-mts100.js +355 -0
  53. package/cli/tests/test-hub-sensors.js +489 -0
  54. package/cli/tests/test-light.js +253 -0
  55. package/cli/tests/test-presence.js +497 -0
  56. package/cli/tests/test-registry.js +419 -0
  57. package/cli/tests/test-roller-shutter.js +628 -0
  58. package/cli/tests/test-runner.js +415 -0
  59. package/cli/tests/test-runtime.js +234 -0
  60. package/cli/tests/test-screen.js +133 -0
  61. package/cli/tests/test-sensor-history.js +146 -0
  62. package/cli/tests/test-smoke-config.js +138 -0
  63. package/cli/tests/test-spray.js +131 -0
  64. package/cli/tests/test-temp-unit.js +133 -0
  65. package/cli/tests/test-template.js +238 -0
  66. package/cli/tests/test-thermostat.js +919 -0
  67. package/cli/tests/test-timer.js +372 -0
  68. package/cli/tests/test-toggle.js +342 -0
  69. package/cli/tests/test-trigger.js +279 -0
  70. package/cli/utils/display.js +86 -0
  71. package/cli/utils/terminal.js +137 -0
  72. package/package.json +53 -0
@@ -0,0 +1,1403 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const { MerossHubDevice, DNDMode, ThermostatMode, WorkMode, SensitivityLevel } = require('meross-iot');
6
+
7
+ /**
8
+ * Returns list of dangerous namespaces that should never be called automatically.
9
+ */
10
+ function _getDangerousNamespaces() {
11
+ return [
12
+ 'Appliance.Control.Unbind', // Removes device from account!
13
+ 'Appliance.Control.Bind', // Binds devices (should be explicit)
14
+ 'Appliance.Control.Upgrade', // Triggers firmware upgrades
15
+ 'Appliance.Control.ChangeWiFi', // Changes WiFi settings
16
+ 'Appliance.Hub.Unbind', // Unbinds subdevices
17
+ 'Appliance.Hub.Bind' // Binds subdevices
18
+ ];
19
+ }
20
+
21
+ /**
22
+ * Filters out namespaces that don't support GET requests or are dangerous.
23
+ */
24
+ function _getAllNamespaces(abilities, dangerousNamespaces) {
25
+ return Object.keys(abilities).filter(ns => {
26
+ if (ns.startsWith('Appliance.Hub.')) {return false;}
27
+ if (ns === 'Appliance.System.Ability' || ns === 'Appliance.System.All') {return false;}
28
+ if (ns === 'Appliance.System.Clock') {return false;} // Only supports PUSH mode (device-initiated), not GET
29
+ if (ns === 'Appliance.System.Report') {return false;} // Only supports PUSH mode (device-initiated reports), not GET
30
+ if (ns === 'Appliance.Control.Multiple') {return false;} // Only supports SET (executing multiple commands), not GET
31
+ if (ns === 'Appliance.Control.Upgrade') {return false;} // Only supports SET/PUSH (triggering upgrades), not GET
32
+ if (ns === 'Appliance.Control.OverTemp') {return false;} // Only supports SET (device-initiated when over-temp), not GET
33
+ if (ns === 'Appliance.Control.AlertReport') {return false;} // Only supports PUSH mode (device-initiated alerts), not GET
34
+ if (ns === 'Appliance.Config.Key') {return false;} // Only supports SET (security-sensitive MQTT credentials), not GET
35
+ if (ns.startsWith('Appliance.Encrypt.')) {return false;} // Encryption setup, not GET-able
36
+ if (dangerousNamespaces.includes(ns)) {return false;} // Never call dangerous namespaces
37
+ return true;
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Categorizes namespaces into sensor and config namespaces.
43
+ */
44
+ function _categorizeNamespaces(allNamespaces) {
45
+ const sensorNamespaces = allNamespaces.filter(ns => {
46
+ // Control namespaces are sensors/state
47
+ if (ns.startsWith('Appliance.Control.')) {return true;}
48
+ // Digest namespaces are state
49
+ if (ns.startsWith('Appliance.Digest.')) {return true;}
50
+ // System namespaces that are state (not config)
51
+ if (ns.startsWith('Appliance.System.')) {
52
+ // System.DNDMode and System.LedMode are config, others are state
53
+ if (ns === 'Appliance.System.DNDMode' || ns === 'Appliance.System.LedMode') {return false;}
54
+ return true;
55
+ }
56
+ // Garage door, roller shutter, etc. are sensors/state
57
+ if (ns.startsWith('Appliance.GarageDoor.') || ns.startsWith('Appliance.RollerShutter.')) {return true;}
58
+ return false;
59
+ });
60
+
61
+ const configNamespaces = allNamespaces.filter(ns => {
62
+ // Config namespaces are configuration
63
+ if (ns.startsWith('Appliance.Config.')) {return true;}
64
+ // Some System namespaces are config
65
+ if (ns === 'Appliance.System.DNDMode' || ns === 'Appliance.System.LedMode') {return true;}
66
+ // Presence config is a Control namespace but it's configuration
67
+ if (ns === 'Appliance.Control.Presence.Config') {return true;}
68
+ return false;
69
+ });
70
+
71
+ return { sensorNamespaces, configNamespaces };
72
+ }
73
+
74
+ /**
75
+ * Filters sensor namespaces to determine which ones need to be fetched.
76
+ */
77
+ function _filterNamespacesToFetch(sensorNamespaces, isMqttConnected, device) {
78
+ const pushNotificationNamespaces = [
79
+ 'Appliance.Control.ToggleX',
80
+ 'Appliance.Control.Toggle',
81
+ 'Appliance.Control.Thermostat.Mode',
82
+ 'Appliance.Control.Thermostat.ModeB',
83
+ 'Appliance.Control.Light',
84
+ 'Appliance.Digest.TimerX',
85
+ 'Appliance.Digest.TriggerX'
86
+ ];
87
+
88
+ const pollingRequiredNamespaces = [
89
+ 'Appliance.Control.Electricity',
90
+ 'Appliance.Control.ConsumptionX',
91
+ 'Appliance.Control.Consumption',
92
+ 'Appliance.Control.Sensor.History',
93
+ 'Appliance.Control.Sensor.Latest',
94
+ 'Appliance.Control.Sensor.LatestX'
95
+ ];
96
+
97
+ return sensorNamespaces.filter(ns => {
98
+ // Always fetch polling-required namespaces
99
+ if (pollingRequiredNamespaces.some(pns => ns === pns || ns.startsWith(pns))) {
100
+ return true;
101
+ }
102
+ // For push-based namespaces, only fetch if:
103
+ // 1. Not connected via MQTT, OR
104
+ // 2. No cached state available
105
+ if (pushNotificationNamespaces.some(pns => ns === pns || ns.startsWith(pns))) {
106
+ if (!isMqttConnected) {
107
+ return true; // Not MQTT connected, need to poll
108
+ }
109
+ // Check if we have cached state - if yes, skip polling
110
+ if (ns === 'Appliance.Control.ToggleX' && typeof device.isOn === 'function') {
111
+ const isOn = device.isOn(0);
112
+ if (isOn !== undefined) {
113
+ return false; // Have cached state, skip polling
114
+ }
115
+ }
116
+ // For other push-based namespaces, still fetch to ensure we have latest data
117
+ return true;
118
+ }
119
+ // For unknown namespaces, fetch them
120
+ return true;
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Builds payload for a namespace (fallback for namespaces without feature methods).
126
+ * NOTE: This is a FALLBACK. Feature methods are preferred because they handle payload format correctly,
127
+ * update device state automatically, handle unit conversions, and return typed objects.
128
+ */
129
+ function _buildPayloadForNamespace(namespace) {
130
+ // Known payload patterns
131
+ if (namespace === 'Appliance.Control.ToggleX') {
132
+ return { togglex: { channel: 0 } };
133
+ } else if (namespace === 'Appliance.Control.Toggle') {
134
+ return {};
135
+ } else if (namespace === 'Appliance.Control.Electricity') {
136
+ return { channel: 0 };
137
+ } else if (namespace === 'Appliance.Control.Thermostat.Mode') {
138
+ return { mode: [{ channel: 0 }] };
139
+ } else if (namespace === 'Appliance.Control.Thermostat.ModeB') {
140
+ return { modeB: [{ channel: 0 }] };
141
+ } else if (namespace.startsWith('Appliance.Control.Thermostat.')) {
142
+ // Extract the key from namespace (e.g., "WindowOpened" from "Appliance.Control.Thermostat.WindowOpened")
143
+ const key = namespace.replace('Appliance.Control.Thermostat.', '');
144
+ // Convert to camelCase for payload key
145
+ const payloadKey = key.charAt(0).toLowerCase() + key.slice(1);
146
+ // Special cases
147
+ if (key === 'DeadZone') {
148
+ return { deadZone: [{ channel: 0 }] };
149
+ } else if (key === 'HoldAction') {
150
+ return { holdAction: [{ channel: 0 }] };
151
+ } else if (key === 'WindowOpened') {
152
+ return { windowOpened: [{ channel: 0 }] };
153
+ }
154
+ const payload = {};
155
+ payload[payloadKey] = [{ channel: 0 }];
156
+ return payload;
157
+ } else if (namespace === 'Appliance.Control.Screen.Brightness') {
158
+ return { brightness: [{ channel: 0 }] };
159
+ } else if (namespace === 'Appliance.Control.PhysicalLock') {
160
+ return { lock: [{ channel: 0 }] };
161
+ } else if (namespace === 'Appliance.Control.Sensor.LatestX') {
162
+ // Presence sensor - use device method instead of raw publishMessage
163
+ return null; // Signal to use device method
164
+ } else if (namespace === 'Appliance.Control.TimerX') {
165
+ // TimerX GET - use device feature method which handles payload correctly
166
+ return null; // Signal to use device method
167
+ } else if (namespace.startsWith('Appliance.Control.')) {
168
+ // Generic Control namespace - try to infer payload key
169
+ const key = namespace.replace('Appliance.Control.', '');
170
+ const payloadKey = key.charAt(0).toLowerCase() + key.slice(1);
171
+ // Try array format first (most common)
172
+ const payload = {};
173
+ payload[payloadKey] = [{ channel: 0 }];
174
+ return payload;
175
+ } else if (namespace.startsWith('Appliance.Digest.')) {
176
+ // Digest namespaces usually don't need payload or use empty
177
+ return {};
178
+ } else if (namespace.startsWith('Appliance.System.')) {
179
+ // System namespaces usually don't need payload
180
+ return {};
181
+ } else if (namespace.startsWith('Appliance.GarageDoor.')) {
182
+ // Try generic payload
183
+ const key = namespace.replace('Appliance.GarageDoor.', '');
184
+ const payloadKey = key.charAt(0).toLowerCase() + key.slice(1);
185
+ const payload = {};
186
+ payload[payloadKey] = [{ channel: 0 }];
187
+ return payload;
188
+ } else if (namespace.startsWith('Appliance.RollerShutter.')) {
189
+ const key = namespace.replace('Appliance.RollerShutter.', '');
190
+ const payloadKey = key.charAt(0).toLowerCase() + key.slice(1);
191
+ const payload = {};
192
+ payload[payloadKey] = [{ channel: 0 }];
193
+ return payload;
194
+ }
195
+ // Default: empty payload
196
+ return {};
197
+ }
198
+
199
+ /**
200
+ * Maps Toggle feature namespaces to feature methods.
201
+ */
202
+ function _mapToggleFeatureNamespace(namespace, device) {
203
+ if (namespace === 'Appliance.Control.ToggleX' && typeof device.getToggleState === 'function') {
204
+ return { featureMethod: device.getToggleState.bind(device), featureArgs: [0] };
205
+ }
206
+ return null;
207
+ }
208
+
209
+ /**
210
+ * Maps Light feature namespaces to feature methods.
211
+ */
212
+ function _mapLightFeatureNamespace(namespace, device) {
213
+ if (namespace === 'Appliance.Control.Light' && typeof device.getLightState === 'function') {
214
+ return { featureMethod: device.getLightState.bind(device), featureArgs: [] };
215
+ }
216
+ return null;
217
+ }
218
+
219
+ /**
220
+ * Maps Diffuser feature namespaces to feature methods.
221
+ */
222
+ function _mapDiffuserFeatureNamespace(namespace, device) {
223
+ if (namespace === 'Appliance.Control.Diffuser.Light' && typeof device.getDiffuserLightState === 'function') {
224
+ return { featureMethod: device.getDiffuserLightState.bind(device), featureArgs: [] };
225
+ } else if (namespace === 'Appliance.Control.Diffuser.Spray' && typeof device.getDiffuserSprayState === 'function') {
226
+ return { featureMethod: device.getDiffuserSprayState.bind(device), featureArgs: [] };
227
+ }
228
+ return null;
229
+ }
230
+
231
+ /**
232
+ * Maps Spray feature namespaces to feature methods.
233
+ */
234
+ function _mapSprayFeatureNamespace(namespace, device) {
235
+ if (namespace === 'Appliance.Control.Spray' && typeof device.getSprayState === 'function') {
236
+ return { featureMethod: device.getSprayState.bind(device), featureArgs: [] };
237
+ }
238
+ return null;
239
+ }
240
+
241
+ /**
242
+ * Maps Electricity feature namespaces to feature methods.
243
+ */
244
+ function _mapElectricityFeatureNamespace(namespace, device) {
245
+ if (namespace === 'Appliance.Control.Electricity' && typeof device.getElectricity === 'function') {
246
+ return { featureMethod: device.getElectricity.bind(device), featureArgs: [0] };
247
+ }
248
+ return null;
249
+ }
250
+
251
+ /**
252
+ * Maps Consumption feature namespaces to feature methods.
253
+ */
254
+ function _mapConsumptionFeatureNamespace(namespace, device) {
255
+ if (namespace === 'Appliance.Control.ConsumptionX' && typeof device.getPowerConsumptionX === 'function') {
256
+ return { featureMethod: device.getRawPowerConsumptionX.bind(device), featureArgs: [0] };
257
+ } else if (namespace === 'Appliance.Control.Consumption' && typeof device.getPowerConsumption === 'function') {
258
+ return { featureMethod: device.getRawPowerConsumption.bind(device), featureArgs: [0] };
259
+ } else if (namespace === 'Appliance.Control.ConsumptionConfig' && typeof device.getConsumptionConfig === 'function') {
260
+ return { featureMethod: device.getConsumptionConfig.bind(device), featureArgs: [] };
261
+ }
262
+ return null;
263
+ }
264
+
265
+ /**
266
+ * Maps Timer feature namespaces to feature methods.
267
+ */
268
+ function _mapTimerFeatureNamespace(namespace, device) {
269
+ if (namespace === 'Appliance.Control.TimerX' && typeof device.getTimerX === 'function') {
270
+ // TimerX GET requests can return large amounts of data and may cause connection issues over HTTP
271
+ // TimerX data is available in System.All digest, so skip direct GET requests over HTTP
272
+ return { featureMethod: null, featureArgs: [] }; // Skip TimerX GET over HTTP
273
+ }
274
+ return null;
275
+ }
276
+
277
+ /**
278
+ * Maps Trigger feature namespaces to feature methods.
279
+ */
280
+ function _mapTriggerFeatureNamespace(namespace, device) {
281
+ if (namespace === 'Appliance.Control.TriggerX' && typeof device.getTriggerX === 'function') {
282
+ return { featureMethod: device.getTriggerX.bind(device), featureArgs: [0] };
283
+ }
284
+ return null;
285
+ }
286
+
287
+ /**
288
+ * Maps Sensor feature namespaces to feature methods.
289
+ */
290
+ function _mapSensorFeatureNamespace(namespace, device) {
291
+ if (namespace === 'Appliance.Control.Sensor.LatestX' && typeof device.getLatestSensorReadings === 'function') {
292
+ return { featureMethod: device.getLatestSensorReadings.bind(device), featureArgs: [['presence', 'light']] };
293
+ } else if (namespace === 'Appliance.Control.Sensor.History' && typeof device.getSensorHistory === 'function') {
294
+ return { featureMethod: device.getSensorHistory.bind(device), featureArgs: [0, 1] }; // channel, capacity
295
+ }
296
+ return null;
297
+ }
298
+
299
+ /**
300
+ * Maps shared/common Control namespaces to feature methods.
301
+ * These are namespaces that don't belong to a specific feature but are common across devices.
302
+ */
303
+ function _mapSharedControlNamespace(namespace, device) {
304
+ if (namespace === 'Appliance.Control.Alarm' && typeof device.getAlarmStatus === 'function') {
305
+ return { featureMethod: device.getAlarmStatus.bind(device), featureArgs: [0] };
306
+ } else if (namespace === 'Appliance.Control.TempUnit' && typeof device.getTempUnit === 'function') {
307
+ return { featureMethod: device.getTempUnit.bind(device), featureArgs: [0] };
308
+ } else if (namespace === 'Appliance.Control.Smoke.Config' && typeof device.getSmokeConfig === 'function') {
309
+ return { featureMethod: device.getSmokeConfig.bind(device), featureArgs: [0] };
310
+ } else if (namespace === 'Appliance.Control.Presence.Study' && typeof device.getPresenceStudy === 'function') {
311
+ return { featureMethod: device.getPresenceStudy.bind(device), featureArgs: [] };
312
+ } else if (namespace === 'Appliance.Control.Screen.Brightness' && typeof device.getScreenBrightness === 'function') {
313
+ return { featureMethod: device.getScreenBrightness.bind(device), featureArgs: [0] };
314
+ } else if (namespace === 'Appliance.Control.PhysicalLock' && typeof device.getChildLock === 'function') {
315
+ return { featureMethod: device.getChildLock.bind(device), featureArgs: [0] };
316
+ }
317
+ return null;
318
+ }
319
+
320
+ /**
321
+ * Maps Thermostat namespaces to feature methods.
322
+ */
323
+ function _mapThermostatNamespaceToFeatureMethod(namespace, device) {
324
+ if (namespace === 'Appliance.Control.Thermostat.Mode' && typeof device.getThermostatMode === 'function') {
325
+ return { featureMethod: device.getThermostatMode.bind(device), featureArgs: [0] };
326
+ } else if (namespace === 'Appliance.Control.Thermostat.ModeB' && typeof device.getThermostatModeB === 'function') {
327
+ return { featureMethod: device.getThermostatModeB.bind(device), featureArgs: [0] };
328
+ } else if (namespace === 'Appliance.Control.Thermostat.WindowOpened' && typeof device.getThermostatWindowOpened === 'function') {
329
+ return { featureMethod: device.getThermostatWindowOpened.bind(device), featureArgs: [0] };
330
+ } else if (namespace === 'Appliance.Control.Thermostat.Calibration' && typeof device.getThermostatCalibration === 'function') {
331
+ return { featureMethod: device.getThermostatCalibration.bind(device), featureArgs: [0] };
332
+ } else if (namespace === 'Appliance.Control.Thermostat.DeadZone' && typeof device.getThermostatDeadZone === 'function') {
333
+ return { featureMethod: device.getThermostatDeadZone.bind(device), featureArgs: [0] };
334
+ } else if (namespace === 'Appliance.Control.Thermostat.HoldAction' && typeof device.getThermostatHoldAction === 'function') {
335
+ return { featureMethod: device.getThermostatHoldAction.bind(device), featureArgs: [0] };
336
+ } else if (namespace === 'Appliance.Control.Thermostat.Overheat' && typeof device.getThermostatOverheat === 'function') {
337
+ return { featureMethod: device.getThermostatOverheat.bind(device), featureArgs: [0] };
338
+ } else if (namespace === 'Appliance.Control.Thermostat.Frost' && typeof device.getThermostatFrost === 'function') {
339
+ return { featureMethod: device.getThermostatFrost.bind(device), featureArgs: [0] };
340
+ } else if (namespace === 'Appliance.Control.Thermostat.Sensor' && typeof device.getThermostatSensor === 'function') {
341
+ return { featureMethod: device.getThermostatSensor.bind(device), featureArgs: [0] };
342
+ } else if (namespace === 'Appliance.Control.Thermostat.Schedule' && typeof device.getThermostatSchedule === 'function') {
343
+ return { featureMethod: device.getThermostatSchedule.bind(device), featureArgs: [0] };
344
+ }
345
+ return null;
346
+ }
347
+
348
+ /**
349
+ * Maps System namespaces to feature methods.
350
+ */
351
+ function _mapSystemNamespaceToFeatureMethod(namespace, device) {
352
+ if (namespace === 'Appliance.System.Runtime' && typeof device.updateRuntimeInfo === 'function') {
353
+ return { featureMethod: device.updateRuntimeInfo.bind(device), featureArgs: [] };
354
+ } else if (namespace === 'Appliance.System.Hardware' && typeof device.getSystemHardware === 'function') {
355
+ return { featureMethod: device.getSystemHardware.bind(device), featureArgs: [] };
356
+ } else if (namespace === 'Appliance.System.Firmware' && typeof device.getSystemFirmware === 'function') {
357
+ return { featureMethod: device.getSystemFirmware.bind(device), featureArgs: [] };
358
+ } else if (namespace === 'Appliance.System.Debug' && typeof device.getSystemDebug === 'function') {
359
+ return { featureMethod: device.getSystemDebug.bind(device), featureArgs: [] };
360
+ } else if (namespace === 'Appliance.System.Online' && typeof device.getOnlineStatus === 'function') {
361
+ return { featureMethod: device.getOnlineStatus.bind(device), featureArgs: [] };
362
+ } else if (namespace === 'Appliance.System.Time' && typeof device.getSystemTime === 'function') {
363
+ return { featureMethod: device.getSystemTime.bind(device), featureArgs: [] };
364
+ } else if (namespace === 'Appliance.System.Position' && typeof device.getSystemPosition === 'function') {
365
+ return { featureMethod: device.getSystemPosition.bind(device), featureArgs: [] };
366
+ } else if (namespace === 'Appliance.System.DNDMode' && typeof device.getDNDMode === 'function') {
367
+ return { featureMethod: device.getRawDNDMode.bind(device), featureArgs: [] };
368
+ }
369
+ return null;
370
+ }
371
+
372
+ /**
373
+ * Maps Digest namespaces to feature methods.
374
+ */
375
+ function _mapDigestNamespaceToFeatureMethod(namespace, device) {
376
+ if (namespace === 'Appliance.Digest.TimerX' && typeof device.getTimerXDigest === 'function') {
377
+ return { featureMethod: device.getTimerXDigest.bind(device), featureArgs: [] };
378
+ } else if (namespace === 'Appliance.Digest.TriggerX' && typeof device.getTriggerXDigest === 'function') {
379
+ return { featureMethod: device.getTriggerXDigest.bind(device), featureArgs: [] };
380
+ }
381
+ return null;
382
+ }
383
+
384
+
385
+ /**
386
+ * Maps GarageDoor feature namespaces to feature methods.
387
+ */
388
+ function _mapGarageFeatureNamespace(namespace, device) {
389
+ if (namespace === 'Appliance.GarageDoor.State' && typeof device.getGarageDoorState === 'function') {
390
+ return { featureMethod: device.getGarageDoorState.bind(device), featureArgs: [0] };
391
+ } else if (namespace === 'Appliance.GarageDoor.MultipleConfig' && typeof device.getGarageDoorMultipleState === 'function') {
392
+ return { featureMethod: device.getGarageDoorMultipleState.bind(device), featureArgs: [] };
393
+ }
394
+ return null;
395
+ }
396
+
397
+ /**
398
+ * Maps RollerShutter feature namespaces to feature methods.
399
+ */
400
+ function _mapRollerShutterFeatureNamespace(namespace, device) {
401
+ if (namespace === 'Appliance.RollerShutter.State' && typeof device.getRollerShutterState === 'function') {
402
+ return { featureMethod: device.getRollerShutterState.bind(device), featureArgs: [] };
403
+ } else if (namespace === 'Appliance.RollerShutter.Position' && typeof device.getRollerShutterPosition === 'function') {
404
+ return { featureMethod: device.getRollerShutterPosition.bind(device), featureArgs: [] };
405
+ } else if (namespace === 'Appliance.RollerShutter.Config' && typeof device.getRollerShutterConfig === 'function') {
406
+ return { featureMethod: device.getRollerShutterConfig.bind(device), featureArgs: [] };
407
+ }
408
+ return null;
409
+ }
410
+
411
+ /**
412
+ * Feature namespace mapping registry organized by feature type.
413
+ * Matches the MerossManager class structure where features are organized by type.
414
+ */
415
+ const FEATURE_NAMESPACE_MAPPERS = [
416
+ { prefix: 'Appliance.Control.Toggle', mapper: _mapToggleFeatureNamespace },
417
+ { prefix: 'Appliance.Control.Light', mapper: _mapLightFeatureNamespace },
418
+ { prefix: 'Appliance.Control.Diffuser.', mapper: _mapDiffuserFeatureNamespace },
419
+ { prefix: 'Appliance.Control.Spray', mapper: _mapSprayFeatureNamespace },
420
+ { prefix: 'Appliance.Control.Electricity', mapper: _mapElectricityFeatureNamespace },
421
+ { prefix: 'Appliance.Control.Consumption', mapper: _mapConsumptionFeatureNamespace },
422
+ { prefix: 'Appliance.Control.Timer', mapper: _mapTimerFeatureNamespace },
423
+ { prefix: 'Appliance.Control.Trigger', mapper: _mapTriggerFeatureNamespace },
424
+ { prefix: 'Appliance.Control.Sensor.', mapper: _mapSensorFeatureNamespace },
425
+ { prefix: 'Appliance.Control.Thermostat.', mapper: _mapThermostatNamespaceToFeatureMethod },
426
+ { prefix: 'Appliance.GarageDoor.', mapper: _mapGarageFeatureNamespace },
427
+ { prefix: 'Appliance.RollerShutter.', mapper: _mapRollerShutterFeatureNamespace },
428
+ { prefix: 'Appliance.Control.', mapper: _mapSharedControlNamespace },
429
+ { prefix: 'Appliance.System.', mapper: _mapSystemNamespaceToFeatureMethod },
430
+ { prefix: 'Appliance.Digest.', mapper: _mapDigestNamespaceToFeatureMethod }
431
+ ];
432
+
433
+ /**
434
+ * Maps a namespace to its feature method and arguments.
435
+ * Returns { featureMethod, featureArgs } or { featureMethod: null, featureArgs: [] } if no feature method exists.
436
+ * Organized by feature type to match MerossManager class structure.
437
+ */
438
+ function _mapNamespaceToFeatureMethod(namespace, device) {
439
+ for (const { prefix, mapper } of FEATURE_NAMESPACE_MAPPERS) {
440
+ if (namespace.startsWith(prefix)) {
441
+ const result = mapper(namespace, device);
442
+ if (result) {
443
+ return result;
444
+ }
445
+ }
446
+ }
447
+ return { featureMethod: null, featureArgs: [] };
448
+ }
449
+
450
+ /**
451
+ * Collects toggle states from device cache.
452
+ */
453
+ function _collectToggleStates(device) {
454
+ const toggleStatesByChannel = new Map();
455
+ if (typeof device.getAllCachedToggleStates === 'function') {
456
+ const allToggleStates = device.getAllCachedToggleStates();
457
+ if (allToggleStates && allToggleStates.size > 0) {
458
+ allToggleStates.forEach((state, channel) => {
459
+ if (state && typeof state.isOn !== 'undefined') {
460
+ toggleStatesByChannel.set(channel, state.isOn);
461
+ }
462
+ });
463
+ }
464
+ }
465
+ return toggleStatesByChannel;
466
+ }
467
+
468
+ /**
469
+ * Handles Light namespace display.
470
+ */
471
+ function _handleLightNamespace(device, sensorLines) {
472
+ if (typeof device.getCachedLightState === 'function') {
473
+ const lightState = device.getCachedLightState(0);
474
+ if (lightState) {
475
+ const isOn = typeof device.getLightIsOn === 'function' ? device.getLightIsOn(0) : lightState.isOn;
476
+ const stateColor = isOn ? chalk.green('On') : chalk.red('Off');
477
+ sensorLines.push(` ${chalk.white.bold('Light State')}: ${chalk.italic(stateColor)}`);
478
+
479
+ if (lightState.luminance !== undefined && lightState.luminance !== null) {
480
+ sensorLines.push(` ${chalk.white.bold('Brightness')}: ${chalk.italic(`${lightState.luminance}%`)}`);
481
+ }
482
+ return { handled: true, hasReadings: true };
483
+ }
484
+ }
485
+ return { handled: false, hasReadings: false };
486
+ }
487
+
488
+ /**
489
+ * Handles Toggle namespace display.
490
+ */
491
+ function _handleToggleNamespace(device, toggleStatesByChannel) {
492
+ if (typeof device.getAllCachedToggleStates === 'function') {
493
+ const allToggleStates = device.getAllCachedToggleStates();
494
+ if (allToggleStates && allToggleStates.size > 0) {
495
+ allToggleStates.forEach((state, channel) => {
496
+ if (state && typeof state.isOn !== 'undefined') {
497
+ toggleStatesByChannel.set(channel, state.isOn);
498
+ }
499
+ });
500
+ }
501
+ }
502
+ return { handled: true };
503
+ }
504
+
505
+ /**
506
+ * Handles Electricity namespace display.
507
+ */
508
+ function _handleElectricityNamespace(device, sensorLines) {
509
+ if (typeof device.getCachedElectricity === 'function') {
510
+ const powerInfo = device.getCachedElectricity(0);
511
+ if (powerInfo) {
512
+ sensorLines.push(` ${chalk.white.bold('Power')}: ${chalk.italic(`${powerInfo.wattage.toFixed(2)} W`)}`);
513
+ if (powerInfo.voltage !== undefined) {
514
+ sensorLines.push(` ${chalk.white.bold('Voltage')}: ${chalk.italic(`${powerInfo.voltage.toFixed(1)} V`)}`);
515
+ }
516
+ if (powerInfo.amperage !== undefined) {
517
+ sensorLines.push(` ${chalk.white.bold('Current')}: ${chalk.italic(`${powerInfo.amperage.toFixed(3)} A`)}`);
518
+ }
519
+ return { handled: true, hasReadings: true, hasElectricity: true };
520
+ }
521
+ }
522
+ return { handled: false, hasReadings: false, hasElectricity: false };
523
+ }
524
+
525
+ /**
526
+ * Displays thermostat temperature and warning info.
527
+ */
528
+ function _displayThermostatTemperatureInfo(thermostatState, sensorLines) {
529
+ const currentTemp = thermostatState.currentTemperatureCelsius;
530
+ if (currentTemp !== undefined && currentTemp !== null) {
531
+ sensorLines.push(` ${chalk.white.bold('Temperature')}: ${chalk.italic(`${currentTemp.toFixed(1)}°C`)}`);
532
+ }
533
+ const targetTemp = thermostatState.targetTemperatureCelsius;
534
+ if (targetTemp !== undefined && targetTemp !== null) {
535
+ sensorLines.push(` ${chalk.white.bold('Target Temperature')}: ${chalk.italic(`${targetTemp.toFixed(1)}°C`)}`);
536
+ }
537
+ if (thermostatState.warning) {
538
+ sensorLines.push(` ${chalk.white.bold('Warning')}: ${chalk.italic('Active')}`);
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Handles Thermostat.Mode namespace display.
544
+ */
545
+ function _handleThermostatModeNamespace(device, sensorLines) {
546
+ if (typeof device.getCachedThermostatState === 'function') {
547
+ const thermostatState = device.getCachedThermostatState(0);
548
+ if (thermostatState) {
549
+ _displayThermostatTemperatureInfo(thermostatState, sensorLines);
550
+ return { handled: true, hasReadings: true };
551
+ }
552
+ }
553
+ return { handled: false, hasReadings: false };
554
+ }
555
+
556
+ /**
557
+ * Handles Thermostat.ModeB namespace display.
558
+ */
559
+ function _handleThermostatModeBNamespace(device, sensorLines) {
560
+ if (typeof device.getCachedThermostatModeBState === 'function') {
561
+ const thermostatState = device.getCachedThermostatModeBState(0);
562
+ if (thermostatState) {
563
+ _displayThermostatTemperatureInfo(thermostatState, sensorLines);
564
+ return { handled: true, hasReadings: true };
565
+ }
566
+ }
567
+ return { handled: false, hasReadings: false };
568
+ }
569
+
570
+ /**
571
+ * Handles Thermostat.WindowOpened namespace display.
572
+ */
573
+ function _handleThermostatWindowOpenedNamespace(response, sensorLines) {
574
+ if (response.windowOpened) {
575
+ const wo = response.windowOpened[0];
576
+ if (wo.status !== undefined) {
577
+ sensorLines.push(` ${chalk.white.bold('Window Opened')}: ${chalk.italic(wo.status === 1 ? 'Open' : 'Closed')}`);
578
+ return { handled: true, hasReadings: true };
579
+ }
580
+ }
581
+ return { handled: false, hasReadings: false };
582
+ }
583
+
584
+ /**
585
+ * Handles Thermostat.Overheat namespace display.
586
+ */
587
+ function _handleThermostatOverheatNamespace(response, sensorLines) {
588
+ if (response.overheat) {
589
+ const oh = response.overheat[0];
590
+ let hasReadings = false;
591
+ if (oh.currentTemp !== undefined) {
592
+ sensorLines.push(` ${chalk.white.bold('External Sensor')}: ${chalk.italic(`${(oh.currentTemp / 10.0).toFixed(1)}°C`)}`);
593
+ hasReadings = true;
594
+ }
595
+ if (oh.warning !== undefined && oh.warning === 1) {
596
+ sensorLines.push(` ${chalk.white.bold('Overheat Warning')}: ${chalk.italic('Active')}`);
597
+ }
598
+ return { handled: true, hasReadings };
599
+ }
600
+ return { handled: false, hasReadings: false };
601
+ }
602
+
603
+ /**
604
+ * Handles Thermostat.Calibration namespace display.
605
+ */
606
+ function _handleThermostatCalibrationNamespace(response, sensorLines) {
607
+ if (response.calibration) {
608
+ const cal = response.calibration[0];
609
+ if (cal.humiValue !== undefined) {
610
+ sensorLines.push(` ${chalk.white.bold('Sensor Humidity')}: ${chalk.italic(`${(cal.humiValue / 10.0).toFixed(1)}%`)}`);
611
+ return { handled: true, hasReadings: true };
612
+ }
613
+ }
614
+ return { handled: false, hasReadings: false };
615
+ }
616
+
617
+ /**
618
+ * Handles Thermostat.Frost namespace display.
619
+ */
620
+ function _handleThermostatFrostNamespace(response, sensorLines) {
621
+ if (response.frost) {
622
+ const frost = response.frost[0];
623
+ if (frost.warning !== undefined && frost.warning === 1) {
624
+ sensorLines.push(` ${chalk.white.bold('Frost Warning')}: ${chalk.italic('Active')}`);
625
+ }
626
+ return { handled: true, hasReadings: false };
627
+ }
628
+ return { handled: false, hasReadings: false };
629
+ }
630
+
631
+ /**
632
+ * Handles Thermostat namespace display.
633
+ */
634
+ function _handleThermostatNamespace(namespace, response, device, sensorLines) {
635
+ if (namespace === 'Appliance.Control.Thermostat.Mode') {
636
+ return _handleThermostatModeNamespace(device, sensorLines);
637
+ } else if (namespace === 'Appliance.Control.Thermostat.ModeB') {
638
+ return _handleThermostatModeBNamespace(device, sensorLines);
639
+ } else if (namespace === 'Appliance.Control.Thermostat.WindowOpened') {
640
+ return _handleThermostatWindowOpenedNamespace(response, sensorLines);
641
+ } else if (namespace === 'Appliance.Control.Thermostat.Overheat') {
642
+ return _handleThermostatOverheatNamespace(response, sensorLines);
643
+ } else if (namespace === 'Appliance.Control.Thermostat.Calibration') {
644
+ return _handleThermostatCalibrationNamespace(response, sensorLines);
645
+ } else if (namespace === 'Appliance.Control.Thermostat.Frost') {
646
+ return _handleThermostatFrostNamespace(response, sensorLines);
647
+ }
648
+ return { handled: false, hasReadings: false };
649
+ }
650
+
651
+ /**
652
+ * Handles a specific sensor namespace and adds display lines.
653
+ * Returns { handled: boolean, sensorLines: string[], hasReadings: boolean, hasElectricity: boolean }
654
+ */
655
+ function _handleSensorNamespace(namespace, response, device, sensorLines, toggleStatesByChannel) {
656
+ // Handle System.All digest data (state is already cached by device.handleMessage)
657
+ if (namespace === 'Appliance.System.All' && response.all && response.all.digest) {
658
+ // Toggle states are already cached from handleMessage
659
+ if (typeof device.getAllCachedToggleStates === 'function') {
660
+ const allToggleStates = device.getAllCachedToggleStates();
661
+ if (allToggleStates && allToggleStates.size > 0) {
662
+ allToggleStates.forEach((state, channel) => {
663
+ if (state && typeof state.isOn !== 'undefined') {
664
+ toggleStatesByChannel.set(channel, state.isOn);
665
+ }
666
+ });
667
+ }
668
+ }
669
+ return { handled: true, sensorLines, hasReadings: false, hasElectricity: false };
670
+ }
671
+
672
+ // Filter out System namespaces that aren't useful for status display
673
+ if (namespace.startsWith('Appliance.System.')) {
674
+ return { handled: true, sensorLines, hasReadings: false, hasElectricity: false };
675
+ }
676
+
677
+ // Handle specific namespaces
678
+ if (namespace === 'Appliance.Control.Light') {
679
+ const result = _handleLightNamespace(device, sensorLines);
680
+ return { ...result, sensorLines, hasElectricity: false };
681
+ } else if (namespace === 'Appliance.Control.ToggleX' || namespace === 'Appliance.Control.Toggle') {
682
+ const result = _handleToggleNamespace(device, toggleStatesByChannel);
683
+ return { ...result, sensorLines, hasReadings: false, hasElectricity: false };
684
+ } else if (namespace === 'Appliance.Control.Electricity') {
685
+ const result = _handleElectricityNamespace(device, sensorLines);
686
+ return { ...result, sensorLines };
687
+ } else if (namespace.startsWith('Appliance.Control.Thermostat.')) {
688
+ const result = _handleThermostatNamespace(namespace, response, device, sensorLines);
689
+ return { ...result, sensorLines, hasElectricity: false };
690
+ }
691
+
692
+ return { handled: false, sensorLines, hasReadings: false, hasElectricity: false };
693
+ }
694
+
695
+ /**
696
+ * Formats Consumption namespaces for display.
697
+ */
698
+ function _formatConsumptionNamespace(namespace, response, sensorLines) {
699
+ if (namespace === 'Appliance.Control.ConsumptionX' && response.consumptionx) {
700
+ const consumption = Array.isArray(response.consumptionx) ? response.consumptionx : [response.consumptionx];
701
+ if (consumption.length > 0) {
702
+ const latest = consumption[consumption.length - 1];
703
+ if (latest.value !== undefined && latest.value !== null) {
704
+ const valueKwh = (latest.value / 1000.0).toFixed(2);
705
+ sensorLines.push(` ${chalk.white.bold('Consumption')}: ${chalk.italic(`${valueKwh} kWh`)}`);
706
+ } else {
707
+ sensorLines.push(` ${chalk.white.bold('Consumption')}: ${chalk.italic('N/A')}`);
708
+ }
709
+ return { formatted: true, hasReadings: true };
710
+ }
711
+ } else if (namespace === 'Appliance.Control.ConsumptionH' && response.consumptionH) {
712
+ // Skip hourly consumption - only show daily
713
+ return { formatted: true, hasReadings: false };
714
+ } else if (namespace === 'Appliance.Control.ConsumptionConfig' && response.config) {
715
+ const { config } = response;
716
+ const configLines = [];
717
+ if (config.voltageRatio !== undefined) {
718
+ configLines.push(`voltageRatio: ${config.voltageRatio}`);
719
+ }
720
+ if (config.electricityRatio !== undefined) {
721
+ configLines.push(`electricityRatio: ${config.electricityRatio}`);
722
+ }
723
+ if (config.maxElectricityCurrent !== undefined) {
724
+ configLines.push(`maxCurrent: ${(config.maxElectricityCurrent / 1000.0).toFixed(1)} A`);
725
+ }
726
+ if (configLines.length > 0) {
727
+ sensorLines.push(` ${chalk.white.bold('Consumption Config')}: ${chalk.italic(configLines.join(', '))}`);
728
+ return { formatted: true, hasReadings: true };
729
+ }
730
+ }
731
+ return { formatted: false, hasReadings: false };
732
+ }
733
+
734
+ /**
735
+ * Formats Digest namespaces for display.
736
+ */
737
+ function _formatDigestNamespace(namespace, response, sensorLines) {
738
+ if (namespace === 'Appliance.Digest.TimerX' && response.digest) {
739
+ const timers = Array.isArray(response.digest) ? response.digest : (response.digest ? [response.digest] : []);
740
+ sensorLines.push(` ${chalk.white.bold('Timers')}: ${chalk.italic(`${timers.length} active`)}`);
741
+ return { formatted: true, hasReadings: true };
742
+ } else if (namespace === 'Appliance.Digest.TriggerX' && response.digest) {
743
+ const triggers = Array.isArray(response.digest) ? response.digest : (response.digest ? [response.digest] : []);
744
+ sensorLines.push(` ${chalk.white.bold('Triggers')}: ${chalk.italic(`${triggers.length} active`)}`);
745
+ return { formatted: true, hasReadings: true };
746
+ }
747
+ return { formatted: false, hasReadings: false };
748
+ }
749
+
750
+ /**
751
+ * Formats Sensor.LatestX namespace for display.
752
+ */
753
+ function _formatSensorLatestXNamespace(device, sensorLines) {
754
+ let formatted = false;
755
+ let hasReadings = false;
756
+
757
+ // Use device methods to get formatted presence sensor data
758
+ if (typeof device.getPresence === 'function') {
759
+ const presence = device.getPresence();
760
+ if (presence) {
761
+ const presenceState = presence.isPresent ? chalk.green('Present') : chalk.yellow('Absent');
762
+ sensorLines.push(` ${chalk.white.bold('Presence')}: ${chalk.italic(presenceState)}`);
763
+
764
+ if (presence.distance !== null && presence.distance !== undefined) {
765
+ sensorLines.push(` ${chalk.white.bold('Distance')}: ${chalk.italic(`${presence.distance.toFixed(2)} m`)}`);
766
+ }
767
+
768
+ if (presence.timestamp) {
769
+ sensorLines.push(` ${chalk.white.bold('Last Detection')}: ${chalk.italic(presence.timestamp.toLocaleString())}`);
770
+ }
771
+
772
+ formatted = true;
773
+ hasReadings = true;
774
+ }
775
+ }
776
+
777
+ if (typeof device.getLight === 'function') {
778
+ const light = device.getLight();
779
+ if (light && light.value !== undefined) {
780
+ sensorLines.push(` ${chalk.white.bold('Light')}: ${chalk.italic(`${light.value} lx`)}`);
781
+ formatted = true;
782
+ hasReadings = true;
783
+ }
784
+ }
785
+
786
+ return { formatted, hasReadings };
787
+ }
788
+
789
+ /**
790
+ * Formats unknown sensor namespaces for display.
791
+ * Returns { formatted: boolean, sensorLines: string[], hasReadings: boolean }
792
+ */
793
+ function _formatUnknownSensorNamespace(namespace, response, device, sensorLines) {
794
+ // Try Consumption namespaces
795
+ if (namespace.startsWith('Appliance.Control.Consumption')) {
796
+ const result = _formatConsumptionNamespace(namespace, response, sensorLines);
797
+ if (result.formatted) {
798
+ return { ...result, sensorLines };
799
+ }
800
+ }
801
+ // Try Digest namespaces
802
+ if (namespace.startsWith('Appliance.Digest.')) {
803
+ const result = _formatDigestNamespace(namespace, response, sensorLines);
804
+ if (result.formatted) {
805
+ return { ...result, sensorLines };
806
+ }
807
+ }
808
+ // Try Sensor.LatestX
809
+ if (namespace === 'Appliance.Control.Sensor.LatestX') {
810
+ const result = _formatSensorLatestXNamespace(device, sensorLines);
811
+ return { ...result, sensorLines };
812
+ }
813
+
814
+ return { formatted: false, sensorLines, hasReadings: false };
815
+ }
816
+
817
+ /**
818
+ * Displays toggle states for all channels.
819
+ */
820
+ function _displayToggleStates(device, toggleStatesByChannel, hasElectricity, sensorLines) {
821
+ const deviceChannels = device.channels && device.channels.length > 0 ? device.channels : [];
822
+ const hasChannels = deviceChannels.length > 0;
823
+ const hasToggleStates = toggleStatesByChannel.size > 0;
824
+
825
+ if (!hasToggleStates && !(hasChannels && typeof device.getAllCachedToggleStates === 'function')) {
826
+ return;
827
+ }
828
+
829
+ const baseLabel = hasElectricity ? 'State' : 'Power';
830
+ let channelsToDisplay = [];
831
+ let toggleStatesMap = toggleStatesByChannel;
832
+
833
+ if (hasToggleStates) {
834
+ // Use channels from toggle states (channels with actual state data)
835
+ channelsToDisplay = Array.from(toggleStatesByChannel.keys()).sort((a, b) => a - b);
836
+ } else if (hasChannels) {
837
+ // Fallback: use device.channels structure if no toggle states yet
838
+ channelsToDisplay = deviceChannels.map(ch => ch.index);
839
+ } else if (typeof device.getAllCachedToggleStates === 'function') {
840
+ // Last resort: check cached toggle state
841
+ const allToggleStates = device.getAllCachedToggleStates();
842
+ if (allToggleStates && allToggleStates.size > 0) {
843
+ channelsToDisplay = Array.from(allToggleStates.keys()).sort((a, b) => a - b);
844
+ // Build map from cached states
845
+ toggleStatesMap = new Map();
846
+ allToggleStates.forEach((state, channel) => {
847
+ if (state && typeof state.isOn !== 'undefined') {
848
+ toggleStatesMap.set(channel, state.isOn);
849
+ }
850
+ });
851
+ }
852
+ }
853
+
854
+ if (channelsToDisplay.length > 0) {
855
+ // Determine format based on device channel structure (not just state keys)
856
+ const isSingleChannel = hasChannels ? deviceChannels.length === 1 : (channelsToDisplay.length === 1 && channelsToDisplay[0] === 0);
857
+
858
+ if (isSingleChannel) {
859
+ // Single channel device - show simple format
860
+ const channelIndex = channelsToDisplay[0];
861
+ const state = toggleStatesMap.get(channelIndex);
862
+ const stateColor = state ? chalk.green('On') : chalk.red('Off');
863
+ sensorLines.push(` ${chalk.white.bold(baseLabel)}: ${chalk.italic(stateColor)}`);
864
+ } else {
865
+ // Multi-channel device - show all channels with "Socket X Power:" format
866
+ channelsToDisplay.forEach(channelIndex => {
867
+ const state = toggleStatesMap.get(channelIndex);
868
+ const stateColor = state ? chalk.green('On') : chalk.red('Off');
869
+ sensorLines.push(` ${chalk.white.bold(`Socket ${channelIndex} ${baseLabel}`)}: ${chalk.italic(stateColor)}`);
870
+ });
871
+ }
872
+ }
873
+ }
874
+
875
+ /**
876
+ * Maps a config namespace to its feature method and arguments.
877
+ */
878
+ function _mapConfigNamespaceToFeatureMethod(namespace, device) {
879
+ if (namespace === 'Appliance.Control.Presence.Config' && typeof device.getPresenceConfig === 'function') {
880
+ return { featureMethod: device.getPresenceConfig.bind(device), featureArgs: [0] };
881
+ } else if (namespace === 'Appliance.Config.OverTemp' && typeof device.getConfigOverTemp === 'function') {
882
+ return { featureMethod: device.getConfigOverTemp.bind(device), featureArgs: [] };
883
+ } else if (namespace === 'Appliance.Config.WifiList' && typeof device.getConfigWifiList === 'function') {
884
+ return { featureMethod: device.getConfigWifiList.bind(device), featureArgs: [] };
885
+ } else if (namespace === 'Appliance.Config.Trace' && typeof device.getConfigTrace === 'function') {
886
+ return { featureMethod: device.getConfigTrace.bind(device), featureArgs: [] };
887
+ } else if (namespace === 'Appliance.System.DNDMode' && typeof device.getDNDMode === 'function') {
888
+ return { featureMethod: device.getRawDNDMode.bind(device), featureArgs: [] };
889
+ } else if (namespace === 'Appliance.System.LedMode' && typeof device.getSystemLedMode === 'function') {
890
+ return { featureMethod: device.getSystemLedMode.bind(device), featureArgs: [] };
891
+ } else if (namespace === 'Appliance.Mcu.Firmware' && typeof device.getMcuFirmware === 'function') {
892
+ return { featureMethod: device.getMcuFirmware.bind(device), featureArgs: [] };
893
+ } else if (namespace === 'Appliance.Encrypt.Suite' && typeof device.getEncryptSuite === 'function') {
894
+ return { featureMethod: device.getEncryptSuite.bind(device), featureArgs: [] };
895
+ } else if (namespace === 'Appliance.Encrypt.ECDHE' && typeof device.getEncryptECDHE === 'function') {
896
+ return { featureMethod: device.getEncryptECDHE.bind(device), featureArgs: [] };
897
+ } else if (namespace === 'Appliance.Control.Smoke.Config' && typeof device.getSmokeConfig === 'function') {
898
+ return { featureMethod: device.getSmokeConfig.bind(device), featureArgs: [0] };
899
+ } else if (namespace === 'Appliance.Control.TempUnit' && typeof device.getTempUnit === 'function') {
900
+ return { featureMethod: device.getTempUnit.bind(device), featureArgs: [0] };
901
+ } else if (namespace === 'Appliance.Control.Presence.Study' && typeof device.getPresenceStudy === 'function') {
902
+ return { featureMethod: device.getPresenceStudy.bind(device), featureArgs: [] };
903
+ }
904
+ return { featureMethod: null, featureArgs: [] };
905
+ }
906
+
907
+ async function displayDeviceStatus(device) {
908
+ let hasReadings = false;
909
+
910
+ // Auto-detect and display sensors and configuration based on device abilities
911
+ // Abilities are already loaded at device creation (single-phase initialization)
912
+ if (device.deviceConnected && typeof device.publishMessage === 'function') {
913
+ try {
914
+ // If device is connected via MQTT, wait briefly for push notifications to arrive
915
+ // This allows real-time updates to be processed before we poll
916
+ const isMqttConnected = device.deviceConnected && device.mqttHost;
917
+ if (isMqttConnected) {
918
+ await new Promise(resolve => setTimeout(resolve, 300));
919
+ }
920
+
921
+ const abilities = device.abilities || {};
922
+ const dangerousNamespaces = _getDangerousNamespaces();
923
+ const allNamespaces = _getAllNamespaces(abilities, dangerousNamespaces);
924
+ const { sensorNamespaces, configNamespaces } = _categorizeNamespaces(allNamespaces);
925
+ const namespacesToFetch = _filterNamespacesToFetch(sensorNamespaces, isMqttConnected, device);
926
+
927
+ // Create promise factories (functions that return promises) instead of creating promises directly
928
+ // This allows us to throttle requests by executing them in batches
929
+ const promiseFactories = [];
930
+
931
+ // Always fetch System.All to get digest data (contains togglex, timers, etc.)
932
+ if (abilities['Appliance.System.All']) {
933
+ promiseFactories.push(() =>
934
+ device.getSystemAllData()
935
+ .then(response => ({ namespace: 'Appliance.System.All', response, type: 'sensor' }))
936
+ .catch(error => ({ namespace: 'Appliance.System.All', error: error.message, type: 'sensor', success: false }))
937
+ );
938
+ }
939
+
940
+ // Add sensor namespace fetches (only for namespaces that need polling)
941
+ // Use device feature methods when available - they handle payload format correctly and update state
942
+ for (const namespace of namespacesToFetch) {
943
+ const { featureMethod, featureArgs } = _mapNamespaceToFeatureMethod(namespace, device);
944
+
945
+ if (featureMethod) {
946
+ // Use feature method - it handles payload format correctly and updates state
947
+ promiseFactories.push(() =>
948
+ featureMethod(...featureArgs)
949
+ .then(response => {
950
+ // For Electricity, convert back to original format for display
951
+ if (namespace === 'Appliance.Control.Electricity' && response && typeof response === 'object' && response.wattage !== undefined) {
952
+ return { namespace, response: { electricity: { power: response.wattage * 1000, voltage: response.voltage * 10, current: response.amperage * 1000 } }, type: 'sensor' };
953
+ }
954
+ // For DNDMode, convert to expected format
955
+ if (namespace === 'Appliance.System.DNDMode' && typeof response === 'number') {
956
+ return { namespace, response: { DNDMode: { mode: response } }, type: 'sensor' };
957
+ }
958
+ // For System.Runtime, updateRuntimeInfo returns just the runtime data, wrap it
959
+ if (namespace === 'Appliance.System.Runtime' && response && typeof response === 'object' && !response.runtime) {
960
+ return { namespace, response: { runtime: response }, type: 'sensor' };
961
+ }
962
+ return { namespace, response, type: 'sensor' };
963
+ })
964
+ .catch(error => ({ namespace, error: error.message, type: 'sensor', success: false }))
965
+ );
966
+ } else {
967
+ // Fall back to raw publishMessage for namespaces without feature methods
968
+ const payload = _buildPayloadForNamespace(namespace);
969
+ if (payload !== null) {
970
+ promiseFactories.push(() =>
971
+ device.publishMessage('GET', namespace, payload)
972
+ .then(response => ({ namespace, response, type: 'sensor' }))
973
+ .catch(error => ({ namespace, error: error.message, type: 'sensor', success: false }))
974
+ );
975
+ }
976
+ // If payload is null, skip this namespace (intentionally skipped)
977
+ }
978
+ }
979
+
980
+ // Add configuration namespace fetches
981
+ // Use device feature methods when available - they handle payload format correctly
982
+ for (const namespace of configNamespaces) {
983
+ const { featureMethod, featureArgs } = _mapConfigNamespaceToFeatureMethod(namespace, device);
984
+
985
+ if (featureMethod) {
986
+ // Use feature method - it handles payload format correctly
987
+ promiseFactories.push(() =>
988
+ featureMethod(...featureArgs)
989
+ .then(response => {
990
+ // For DNDMode, convert to expected format
991
+ if (namespace === 'Appliance.System.DNDMode' && typeof response === 'number') {
992
+ return { namespace, response: { DNDMode: { mode: response } }, type: 'config' };
993
+ }
994
+ return { namespace, response, type: 'config' };
995
+ })
996
+ .catch(error => ({ namespace, error: error.message, type: 'config', success: false }))
997
+ );
998
+ } else {
999
+ // Fall back to raw publishMessage for namespaces without feature methods
1000
+ promiseFactories.push(() =>
1001
+ device.publishMessage('GET', namespace, {})
1002
+ .then(response => ({ namespace, response, type: 'config' }))
1003
+ .catch(error => ({ namespace, error: error.message, type: 'config', success: false }))
1004
+ );
1005
+ }
1006
+ }
1007
+
1008
+ // Show loading spinner while fetching data
1009
+ const spinner = promiseFactories.length > 0 ? ora('Fetching device data').start() : null;
1010
+
1011
+ // Execute all promise factories - throttling is handled by core library (requestMessage)
1012
+ const allPromises = promiseFactories.map(factory => factory());
1013
+ const allResults = await Promise.allSettled(allPromises);
1014
+
1015
+ if (spinner) {
1016
+ spinner.stop();
1017
+ }
1018
+ const successfulSensors = allResults
1019
+ .filter(result => result.status === 'fulfilled' && result.value.type === 'sensor' && !result.value.error)
1020
+ .map(result => result.value);
1021
+
1022
+ const successfulConfigs = allResults
1023
+ .filter(result => result.status === 'fulfilled' && result.value.type === 'config' && !result.value.error)
1024
+ .map(result => result.value);
1025
+
1026
+ // Display sensors
1027
+ let sensorDataDisplayed = false;
1028
+ const sensorLines = [];
1029
+ const controlFeatureLines = []; // Separate list for control features
1030
+ const unknownSensorNamespaces = [];
1031
+ let hasElectricity = false;
1032
+ const toggleStatesByChannel = _collectToggleStates(device);
1033
+
1034
+ if (successfulSensors.length > 0) {
1035
+ for (const { namespace, response } of successfulSensors) {
1036
+ const result = _handleSensorNamespace(namespace, response, device, sensorLines, toggleStatesByChannel);
1037
+ if (result.handled) {
1038
+ if (result.hasReadings) {
1039
+ sensorDataDisplayed = true;
1040
+ hasReadings = true;
1041
+ }
1042
+ if (result.hasElectricity) {
1043
+ hasElectricity = true;
1044
+ }
1045
+ continue;
1046
+ }
1047
+
1048
+ // If not handled by known formatters, add to unknown list for generic display
1049
+ if (response && Object.keys(response).length > 0) {
1050
+ unknownSensorNamespaces.push({ namespace, response });
1051
+ }
1052
+ }
1053
+
1054
+ // Display unknown sensor namespaces generically
1055
+ for (const { namespace, response } of unknownSensorNamespaces) {
1056
+ const result = _formatUnknownSensorNamespace(namespace, response, device, sensorLines);
1057
+ if (result.formatted) {
1058
+ if (result.hasReadings) {
1059
+ sensorDataDisplayed = true;
1060
+ hasReadings = true;
1061
+ }
1062
+ } else {
1063
+ // Control/configuration namespaces that aren't formatted go to a separate list
1064
+ const shortName = namespace.replace('Appliance.', '');
1065
+ // Only add control namespaces (not sensor namespaces) to control features
1066
+ if (namespace.includes('Control.') || namespace.includes('Digest.')) {
1067
+ controlFeatureLines.push(shortName);
1068
+ } else {
1069
+ // For unknown sensor namespaces, still show them but in status
1070
+ sensorLines.push(` ${chalk.white.bold(shortName)}: ${chalk.italic('(data available, use verbose mode to view)')}`);
1071
+ sensorDataDisplayed = true;
1072
+ hasReadings = true;
1073
+ }
1074
+ }
1075
+ }
1076
+ }
1077
+
1078
+ // Also check cached presence sensor state if available
1079
+ if (!sensorDataDisplayed && typeof device.getPresence === 'function') {
1080
+ const presence = device.getPresence();
1081
+ if (presence) {
1082
+ const presenceState = presence.isPresent ? chalk.green('Present') : chalk.yellow('Absent');
1083
+ sensorLines.push(` ${chalk.white.bold('Presence')}: ${chalk.italic(presenceState)}`);
1084
+
1085
+ if (presence.distance !== null) {
1086
+ sensorLines.push(` ${chalk.white.bold('Distance')}: ${chalk.italic(`${presence.distance.toFixed(2)} m`)}`);
1087
+ }
1088
+
1089
+ if (presence.timestamp) {
1090
+ sensorLines.push(` ${chalk.white.bold('Last Detection')}: ${chalk.italic(presence.timestamp.toLocaleString())}`);
1091
+ }
1092
+
1093
+ sensorDataDisplayed = true;
1094
+ hasReadings = true;
1095
+ }
1096
+
1097
+ const light = device.getLight && device.getLight();
1098
+ if (light && light.value !== undefined) {
1099
+ sensorLines.push(` ${chalk.white.bold('Light')}: ${chalk.italic(`${light.value} lx`)}`);
1100
+ sensorDataDisplayed = true;
1101
+ hasReadings = true;
1102
+ }
1103
+ }
1104
+
1105
+ // Display toggle states for all channels
1106
+ _displayToggleStates(device, toggleStatesByChannel, hasElectricity, sensorLines);
1107
+ if (sensorLines.length > 0 && !sensorDataDisplayed) {
1108
+ sensorDataDisplayed = true;
1109
+ hasReadings = true;
1110
+ }
1111
+
1112
+ // Display status if we have any data
1113
+ if (sensorDataDisplayed && sensorLines.length > 0) {
1114
+ console.log(`\n ${chalk.bold.underline('Status')}`);
1115
+ sensorLines.forEach(line => console.log(line));
1116
+ }
1117
+
1118
+ // Optionally show control features in verbose mode (hidden by default for cleaner output)
1119
+ // Control features are available but not displayed as they don't show actual status values
1120
+
1121
+ // Display configuration
1122
+ const hasThermostatOrElectricity = successfulSensors.some(s =>
1123
+ s.namespace.includes('Thermostat') || s.namespace === 'Appliance.Control.Electricity'
1124
+ );
1125
+
1126
+ if (successfulConfigs.length > 0 || (successfulSensors.length > 0 && hasThermostatOrElectricity)) {
1127
+ const configSectionShown = hasReadings && device instanceof MerossHubDevice;
1128
+
1129
+ // Collect all configuration items first for consistent alignment
1130
+ const allConfigItems = [];
1131
+
1132
+ // Display thermostat configuration from cached state
1133
+ if (typeof device.getCachedThermostatState === 'function') {
1134
+ const thermostatState = device.getCachedThermostatState(0);
1135
+ if (thermostatState) {
1136
+ const configInfo = [];
1137
+
1138
+ // Mode and power state
1139
+ if (thermostatState.mode !== undefined) {
1140
+ const modeNames = {
1141
+ [ThermostatMode.HEAT]: 'Heat',
1142
+ [ThermostatMode.COOL]: 'Cool',
1143
+ [ThermostatMode.ECONOMY]: 'Economy',
1144
+ [ThermostatMode.AUTO]: 'Auto',
1145
+ [ThermostatMode.MANUAL]: 'Manual'
1146
+ };
1147
+ const modeName = modeNames[thermostatState.mode] || `Mode ${thermostatState.mode}`;
1148
+ const onoffStatus = thermostatState.isOn ? chalk.green('On') : chalk.red('Off');
1149
+ const targetTemp = thermostatState.targetTemperatureCelsius !== undefined
1150
+ ? `${thermostatState.targetTemperatureCelsius.toFixed(1)}°C`
1151
+ : '';
1152
+ configInfo.push(['Mode', `${onoffStatus} - ${modeName} ${targetTemp}`.trim()]);
1153
+ }
1154
+
1155
+ // Preset temperatures
1156
+ if (thermostatState.heatTemperatureCelsius !== undefined) {
1157
+ configInfo.push(['Comfort Temperature', `${thermostatState.heatTemperatureCelsius.toFixed(1)}°C`]);
1158
+ }
1159
+ if (thermostatState.coolTemperatureCelsius !== undefined) {
1160
+ configInfo.push(['Cool Temperature', `${thermostatState.coolTemperatureCelsius.toFixed(1)}°C`]);
1161
+ }
1162
+ if (thermostatState.ecoTemperatureCelsius !== undefined) {
1163
+ configInfo.push(['Economy Temperature', `${thermostatState.ecoTemperatureCelsius.toFixed(1)}°C`]);
1164
+ }
1165
+ if (thermostatState.manualTemperatureCelsius !== undefined) {
1166
+ configInfo.push(['Away Temperature', `${thermostatState.manualTemperatureCelsius.toFixed(1)}°C`]);
1167
+ }
1168
+
1169
+ // Temperature range
1170
+ if (thermostatState.minTemperatureCelsius !== undefined && thermostatState.maxTemperatureCelsius !== undefined) {
1171
+ configInfo.push(['Temperature Range', `${thermostatState.minTemperatureCelsius.toFixed(1)}°C - ${thermostatState.maxTemperatureCelsius.toFixed(1)}°C`]);
1172
+ }
1173
+
1174
+ // Heating status (from workingMode or state)
1175
+ if (thermostatState.workingMode !== undefined) {
1176
+ const isHeating = thermostatState.workingMode === 1; // 1 = heating
1177
+ const stateColor = isHeating ? chalk.green('Heating') : chalk.gray.bold('Idle');
1178
+ configInfo.push(['Status', stateColor]);
1179
+ } else if (thermostatState.state !== undefined) {
1180
+ // ModeB state: 1 = heating/cooling active
1181
+ const isActive = thermostatState.state === 1;
1182
+ const stateColor = isActive ? chalk.green('Heating') : chalk.gray.bold('Idle');
1183
+ configInfo.push(['Status', stateColor]);
1184
+ }
1185
+
1186
+ if (configInfo.length > 0) {
1187
+ allConfigItems.push(...configInfo);
1188
+ }
1189
+ }
1190
+ }
1191
+
1192
+ // Also check ModeB if available
1193
+ if (typeof device.getCachedThermostatModeBState === 'function') {
1194
+ const thermostatStateB = device.getCachedThermostatModeBState(0);
1195
+ if (thermostatStateB && thermostatStateB.state !== undefined) {
1196
+ // ModeB state is already displayed in the Mode section above
1197
+ // Only add if Mode wasn't available
1198
+ if (allConfigItems.length === 0 || !allConfigItems.some(item => item[0] === 'Mode')) {
1199
+ const modeBNames = {
1200
+ 0: 'Off',
1201
+ 1: 'Heating/Cooling',
1202
+ 2: 'Auto'
1203
+ };
1204
+ const modeBName = modeBNames[thermostatStateB.state] || `State ${thermostatStateB.state}`;
1205
+ allConfigItems.push(['Mode B', modeBName]);
1206
+ }
1207
+ }
1208
+ }
1209
+
1210
+ // Display other thermostat config from sensor responses (calibration, etc.)
1211
+ if (successfulSensors.length > 0) {
1212
+ for (const { namespace, response } of successfulSensors) {
1213
+ if (namespace === 'Appliance.Control.Thermostat.Calibration' && response.calibration) {
1214
+ const cal = response.calibration[0];
1215
+ if (cal.value !== undefined) {
1216
+ const calibTemp = cal.value > 1000 ? cal.value / 100.0 : cal.value / 10.0;
1217
+ allConfigItems.push(['Calibration', `${calibTemp.toFixed(1)}°C`]);
1218
+ }
1219
+ } else if (namespace === 'Appliance.Control.Thermostat.DeadZone' && response.deadZone) {
1220
+ const dz = response.deadZone[0];
1221
+ if (dz.value !== undefined) {
1222
+ const deadzoneTemp = dz.value > 100 ? dz.value / 100.0 : dz.value / 10.0;
1223
+ allConfigItems.push(['Deadzone', `${deadzoneTemp.toFixed(1)}°C`]);
1224
+ }
1225
+ } else if (namespace === 'Appliance.Control.Thermostat.Sensor' && response.sensor) {
1226
+ const sensor = response.sensor[0];
1227
+ if (sensor.mode !== undefined) {
1228
+ const sensorModeNames = { 0: 'Internal & External', 1: 'External Only', 2: 'Internal Only' };
1229
+ allConfigItems.push(['External sensor mode', sensorModeNames[sensor.mode] || `Mode ${sensor.mode}`]);
1230
+ }
1231
+ } else if (namespace === 'Appliance.Control.Thermostat.Frost' && response.frost) {
1232
+ const frost = response.frost[0];
1233
+ if (frost.onoff !== undefined) {
1234
+ allConfigItems.push(['Frost alarm', frost.onoff === 1 ? chalk.green('On') : chalk.red('Off')]);
1235
+ }
1236
+ if (frost.value !== undefined) {
1237
+ const frostTemp = frost.value > 100 ? frost.value / 100.0 : frost.value / 10.0;
1238
+ allConfigItems.push(['Frost', `${frostTemp.toFixed(1)}°C`]);
1239
+ }
1240
+ } else if (namespace === 'Appliance.Control.Thermostat.Overheat' && response.overheat) {
1241
+ const oh = response.overheat[0];
1242
+ if (oh.onoff !== undefined) {
1243
+ allConfigItems.push(['Overheat alarm', oh.onoff === 1 ? chalk.green('On') : chalk.red('Off')]);
1244
+ }
1245
+ if (oh.value !== undefined) {
1246
+ const overheatTemp = oh.value > 100 ? oh.value / 100.0 : oh.value / 10.0;
1247
+ allConfigItems.push(['Overheat threshold', `${overheatTemp.toFixed(1)}°C`]);
1248
+ }
1249
+ } else if (namespace === 'Appliance.Control.Thermostat.HoldAction' && response.holdAction) {
1250
+ const ha = response.holdAction[0];
1251
+ if (ha.mode !== undefined) {
1252
+ const holdModeNames = { 0: 'permanent', 1: 'effective until next schedule', 2: 'effective at specified time' };
1253
+ allConfigItems.push(['Hold action', holdModeNames[ha.mode] || `Mode ${ha.mode}`]);
1254
+ if (ha.time !== undefined) {
1255
+ allConfigItems.push(['Hold action time', `${ha.time} min`]);
1256
+ }
1257
+ }
1258
+ } else if (namespace === 'Appliance.Control.Thermostat.Schedule' && response.schedule) {
1259
+ const sched = response.schedule[0];
1260
+ const hasSchedule = sched.mon || sched.tue || sched.wed || sched.thu || sched.fri || sched.sat || sched.sun;
1261
+ allConfigItems.push(['Schedule', hasSchedule ? chalk.green('On') : chalk.red('Off')]);
1262
+ } else if (namespace === 'Appliance.Control.Screen.Brightness' && response.brightness) {
1263
+ const bright = response.brightness[0];
1264
+ if (bright.operation !== undefined) {
1265
+ allConfigItems.push(['Screen brightness (active)', `${bright.operation.toFixed(1)}%`]);
1266
+ }
1267
+ if (bright.standby !== undefined) {
1268
+ allConfigItems.push(['Screen brightness (sleep)', `${bright.standby.toFixed(1)}%`]);
1269
+ }
1270
+ } else if (namespace === 'Appliance.Control.PhysicalLock' && response.lock) {
1271
+ const lock = response.lock[0];
1272
+ if (lock.onoff !== undefined) {
1273
+ allConfigItems.push(['Lock', lock.onoff === 1 ? chalk.green('On') : chalk.red('Off')]);
1274
+ }
1275
+ } else if (namespace === 'Appliance.Control.Electricity') {
1276
+ // Use cached electricity data for sample timestamp
1277
+ if (typeof device.getCachedElectricity === 'function') {
1278
+ const powerInfo = device.getCachedElectricity(0);
1279
+ if (powerInfo && powerInfo.sampleTimestamp) {
1280
+ allConfigItems.push(['Sample Time', powerInfo.sampleTimestamp.toISOString()]);
1281
+ }
1282
+ }
1283
+ }
1284
+ }
1285
+ }
1286
+
1287
+ // Display configuration namespaces
1288
+ for (const { namespace, response } of successfulConfigs) {
1289
+ const configKey = namespace.replace('Appliance.Config.', '').replace('Appliance.System.', '');
1290
+
1291
+ // Format configuration display based on namespace
1292
+ if (namespace === 'Appliance.Config.OverTemp' && response.overTemp) {
1293
+ const ot = response.overTemp;
1294
+ allConfigItems.push(['Over-temperature Protection', ot.enable === 1 ? chalk.green('Enabled') : chalk.red('Disabled')]);
1295
+ if (ot.type !== undefined) {
1296
+ const typeNames = { 1: 'Early warning', 2: 'Early warning and shutdown' };
1297
+ allConfigItems.push(['Type', typeNames[ot.type] || `Type ${ot.type}`]);
1298
+ }
1299
+ } else if (namespace === 'Appliance.System.DNDMode' && response.DNDMode) {
1300
+ const dnd = response.DNDMode;
1301
+ allConfigItems.push(['Do Not Disturb', dnd.mode === DNDMode.DND_ENABLED ? chalk.green('Enabled') : chalk.red('Disabled')]);
1302
+ } else if (namespace === 'Appliance.Control.Presence.Config' && response.config) {
1303
+ const configArray = Array.isArray(response.config) ? response.config : [response.config];
1304
+ const config = configArray[0];
1305
+ if (config) {
1306
+ // Work mode
1307
+ if (config.mode && config.mode.workMode !== undefined) {
1308
+ const workModeNames = {
1309
+ [WorkMode.UNKNOWN]: 'Unknown',
1310
+ [WorkMode.BIOLOGICAL_DETECTION_ONLY]: 'Biological Detection Only',
1311
+ [WorkMode.SECURITY]: 'Security'
1312
+ };
1313
+ const workModeName = workModeNames[config.mode.workMode] || `Mode ${config.mode.workMode}`;
1314
+ allConfigItems.push(['Work Mode', workModeName]);
1315
+ }
1316
+
1317
+ // Test mode
1318
+ if (config.mode && config.mode.testMode !== undefined) {
1319
+ allConfigItems.push(['Test Mode', config.mode.testMode === 1 ? chalk.green('Enabled') : chalk.red('Disabled')]);
1320
+ }
1321
+
1322
+ // Sensitivity level
1323
+ if (config.sensitivity && config.sensitivity.level !== undefined) {
1324
+ const sensitivityNames = {
1325
+ [SensitivityLevel.RESPONSIVE]: 'Responsive',
1326
+ [SensitivityLevel.ANTI_INTERFERENCE]: 'Anti-Interference',
1327
+ [SensitivityLevel.BALANCE]: 'Balance'
1328
+ };
1329
+ const sensitivityName = sensitivityNames[config.sensitivity.level] || `Level ${config.sensitivity.level}`;
1330
+ allConfigItems.push(['Sensitivity', sensitivityName]);
1331
+ }
1332
+
1333
+ // Distance threshold
1334
+ if (config.distance && config.distance.value !== undefined) {
1335
+ const distanceMeters = (config.distance.value / 1000).toFixed(2);
1336
+ allConfigItems.push(['Distance Threshold', `${distanceMeters} m`]);
1337
+ }
1338
+
1339
+ // No body time
1340
+ if (config.noBodyTime && config.noBodyTime.time !== undefined) {
1341
+ allConfigItems.push(['No Body Time', `${config.noBodyTime.time} s`]);
1342
+ }
1343
+
1344
+ // Motion thresholds
1345
+ if (config.mthx) {
1346
+ const thresholds = [];
1347
+ if (config.mthx.mth1 !== undefined) {thresholds.push(`MTH1: ${config.mthx.mth1}`);}
1348
+ if (config.mthx.mth2 !== undefined) {thresholds.push(`MTH2: ${config.mthx.mth2}`);}
1349
+ if (config.mthx.mth3 !== undefined) {thresholds.push(`MTH3: ${config.mthx.mth3}`);}
1350
+ if (thresholds.length > 0) {
1351
+ allConfigItems.push(['Motion Thresholds', thresholds.join(', ')]);
1352
+ }
1353
+ }
1354
+ }
1355
+ } else if (namespace === 'Appliance.Config.WifiList' ||
1356
+ namespace === 'Appliance.Config.Trace' ||
1357
+ namespace === 'Appliance.Config.Info') {
1358
+ // Skip these config namespaces - not useful for status display
1359
+ } else {
1360
+ // Generic display for unknown configurations - show summary instead of full JSON
1361
+ const configData = response[configKey] || response;
1362
+ if (configData && typeof configData === 'object' && Object.keys(configData).length > 0) {
1363
+ const keys = Object.keys(configData);
1364
+ if (keys.length <= 3) {
1365
+ // Small objects - show inline with aligned labels
1366
+ const configInfo = keys.map(k => {
1367
+ const v = configData[k];
1368
+ if (typeof v === 'object') {return [`${configKey}.${k}`, chalk.gray.bold('{...}')];}
1369
+ return [`${configKey}.${k}`, JSON.stringify(v)];
1370
+ });
1371
+ allConfigItems.push(...configInfo);
1372
+ } else {
1373
+ // Large objects - show summary
1374
+ allConfigItems.push([configKey, chalk.gray.bold(`(${keys.length} properties)`)]);
1375
+ }
1376
+ } else if (configData !== undefined && configData !== null) {
1377
+ allConfigItems.push([configKey, JSON.stringify(configData)]);
1378
+ }
1379
+ }
1380
+ }
1381
+
1382
+ // Display all configuration items with single space after colon
1383
+ if (allConfigItems.length > 0) {
1384
+ if (!configSectionShown) {
1385
+ console.log(`\n ${chalk.bold.underline('Configuration')}`);
1386
+ }
1387
+ allConfigItems.forEach(([label, value]) => {
1388
+ console.log(` ${chalk.white.bold(label)}: ${chalk.italic(value)}`);
1389
+ });
1390
+ }
1391
+
1392
+ hasReadings = true;
1393
+ }
1394
+ } catch (error) {
1395
+ // Silently fail - abilities might not be available
1396
+ }
1397
+ }
1398
+
1399
+ return hasReadings;
1400
+ }
1401
+
1402
+ module.exports = { displayDeviceStatus };
1403
+