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,187 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+
5
+ async function listMqttConnections(manager, options = {}) {
6
+ const { verbose = false, json = false } = options;
7
+ const mqttConnections = manager.mqttConnections || {};
8
+ const domains = Object.keys(mqttConnections);
9
+
10
+ if (domains.length === 0) {
11
+ if (json) {
12
+ console.log(JSON.stringify({ connections: [] }, null, 2));
13
+ } else {
14
+ console.log(chalk.yellow('No MQTT connections found.'));
15
+ }
16
+ return;
17
+ }
18
+
19
+ const connectionsData = domains.map(domain => {
20
+ const conn = mqttConnections[domain];
21
+ const client = conn?.client;
22
+
23
+ let status = 'disconnected';
24
+ if (client) {
25
+ if (client.connected) {
26
+ status = 'connected';
27
+ } else if (client.reconnecting) {
28
+ status = 'reconnecting';
29
+ }
30
+ }
31
+
32
+ const deviceCount = conn?.deviceList?.length || 0;
33
+ const deviceUuids = conn?.deviceList || [];
34
+
35
+ const subscribedTopics = [];
36
+ if (manager.userId) {
37
+ subscribedTopics.push(`/app/${manager.userId}/subscribe`);
38
+ }
39
+ if (manager.clientResponseTopic) {
40
+ subscribedTopics.push(manager.clientResponseTopic);
41
+ }
42
+
43
+ const namespaces = new Set();
44
+ deviceUuids.forEach(uuid => {
45
+ const device = manager.getDevice(uuid);
46
+ if (device && device.abilities) {
47
+ Object.keys(device.abilities).forEach(namespace => {
48
+ namespaces.add(namespace);
49
+ });
50
+ }
51
+ });
52
+ const namespacesList = Array.from(namespaces).sort();
53
+
54
+ let clientId = 'N/A';
55
+ let keepalive = 'N/A';
56
+ let port = 'N/A';
57
+ let protocol = 'N/A';
58
+ let protocolRaw = 'N/A';
59
+
60
+ if (client && client.options) {
61
+ clientId = client.options.clientId || 'N/A';
62
+ keepalive = client.options.keepalive || 'N/A';
63
+ port = client.options.port || 'N/A';
64
+ const protocolValue = client.options.protocol || client.options.protocolId;
65
+ if (protocolValue) {
66
+ protocolRaw = protocolValue;
67
+ protocol = protocolValue === 'mqtts' ? 'MQTTS (TLS)' : protocolValue.toUpperCase();
68
+ }
69
+ }
70
+
71
+ return {
72
+ domain,
73
+ status,
74
+ deviceCount,
75
+ deviceUuids,
76
+ clientId,
77
+ keepalive,
78
+ port,
79
+ protocol: protocolRaw,
80
+ protocolDisplay: protocol,
81
+ subscribedTopics,
82
+ namespaces: namespacesList
83
+ };
84
+ });
85
+
86
+ if (json) {
87
+ const jsonData = connectionsData.map(conn => ({
88
+ domain: conn.domain,
89
+ status: conn.status,
90
+ deviceCount: conn.deviceCount,
91
+ deviceUuids: conn.deviceUuids,
92
+ clientId: conn.clientId,
93
+ keepalive: conn.keepalive,
94
+ port: conn.port,
95
+ protocol: conn.protocol,
96
+ subscribedTopics: conn.subscribedTopics,
97
+ namespaces: conn.namespaces
98
+ }));
99
+ console.log(JSON.stringify({ connections: jsonData }, null, 2));
100
+ return;
101
+ }
102
+
103
+ console.log(`\n${chalk.bold.underline('MQTT Connections')}\n`);
104
+
105
+ connectionsData.forEach((conn, index) => {
106
+ console.log(` [${index}] ${chalk.bold.underline(conn.domain)}`);
107
+
108
+ const statusColor = conn.status === 'connected'
109
+ ? chalk.green('Connected')
110
+ : conn.status === 'reconnecting'
111
+ ? chalk.yellow('Reconnecting')
112
+ : chalk.red('Disconnected');
113
+
114
+ const connectionInfo = [
115
+ ['Status', statusColor],
116
+ ['Port', chalk.cyan(conn.port)],
117
+ ['Protocol', chalk.cyan(conn.protocolDisplay || conn.protocol)],
118
+ ['Devices', chalk.cyan(conn.deviceCount.toString())]
119
+ ];
120
+
121
+ const maxLabelLength = Math.max(...connectionInfo.map(([label]) => label.length));
122
+
123
+ connectionInfo.forEach(([label, value]) => {
124
+ const padding = ' '.repeat(maxLabelLength - label.length);
125
+ console.log(` ${chalk.white.bold(label)}:${padding} ${chalk.italic(value)}`);
126
+ });
127
+
128
+ if (conn.subscribedTopics.length > 0) {
129
+ console.log(`\n ${chalk.white.bold(`Subscribed Topics (${chalk.cyan(conn.subscribedTopics.length)}):`)}`);
130
+ conn.subscribedTopics.forEach(topic => {
131
+ console.log(` ${chalk.cyan(topic)}`);
132
+ });
133
+ }
134
+
135
+ if (conn.namespaces.length > 0) {
136
+ console.log(`\n ${chalk.white.bold(`Namespaces (${chalk.cyan(conn.namespaces.length)}):`)}`);
137
+ if (verbose) {
138
+ conn.namespaces.forEach(namespace => {
139
+ console.log(` ${chalk.cyan(namespace)}`);
140
+ });
141
+ } else {
142
+ const displayCount = Math.min(5, conn.namespaces.length);
143
+ conn.namespaces.slice(0, displayCount).forEach(namespace => {
144
+ console.log(` ${chalk.cyan(namespace)}`);
145
+ });
146
+ const remainingCount = conn.namespaces.length - displayCount;
147
+ if (remainingCount > 0) {
148
+ console.log(` ${chalk.gray(`... and ${remainingCount} more (use --verbose to see all)`)}`);
149
+ }
150
+ }
151
+ } else if (conn.deviceCount > 0) {
152
+ console.log(`\n ${chalk.gray('Namespaces: No abilities loaded yet')}`);
153
+ }
154
+
155
+ if (verbose) {
156
+ const clientIdDisplay = conn.clientId !== 'N/A' && conn.clientId.length > 20
157
+ ? `${conn.clientId.substring(0, 8)}...${conn.clientId.substring(conn.clientId.length - 8)}`
158
+ : conn.clientId;
159
+
160
+ const verboseInfo = [
161
+ ['Client ID', chalk.cyan(clientIdDisplay)],
162
+ ['Keepalive', chalk.cyan(`${conn.keepalive}s`)]
163
+ ];
164
+
165
+ verboseInfo.forEach(([label, value]) => {
166
+ const padding = ' '.repeat(maxLabelLength - label.length);
167
+ console.log(` ${chalk.white.bold(label)}:${padding} ${chalk.italic(value)}`);
168
+ });
169
+
170
+ if (conn.deviceUuids.length > 0) {
171
+ console.log(`\n ${chalk.white.bold(`Device UUIDs (${chalk.cyan(conn.deviceUuids.length)}):`)}`);
172
+ conn.deviceUuids.forEach((uuid) => {
173
+ const device = manager.getDevice(uuid);
174
+ const deviceName = device ? (device.name || 'Unknown') : 'Unknown';
175
+ console.log(` ${chalk.cyan(uuid)} ${chalk.gray(`(${deviceName})`)}`);
176
+ });
177
+ }
178
+ }
179
+
180
+ if (index < connectionsData.length - 1) {
181
+ console.log('');
182
+ }
183
+ });
184
+ }
185
+
186
+ module.exports = { listMqttConnections };
187
+
@@ -0,0 +1,217 @@
1
+ 'use strict';
2
+
3
+ const mqtt = require('mqtt');
4
+ const crypto = require('crypto');
5
+ const { buildDeviceRequestTopic } = require('meross-iot/lib/utilities/mqtt');
6
+ const MessageQueue = require('./message-queue');
7
+
8
+ /**
9
+ * DeviceSniffer - Impersonates a Meross device to intercept app commands.
10
+ *
11
+ * Connects to MQTT broker using device credentials and subscribes to the device's
12
+ * request topic to capture commands sent from the Meross app.
13
+ */
14
+ class DeviceSniffer {
15
+ constructor(options) {
16
+ const { uuid, macAddress, userId, cloudKey, mqttHost, mqttPort, logger } = options;
17
+
18
+ this._uuid = uuid.toLowerCase();
19
+ this._macAddress = macAddress;
20
+ this._userId = userId;
21
+ this._cloudKey = cloudKey;
22
+ this._mqttHost = mqttHost;
23
+ this._mqttPort = mqttPort || 2001;
24
+ this._logger = logger || console.log;
25
+
26
+ this._client = null;
27
+ this._msgQueue = new MessageQueue();
28
+ this._connected = false;
29
+ this._subscribed = false;
30
+ this._deviceTopic = buildDeviceRequestTopic(this._uuid);
31
+
32
+ // Build client ID: fmware:{uuid}_random
33
+ this._clientId = `fmware:${this._uuid}_random`;
34
+
35
+ // Build device password: {userId}_{MD5(macAddress + cloudKey).toLowerCase()}
36
+ const macKeyDigest = crypto.createHash('md5')
37
+ .update(`${macAddress}${cloudKey}`)
38
+ .digest('hex')
39
+ .toLowerCase();
40
+ this._devicePassword = `${userId}_${macKeyDigest}`;
41
+ }
42
+
43
+ /**
44
+ * Start the device sniffer and connect to MQTT broker
45
+ * @param {number} timeout - Connection timeout in milliseconds
46
+ * @returns {Promise<void>}
47
+ */
48
+ async start(timeout = 5000) {
49
+ return new Promise((resolve, reject) => {
50
+ const timeoutId = setTimeout(() => {
51
+ if (!this._connected) {
52
+ this._client?.end();
53
+ reject(new Error('Device sniffer connection timeout'));
54
+ }
55
+ }, timeout);
56
+
57
+ this._client = mqtt.connect({
58
+ protocol: 'mqtts',
59
+ host: this._mqttHost,
60
+ port: this._mqttPort,
61
+ clientId: this._clientId,
62
+ username: this._macAddress,
63
+ password: this._devicePassword,
64
+ rejectUnauthorized: true,
65
+ reconnectPeriod: 0 // Disable auto-reconnect for sniffer
66
+ });
67
+
68
+ this._client.on('connect', (connack) => {
69
+ this._connected = true;
70
+ clearTimeout(timeoutId);
71
+ this._logger(`Device sniffer connected to ${this._mqttHost}:${this._mqttPort}`);
72
+
73
+ if (connack && connack.returnCode !== 0) {
74
+ const error = new Error(`Connection refused: return code ${connack.returnCode}`);
75
+ this._logger(`Connection error: ${error.message}`);
76
+ reject(error);
77
+ return;
78
+ }
79
+
80
+ // Subscribe to device topic after connection is established
81
+ // Match Python pattern: subscribe after connect, wait for subscription callback
82
+ this._logger(`Subscribing to device topic: ${this._deviceTopic}`);
83
+ this._client.subscribe(this._deviceTopic, (err) => {
84
+ if (err) {
85
+ const errorMsg = err.message || err.toString() || 'Unknown subscription error';
86
+ this._logger(`Warning: Subscription to device topic failed: ${errorMsg}`);
87
+ this._logger(`Topic: ${this._deviceTopic}`);
88
+ this._logger(`Client ID: ${this._clientId}`);
89
+ this._logger(`Username: ${this._macAddress}`);
90
+ // In Python, subscription failure doesn't stop the sniffer
91
+ // Messages may still be routed automatically based on client ID
92
+ this._subscribed = true;
93
+ this._logger('Continuing anyway - messages may be routed automatically');
94
+ resolve();
95
+ return;
96
+ }
97
+ // Subscription successful - match Python's _on_subscribe behavior
98
+ this._subscribed = true;
99
+ this._logger(`Subscribed to topic: ${this._deviceTopic}`);
100
+ resolve();
101
+ });
102
+ });
103
+
104
+ // Handle subscription acknowledgments (like Python's on_subscribe)
105
+ this._client.on('packetsend', (packet) => {
106
+ if (packet.cmd === 'subscribe') {
107
+ this._logger(`Subscription packet sent for topic: ${this._deviceTopic}`);
108
+ }
109
+ });
110
+
111
+ this._client.on('error', (error) => {
112
+ clearTimeout(timeoutId);
113
+ this._logger(`Device sniffer MQTT error: ${error.message}`);
114
+ if (!this._connected) {
115
+ reject(error);
116
+ }
117
+ });
118
+
119
+ this._client.on('message', (topic, message) => {
120
+ try {
121
+ const parsed = JSON.parse(message.toString());
122
+ this._logger(`Device sniffer received message on ${topic}: ${JSON.stringify(parsed)}`);
123
+ this._msgQueue.syncPut({ topic, message: message.toString(), parsed });
124
+ } catch (err) {
125
+ this._logger(`Error parsing message: ${err.message}`);
126
+ }
127
+ });
128
+
129
+ this._client.on('close', () => {
130
+ this._connected = false;
131
+ this._subscribed = false;
132
+ this._logger('Device sniffer disconnected');
133
+ });
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Stop the device sniffer and disconnect from MQTT broker
139
+ * @returns {Promise<void>}
140
+ */
141
+ async stop() {
142
+ return new Promise((resolve) => {
143
+ if (!this._client) {
144
+ resolve();
145
+ return;
146
+ }
147
+
148
+ // Remove event listeners to prevent memory leaks
149
+ this._client.removeAllListeners();
150
+
151
+ // Set a timeout to force resolve if end() doesn't callback
152
+ const timeout = setTimeout(() => {
153
+ this._connected = false;
154
+ this._subscribed = false;
155
+ this._client = null;
156
+ resolve();
157
+ }, 2000);
158
+
159
+ try {
160
+ this._client.end(false, () => {
161
+ clearTimeout(timeout);
162
+ this._connected = false;
163
+ this._subscribed = false;
164
+ this._client = null;
165
+ resolve();
166
+ });
167
+ } catch (err) {
168
+ clearTimeout(timeout);
169
+ this._connected = false;
170
+ this._subscribed = false;
171
+ this._client = null;
172
+ resolve();
173
+ }
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Wait for a message from the device topic
179
+ * Filters for SET/GET methods only (ignores ACKs and PUSH)
180
+ * @param {string[]} validMethods - Array of valid methods to accept (default: ['SET', 'GET'])
181
+ * @returns {Promise<{topic: string, message: string, parsed: Object, namespace: string, method: string, payload: Object}>}
182
+ */
183
+ async waitForMessage(validMethods = ['SET', 'GET']) {
184
+ while (true) {
185
+ const { topic, message, parsed } = await this._msgQueue.asyncGet();
186
+
187
+ if (!parsed || !parsed.header) {
188
+ continue;
189
+ }
190
+
191
+ const { namespace, method } = parsed.header;
192
+ const payload = parsed.payload || {};
193
+
194
+ // Filter for valid methods only
195
+ if (validMethods.includes(method)) {
196
+ return {
197
+ topic,
198
+ message,
199
+ parsed,
200
+ namespace,
201
+ method,
202
+ payload
203
+ };
204
+ }
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Check if the sniffer is connected
210
+ * @returns {boolean}
211
+ */
212
+ get isConnected() {
213
+ return this._connected && this._subscribed;
214
+ }
215
+ }
216
+
217
+ module.exports = DeviceSniffer;
@@ -0,0 +1,233 @@
1
+ 'use strict';
2
+
3
+ const mqtt = require('mqtt');
4
+ const {
5
+ buildDeviceRequestTopic,
6
+ buildClientResponseTopic,
7
+ buildClientUserTopic,
8
+ generateClientAndAppId,
9
+ generateMqttPassword
10
+ } = require('meross-iot/lib/utilities/mqtt');
11
+ const MessageQueue = require('./message-queue');
12
+
13
+ /**
14
+ * AppSniffer - Impersonates a Meross app client to capture push notifications.
15
+ *
16
+ * Connects to MQTT broker using app credentials and subscribes to multiple topics
17
+ * to capture push notifications and device responses.
18
+ */
19
+ class AppSniffer {
20
+ constructor(options) {
21
+ const { userId, cloudKey, deviceUuid, mqttHost, mqttPort, logger } = options;
22
+
23
+ this._userId = userId;
24
+ this._cloudKey = cloudKey;
25
+ this._deviceUuid = deviceUuid.toLowerCase();
26
+ this._mqttHost = mqttHost;
27
+ this._mqttPort = mqttPort || 2001;
28
+ this._logger = logger || console.log;
29
+
30
+ this._client = null;
31
+ this._pushQueue = new MessageQueue();
32
+ this._connected = false;
33
+ this._subscribed = false;
34
+
35
+ // Generate app ID and client ID
36
+ // Match Python: app_id is "sniffer", client_id is "app:sniffer-{randomHash}"
37
+ const { clientId } = generateClientAndAppId();
38
+ this._appId = 'sniffer'; // Fixed app ID like Python version
39
+ this._clientId = `app:sniffer-${clientId.replace('app:', '')}`;
40
+
41
+ // Build topics
42
+ this._deviceTopic = buildDeviceRequestTopic(this._deviceUuid);
43
+ this._clientResponseTopic = buildClientResponseTopic(this._userId, this._appId);
44
+ this._userTopic = buildClientUserTopic(this._userId);
45
+
46
+ // Generate MQTT password
47
+ this._mqttPassword = generateMqttPassword(this._userId, this._cloudKey);
48
+
49
+ // Track subscription state
50
+ this._subscriptionPromise = null;
51
+ this._subscriptionResolve = null;
52
+ this._subscriptionReject = null;
53
+ }
54
+
55
+ /**
56
+ * Subscribe to topics sequentially, waiting for each subscription to complete
57
+ * Matches Python implementation pattern
58
+ * @private
59
+ */
60
+ _subscribeSequentially(resolve, _reject) {
61
+ const topics = [
62
+ { topic: this._deviceTopic, name: 'device' },
63
+ { topic: this._clientResponseTopic, name: 'client-response' },
64
+ { topic: this._userTopic, name: 'user' }
65
+ ];
66
+
67
+ let currentIndex = 0;
68
+
69
+ const subscribeNext = () => {
70
+ if (currentIndex >= topics.length) {
71
+ // All subscriptions completed
72
+ this._subscribed = true;
73
+ resolve();
74
+ return;
75
+ }
76
+
77
+ const { topic, name } = topics[currentIndex];
78
+ this._logger(`Subscribing to ${name} topic: ${topic}`);
79
+
80
+ // Subscribe and wait for acknowledgment
81
+ this._client.subscribe(topic, (err) => {
82
+ if (err) {
83
+ const errorMsg = err.message || err.toString() || 'Unknown subscription error';
84
+ this._logger(`Warning: Subscription to ${name} topic failed: ${errorMsg}`);
85
+ this._logger(`Topic: ${topic}`);
86
+ // Continue to next subscription even if this one fails
87
+ currentIndex++;
88
+ setImmediate(() => subscribeNext());
89
+ return;
90
+ }
91
+ // Success - proceed to next subscription
92
+ this._logger(`Subscribed to ${name} topic: ${topic}`);
93
+ currentIndex++;
94
+ // Wait a tiny bit before next subscription (like Python's event.wait())
95
+ setImmediate(() => subscribeNext());
96
+ });
97
+ };
98
+
99
+ // Start sequential subscriptions
100
+ subscribeNext();
101
+ }
102
+
103
+ /**
104
+ * Start the app sniffer and connect to MQTT broker
105
+ * @param {number} timeout - Connection timeout in milliseconds
106
+ * @returns {Promise<void>}
107
+ */
108
+ async start(timeout = 5000) {
109
+ return new Promise((resolve, reject) => {
110
+ const timeoutId = setTimeout(() => {
111
+ if (!this._connected) {
112
+ this._client?.end();
113
+ reject(new Error('App sniffer connection timeout'));
114
+ }
115
+ }, timeout);
116
+
117
+ this._client = mqtt.connect({
118
+ protocol: 'mqtts',
119
+ host: this._mqttHost,
120
+ port: this._mqttPort,
121
+ clientId: this._clientId,
122
+ username: this._userId,
123
+ password: this._mqttPassword,
124
+ rejectUnauthorized: true,
125
+ reconnectPeriod: 0 // Disable auto-reconnect for sniffer
126
+ });
127
+
128
+ this._client.on('connect', (connack) => {
129
+ this._connected = true;
130
+ clearTimeout(timeoutId);
131
+ this._logger(`App sniffer connected to ${this._mqttHost}:${this._mqttPort}`);
132
+
133
+ if (connack && connack.returnCode !== 0) {
134
+ const error = new Error(`Connection refused: return code ${connack.returnCode}`);
135
+ this._logger(`Connection error: ${error.message}`);
136
+ reject(error);
137
+ return;
138
+ }
139
+
140
+ // Subscribe to topics sequentially, matching Python implementation
141
+ // Python subscribes one at a time, waiting for each to complete
142
+ this._subscribeSequentially(resolve, reject);
143
+ });
144
+
145
+ this._client.on('error', (error) => {
146
+ clearTimeout(timeoutId);
147
+ this._logger(`App sniffer MQTT error: ${error.message}`);
148
+ if (!this._connected) {
149
+ reject(error);
150
+ }
151
+ });
152
+
153
+ this._client.on('message', (topic, message) => {
154
+ try {
155
+ const parsed = JSON.parse(message.toString());
156
+ const method = parsed.header?.method || 'UNKNOWN';
157
+
158
+ // Only capture PUSH notifications
159
+ if (method === 'PUSH') {
160
+ this._logger(`App sniffer received PUSH on ${topic}: ${JSON.stringify(parsed)}`);
161
+ this._pushQueue.syncPut(parsed);
162
+ }
163
+ } catch (err) {
164
+ this._logger(`Error parsing message: ${err.message}`);
165
+ }
166
+ });
167
+
168
+ this._client.on('close', () => {
169
+ this._connected = false;
170
+ this._subscribed = false;
171
+ this._logger('App sniffer disconnected');
172
+ });
173
+ });
174
+ }
175
+
176
+ /**
177
+ * Stop the app sniffer and disconnect from MQTT broker
178
+ * @returns {Promise<void>}
179
+ */
180
+ async stop() {
181
+ return new Promise((resolve) => {
182
+ if (!this._client) {
183
+ resolve();
184
+ return;
185
+ }
186
+
187
+ // Remove event listeners to prevent memory leaks
188
+ this._client.removeAllListeners();
189
+
190
+ // Set a timeout to force resolve if end() doesn't callback
191
+ const timeout = setTimeout(() => {
192
+ this._connected = false;
193
+ this._subscribed = false;
194
+ this._client = null;
195
+ resolve();
196
+ }, 2000);
197
+
198
+ try {
199
+ this._client.end(false, () => {
200
+ clearTimeout(timeout);
201
+ this._connected = false;
202
+ this._subscribed = false;
203
+ this._client = null;
204
+ resolve();
205
+ });
206
+ } catch (err) {
207
+ clearTimeout(timeout);
208
+ this._connected = false;
209
+ this._subscribed = false;
210
+ this._client = null;
211
+ resolve();
212
+ }
213
+ });
214
+ }
215
+
216
+ /**
217
+ * Wait for a push notification
218
+ * @returns {Promise<Object>} Parsed push notification message
219
+ */
220
+ async waitForPushNotification() {
221
+ return await this._pushQueue.asyncGet();
222
+ }
223
+
224
+ /**
225
+ * Check if the sniffer is connected
226
+ * @returns {boolean}
227
+ */
228
+ get isConnected() {
229
+ return this._connected && this._subscribed;
230
+ }
231
+ }
232
+
233
+ module.exports = AppSniffer;
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const { snifferMenu } = require('./sniffer-menu');
4
+
5
+ module.exports = {
6
+ snifferMenu
7
+ };