smart-home-engine 1.20.0 → 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.
- package/dist/web/assets/index-C31bVgVn.js +234 -0
- package/dist/web/assets/{index-nf1nG74s.css → index-CjRDTePT.css} +1 -1
- package/dist/web/assets/{tsMode-Dbzvu1EX.js → tsMode-wYM5jYsY.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/index.js +22 -0
- package/src/influx.js +16 -3
- package/src/lib/redis.js +7 -0
- package/src/lib/state-store.js +16 -0
- package/src/matter/controller.js +54 -1
- package/src/sandbox/influx-sandbox.js +35 -16
- package/src/web/mqtt-api.js +8 -1
- package/dist/web/assets/index-BUev42o8.js +0 -234
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as O}from"./monaco-langs-Dt8a9eeo.js";import{t as I}from"./index-
|
|
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
|
package/dist/web/index.html
CHANGED
|
@@ -172,10 +172,10 @@
|
|
|
172
172
|
}
|
|
173
173
|
})();
|
|
174
174
|
</script>
|
|
175
|
-
<script type="module" crossorigin src="/assets/index-
|
|
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
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
178
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CjRDTePT.css">
|
|
179
179
|
</head>
|
|
180
180
|
<body>
|
|
181
181
|
<div id="app"></div>
|
package/package.json
CHANGED
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/influx.js
CHANGED
|
@@ -6,6 +6,10 @@ let _mode = null; // 'v1' | 'v2' | null
|
|
|
6
6
|
|
|
7
7
|
const V1_TIMEOUT_MS = 10000;
|
|
8
8
|
|
|
9
|
+
function v1Timeout() {
|
|
10
|
+
return Number(_opts.timeout) || V1_TIMEOUT_MS;
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
/**
|
|
10
14
|
* Initialise the InfluxDB client. Called from index.js when config.influx is set.
|
|
11
15
|
*
|
|
@@ -119,7 +123,7 @@ async function v1Write(lines) {
|
|
|
119
123
|
method: 'POST',
|
|
120
124
|
headers: v1Headers(),
|
|
121
125
|
body: lines,
|
|
122
|
-
signal: AbortSignal.timeout(
|
|
126
|
+
signal: AbortSignal.timeout(v1Timeout()),
|
|
123
127
|
});
|
|
124
128
|
if (!res.ok) {
|
|
125
129
|
const body = await res.text().catch(() => '');
|
|
@@ -139,7 +143,7 @@ async function v1Query(q) {
|
|
|
139
143
|
const params = new URLSearchParams({ db: _opts.database, q, epoch: 'ms' });
|
|
140
144
|
const res = await fetch(`${_opts.url.replace(/\/$/, '')}/query?${params}`, {
|
|
141
145
|
headers: v1Headers(),
|
|
142
|
-
signal: AbortSignal.timeout(
|
|
146
|
+
signal: AbortSignal.timeout(v1Timeout()),
|
|
143
147
|
});
|
|
144
148
|
if (!res.ok) {
|
|
145
149
|
const body = await res.text().catch(() => '');
|
|
@@ -170,4 +174,13 @@ function escapeQL(s) {
|
|
|
170
174
|
return String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
171
175
|
}
|
|
172
176
|
|
|
173
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Escape a string for use inside an InfluxQL double-quoted identifier
|
|
179
|
+
* (measurement/tag/field name).
|
|
180
|
+
* @param {string} s
|
|
181
|
+
*/
|
|
182
|
+
function escapeIdent(s) {
|
|
183
|
+
return String(s).replace(/"/g, '\\"');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { init, getClient, getOpts, getMode, buildLine, v1Write, v1Query, escapeQL, escapeIdent };
|
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
|
|
package/src/lib/state-store.js
CHANGED
|
@@ -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
|
package/src/matter/controller.js
CHANGED
|
@@ -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) {
|
|
@@ -35,6 +35,18 @@ function v1RowVal(row) {
|
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Map v1 result rows to the { ts, val } shape, newest-trimmed to n (LIMIT
|
|
40
|
+
* applies per series in InfluxQL, so multi-series results must be merged and
|
|
41
|
+
* re-trimmed), sorted oldest-first.
|
|
42
|
+
* @param {object[]} rows
|
|
43
|
+
* @param {number} [n]
|
|
44
|
+
*/
|
|
45
|
+
function v1Points(rows, n) {
|
|
46
|
+
const pts = rows.map((r) => ({ ts: r.time, val: v1RowVal(r) })).sort((a, b) => b.ts - a.ts);
|
|
47
|
+
return (n ? pts.slice(0, Number(n)) : pts).reverse();
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
module.exports = function (she) {
|
|
39
51
|
she.influx = {
|
|
40
52
|
/**
|
|
@@ -101,10 +113,12 @@ module.exports = function (she) {
|
|
|
101
113
|
},
|
|
102
114
|
|
|
103
115
|
/**
|
|
104
|
-
* Return the last N recorded values for an MQTT topic.
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
116
|
+
* Return the last N recorded values for an MQTT topic (last 30 days).
|
|
117
|
+
* v2: assumes points carry a "topic" tag, value from "_value".
|
|
118
|
+
* v1: tries a measurement named like the topic first (influx4mqtt-style
|
|
119
|
+
* schema), then falls back to a "topic" tag scan; value from the
|
|
120
|
+
* "value" field, falling back to the first data column.
|
|
121
|
+
* Results are sorted oldest-first.
|
|
108
122
|
* @param {string} topic
|
|
109
123
|
* @param {number} n
|
|
110
124
|
* @returns {Promise<{ ts: number, val: any }[]>}
|
|
@@ -113,16 +127,16 @@ module.exports = function (she) {
|
|
|
113
127
|
const mode = influx.getMode();
|
|
114
128
|
if (!mode) return Promise.resolve([]);
|
|
115
129
|
if (mode === 'v1') {
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
);
|
|
130
|
+
// Fast path: measurement named like the topic (the common
|
|
131
|
+
// mqtt-to-influx v1 schema, e.g. influx4mqtt). Fallback: points
|
|
132
|
+
// stored with a "topic" tag — that needs a scan across all
|
|
133
|
+
// measurements, so keep it time-bounded (30d, like the v2 path).
|
|
134
|
+
const mQl = `SELECT * FROM "${influx.escapeIdent(topic)}" WHERE time >= now() - 30d ORDER BY time DESC LIMIT ${Number(n)}`;
|
|
135
|
+
return influx.v1Query(mQl).then((rows) => {
|
|
136
|
+
if (rows.length) return v1Points(rows, n);
|
|
137
|
+
const tagQl = `SELECT * FROM /.*/ WHERE "topic" = '${influx.escapeQL(topic)}' AND time >= now() - 30d ORDER BY time DESC LIMIT ${Number(n)}`;
|
|
138
|
+
return influx.v1Query(tagQl).then((tagRows) => v1Points(tagRows, n));
|
|
139
|
+
});
|
|
126
140
|
}
|
|
127
141
|
const opts = influx.getOpts();
|
|
128
142
|
const safeTopic = topic.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
@@ -143,8 +157,13 @@ module.exports = function (she) {
|
|
|
143
157
|
if (mode === 'v1') {
|
|
144
158
|
const fromMs = new Date(from).getTime();
|
|
145
159
|
const toMs = new Date(to).getTime();
|
|
146
|
-
const
|
|
147
|
-
|
|
160
|
+
const bounds = `time >= ${fromMs}ms AND time <= ${toMs}ms`;
|
|
161
|
+
const mQl = `SELECT * FROM "${influx.escapeIdent(topic)}" WHERE ${bounds}`;
|
|
162
|
+
return influx.v1Query(mQl).then((rows) => {
|
|
163
|
+
if (rows.length) return v1Points(rows);
|
|
164
|
+
const tagQl = `SELECT * FROM /.*/ WHERE "topic" = '${influx.escapeQL(topic)}' AND ${bounds}`;
|
|
165
|
+
return influx.v1Query(tagQl).then((tagRows) => v1Points(tagRows));
|
|
166
|
+
});
|
|
148
167
|
}
|
|
149
168
|
const opts = influx.getOpts();
|
|
150
169
|
const safeTopic = topic.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
package/src/web/mqtt-api.js
CHANGED
|
@@ -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
|