smart-home-engine 1.20.1 → 1.20.2

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.
@@ -1,4 +1,4 @@
1
- import{m as O}from"./monaco-langs-Dt8a9eeo.js";import{t as I}from"./index-DxR31sxM.js";/*!-----------------------------------------------------------------------------
1
+ import{m as O}from"./monaco-langs-Dt8a9eeo.js";import{t as I}from"./index-C31bVgVn.js";/*!-----------------------------------------------------------------------------
2
2
  * Copyright (c) Microsoft Corporation. All rights reserved.
3
3
  * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
4
4
  * Released under the MIT license
@@ -172,7 +172,7 @@
172
172
  }
173
173
  })();
174
174
  </script>
175
- <script type="module" crossorigin src="/assets/index-DxR31sxM.js"></script>
175
+ <script type="module" crossorigin src="/assets/index-C31bVgVn.js"></script>
176
176
  <link rel="modulepreload" crossorigin href="/assets/monaco-langs-Dt8a9eeo.js">
177
177
  <link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
178
178
  <link rel="stylesheet" crossorigin href="/assets/index-CjRDTePT.css">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-home-engine",
3
- "version": "1.20.1",
3
+ "version": "1.20.2",
4
4
  "description": "Node.js based script runner for use in MQTT based Smart Home environments",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -498,6 +498,28 @@ if (config.url) {
498
498
 
499
499
  if (shedb.handleMqttMessage(topic, payload)) return;
500
500
 
501
+ // An empty payload deletes the topic from the state store (mqtt-smarthome
502
+ // convention; also how a retained message is cleared per MQTT spec). The
503
+ // retain flag cannot be used to detect this: MQTT 3.1.1 brokers clear it
504
+ // when forwarding to established subscriptions, so a retained-clear
505
+ // arrives here as a plain empty message. Previously an empty payload was
506
+ // stored as { val: NaN }, so the MQTT tab kept showing cleared topics.
507
+ // Subscription callbacks are still dispatched (empty publishes are also
508
+ // used as plain triggers) with val '' — only the stored state is dropped.
509
+ if (payload.length === 0) {
510
+ const emptyOld = store.getObject('mqtt::' + topic) || {};
511
+ if (store.delete('mqtt::' + topic)) {
512
+ log.debug('empty payload, removed topic from state store:', topic);
513
+ }
514
+ const delArr = topic.split('/');
515
+ if (delArr[0] === config.variablePrefix && delArr[1] === 'status') {
516
+ store.delete('var::' + delArr.slice(2).join('/'));
517
+ }
518
+ const ts = new Date().getTime();
519
+ stateChange(topic, { val: '', ts, lc: ts }, emptyOld, msg);
520
+ return;
521
+ }
522
+
501
523
  const state = require('./lib/parse-payload')(payload);
502
524
 
503
525
  const topicArr = topic.split('/');
package/src/lib/redis.js CHANGED
@@ -63,6 +63,13 @@ async function init({ url, store, log }) {
63
63
  });
64
64
  });
65
65
 
66
+ // Write-through: StateStore deletions (e.g. cleared retained messages) → hdel
67
+ store.on('delete', (key) => {
68
+ client.hdel('she:state', key).catch((err) => {
69
+ log.error('redis: hdel failed:', err.message);
70
+ });
71
+ });
72
+
66
73
  log.info('redis: connected', url);
67
74
  }
68
75
 
@@ -10,6 +10,7 @@
10
10
  *
11
11
  * Events:
12
12
  * 'change' (key, val, obj, prevObj)
13
+ * 'delete' (key, prevObj)
13
14
  */
14
15
 
15
16
  const { EventEmitter } = require('events');
@@ -47,6 +48,21 @@ class StateStore extends EventEmitter {
47
48
  this.emit('change', key, obj.val, obj, prev);
48
49
  }
49
50
 
51
+ /**
52
+ * Remove a key from the store (e.g. when a retained MQTT message is
53
+ * deleted via an empty retained publish). Emits 'delete' (key, prevObj)
54
+ * when the key existed; a no-op otherwise.
55
+ * @param {string} key
56
+ * @returns {boolean} true when the key existed and was removed
57
+ */
58
+ delete(key) {
59
+ const prev = this._map.get(key);
60
+ if (prev === undefined) return false;
61
+ this._map.delete(key);
62
+ this.emit('delete', key, prev);
63
+ return true;
64
+ }
65
+
50
66
  /**
51
67
  * @param {string} key
52
68
  * @returns {*} the val of the stored state, or undefined
@@ -9,7 +9,11 @@
9
9
  * All nodeIds are exposed as decimal strings (BigInt serialization boundary).
10
10
  */
11
11
 
