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.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/cli/commands/control/execute.js +23 -0
- package/cli/commands/control/index.js +12 -0
- package/cli/commands/control/menu.js +193 -0
- package/cli/commands/control/params/generic.js +229 -0
- package/cli/commands/control/params/index.js +56 -0
- package/cli/commands/control/params/light.js +188 -0
- package/cli/commands/control/params/thermostat.js +166 -0
- package/cli/commands/control/params/timer.js +242 -0
- package/cli/commands/control/params/trigger.js +206 -0
- package/cli/commands/dump.js +35 -0
- package/cli/commands/index.js +34 -0
- package/cli/commands/info.js +221 -0
- package/cli/commands/list.js +112 -0
- package/cli/commands/mqtt.js +187 -0
- package/cli/commands/sniffer/device-sniffer.js +217 -0
- package/cli/commands/sniffer/fake-app.js +233 -0
- package/cli/commands/sniffer/index.js +7 -0
- package/cli/commands/sniffer/message-queue.js +65 -0
- package/cli/commands/sniffer/sniffer-menu.js +676 -0
- package/cli/commands/stats.js +90 -0
- package/cli/commands/status/device-status.js +1403 -0
- package/cli/commands/status/hub-status.js +72 -0
- package/cli/commands/status/index.js +50 -0
- package/cli/commands/status/subdevices/hub-smoke-detector.js +82 -0
- package/cli/commands/status/subdevices/hub-temp-hum-sensor.js +43 -0
- package/cli/commands/status/subdevices/hub-thermostat-valve.js +83 -0
- package/cli/commands/status/subdevices/hub-water-leak-sensor.js +27 -0
- package/cli/commands/status/subdevices/index.js +23 -0
- package/cli/commands/test/index.js +185 -0
- package/cli/config/users.js +108 -0
- package/cli/control-registry.js +875 -0
- package/cli/helpers/client.js +89 -0
- package/cli/helpers/meross.js +106 -0
- package/cli/menu/index.js +10 -0
- package/cli/menu/main.js +648 -0
- package/cli/menu/settings.js +789 -0
- package/cli/meross-cli.js +547 -0
- package/cli/tests/README.md +365 -0
- package/cli/tests/test-alarm.js +144 -0
- package/cli/tests/test-child-lock.js +248 -0
- package/cli/tests/test-config.js +133 -0
- package/cli/tests/test-control.js +189 -0
- package/cli/tests/test-diffuser.js +505 -0
- package/cli/tests/test-dnd.js +246 -0
- package/cli/tests/test-electricity.js +209 -0
- package/cli/tests/test-encryption.js +281 -0
- package/cli/tests/test-garage.js +259 -0
- package/cli/tests/test-helper.js +313 -0
- package/cli/tests/test-hub-mts100.js +355 -0
- package/cli/tests/test-hub-sensors.js +489 -0
- package/cli/tests/test-light.js +253 -0
- package/cli/tests/test-presence.js +497 -0
- package/cli/tests/test-registry.js +419 -0
- package/cli/tests/test-roller-shutter.js +628 -0
- package/cli/tests/test-runner.js +415 -0
- package/cli/tests/test-runtime.js +234 -0
- package/cli/tests/test-screen.js +133 -0
- package/cli/tests/test-sensor-history.js +146 -0
- package/cli/tests/test-smoke-config.js +138 -0
- package/cli/tests/test-spray.js +131 -0
- package/cli/tests/test-temp-unit.js +133 -0
- package/cli/tests/test-template.js +238 -0
- package/cli/tests/test-thermostat.js +919 -0
- package/cli/tests/test-timer.js +372 -0
- package/cli/tests/test-toggle.js +342 -0
- package/cli/tests/test-trigger.js +279 -0
- package/cli/utils/display.js +86 -0
- package/cli/utils/terminal.js +137 -0
- 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
|
+
|