12
- const { Environment, ServerNode, ControllerBehavior } = require('@matter/main');
12
+ const { Environment, ServerNode, ControllerBehavior, Logger, LogLevel, LogFormat } = require('@matter/main');
13
+
14
+ // Upper bound for a commissioning attempt — matter.js has internal retries that
15
+ // can keep an HTTP request (and the pairing wizard) hanging for a very long time.
16
+ const COMMISSION_TIMEOUT_MS = 120000;
13
17
 
14
18
  /** @type {import('@matter/main').ServerNode | null} */
15
19
  let _server = null;
@@ -105,6 +109,17 @@ async function init(storagePath, log, broadcastFn) {
105
109
  if (_server) throw new Error('Matter controller already started');
106
110
  _log = log;
107
111
 
112
+ // Route matter.js's own diagnostics into the daemon logger so commissioning
113
+ // problems (discovery, PASE failures, retries) are visible in the Logs tab —
114
+ // by default matter.js writes straight to stdout, bypassing she's logging.
115
+ Logger.format = LogFormat.PLAIN;
116
+ Logger.level = LogLevel.INFO;
117
+ Logger.destinations.default.write = (text, message) => {
118
+ if (message.level >= LogLevel.ERROR) _log.error('matter.js:', text);
119
+ else if (message.level === LogLevel.WARN) _log.warn('matter.js:', text);
120
+ else _log.debug('matter.js:', text);
121
+ };
122
+
108
123
  // Configure storage before anything else touches StorageService
109
124
  Environment.default.vars.set('storage.path', storagePath);
110
125
 
@@ -240,6 +255,44 @@ function listPaired() {
240
255
  */
241
256
  async function commission(options) {
242
257
  if (!_server) throw new Error('Matter controller not started');
258
+
259
+ // Accept QR-code payloads ("MT:...") — matter.js's pairingCode option only
260
+ // decodes 11/21-digit manual pairing codes, so decode QR payloads here.
261
+ if (typeof options.pairingCode === 'string' && options.pairingCode.trim().toUpperCase().startsWith('MT:')) {
262
+ const { QrPairingCodeCodec } = require('@matter/types');
263
+ const [decoded] = QrPairingCodeCodec.decode(options.pairingCode.trim());
264
+ const { pairingCode: _pc, ...rest } = options;
265
+ options = { ...rest, passcode: decoded.passcode, discriminator: decoded.discriminator };
266
+ _log.debug('matter: decoded QR pairing code, discriminator', decoded.discriminator);
267
+ }
268
+
269
+ const label = options.pairingCode !== undefined ? 'pairing code' : `passcode${options.discriminator !== undefined ? ' + discriminator ' + options.discriminator : ''}`;
270
+ _log.info(`matter: commissioning start (${label}${options.discoveryAddress ? ', direct address ' + options.discoveryAddress : ''})`);
271
+ let timer;
272
+ try {
273
+ const nodeId = await Promise.race([
274
+ _commissionInner(options),
275
+ new Promise((_resolve, reject) => {
276
+ timer = setTimeout(
277
+ () =>
278
+ reject(
279
+ new Error(`commissioning timed out after ${COMMISSION_TIMEOUT_MS / 1000} s — check that the pairing window is still open and the device is reachable`),
280
+ ),
281
+ COMMISSION_TIMEOUT_MS,
282
+ );
283
+ }),
284
+ ]);
285
+ _log.info('matter: commissioning succeeded, nodeId', nodeId);
286
+ return nodeId;
287
+ } catch (err) {
288
+ _log.error('matter: commissioning failed:', err.message);
289
+ throw err;
290
+ } finally {
291
+ clearTimeout(timer);
292
+ }
293
+ }
294
+
295
+ async function _commissionInner(options) {
243
296
  const { discoveryAddress, ...commissionOpts } = options;
244
297
  let clientNode;
245
298
  if (discoveryAddress) {
@@ -12,7 +12,8 @@
12
12
  * (or null when no broker is configured).
13
13
  *
14
14
  * State changes for mqtt:: keys are automatically broadcast to WebSocket clients
15
- * as { type: 'mqtt', topic, val, ts }.
15
+ * as { type: 'mqtt', topic, val, ts }; deletions (retained message cleared via
16
+ * an empty retained publish) as { type: 'mqtt', topic, deleted: true, ts }.
16
17
  */
17
18
 
18
19
  const express = require('express');
@@ -43,6 +44,12 @@ function init(store, getMqttClient) {
43
44
  broadcastBrokerLog(topic.slice('$SYS/broker/log/'.length), String(obj.val), obj.ts);
44
45
  }
45
46
  });
47
+
48
+ // Forward deletions (retained message cleared) so clients drop the topic
49
+ store.on('delete', (key) => {
50
+ if (!key.startsWith('mqtt::')) return;
51
+ broadcast({ type: 'mqtt', topic: key.slice(6), deleted: true, ts: Date.now() });
52
+ });
46
53
  }
47
54
 
48
55
  // GET /she/mqtt/state — sorted snapshot of all retained MQTT topic values