smart-home-engine 1.20.2 → 1.20.3
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-CjRDTePT.css → index-B4M4MeEk.css} +1 -1
- package/dist/web/assets/index-BbXC7eVn.js +234 -0
- package/dist/web/assets/{tsMode-wYM5jYsY.js → tsMode-DBmTyMNv.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/matter/controller.js +91 -4
- package/src/web/matter-api.js +19 -0
- package/dist/web/assets/index-C31bVgVn.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-BbXC7eVn.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-BbXC7eVn.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-B4M4MeEk.css">
|
|
179
179
|
</head>
|
|
180
180
|
<body>
|
|
181
181
|
<div id="app"></div>
|
package/package.json
CHANGED
package/src/matter/controller.js
CHANGED
|
@@ -326,6 +326,11 @@ async function _commissionInner(options) {
|
|
|
326
326
|
if (!addr) throw new Error('Commission succeeded but node has no peerAddress');
|
|
327
327
|
const nodeId = _nodeIdStr(addr.nodeId);
|
|
328
328
|
_subscribeNodeLifecycle(clientNode, nodeId);
|
|
329
|
+
// A freshly commissioned node is already online, so the lifecycle 'online'
|
|
330
|
+
// event (the other place this is wired) will not fire again — attach the
|
|
331
|
+
// attribute watchers now or the node stays without live updates until the
|
|
332
|
+
// next daemon restart.
|
|
333
|
+
_broadcastNodeAttributes(clientNode, nodeId);
|
|
329
334
|
_broadcast?.({ type: 'matter:deviceList', devices: listPaired() });
|
|
330
335
|
return nodeId;
|
|
331
336
|
}
|
|
@@ -394,11 +399,77 @@ async function getAttribute(nodeId, endpointId, clusterName, attrName) {
|
|
|
394
399
|
return clusterState[attrName];
|
|
395
400
|
}
|
|
396
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Write a single attribute value (a remote write for client nodes — the
|
|
404
|
+
* attribute must be writable per the device's Matter data model).
|
|
405
|
+
*
|
|
406
|
+
* @param {string} nodeId
|
|
407
|
+
* @param {number} endpointId
|
|
408
|
+
* @param {string} clusterName camelCase cluster name, e.g. "basicInformation"
|
|
409
|
+
* @param {string} attrName camelCase attribute name, e.g. "nodeLabel"
|
|
410
|
+
* @param {*} value
|
|
411
|
+
* @returns {Promise<void>}
|
|
412
|
+
*/
|
|
413
|
+
async function setAttribute(nodeId, endpointId, clusterName, attrName, value) {
|
|
414
|
+
const node = _findClientNode(nodeId);
|
|
415
|
+
const endpoint = _resolveEndpoint(node, endpointId);
|
|
416
|
+
await endpoint.set({ [_clusterName(clusterName)]: { [attrName]: value } });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Rename a device by writing basicInformation.nodeLabel on the root endpoint —
|
|
421
|
+
* the Matter-standard writable user label (max 32 chars), persisted on the
|
|
422
|
+
* device itself. she prefers nodeLabel over productName wherever names are
|
|
423
|
+
* shown or matched, so the new name is immediately usable in scripts.
|
|
424
|
+
*
|
|
425
|
+
* @param {string} nodeId
|
|
426
|
+
* @param {string} name
|
|
427
|
+
* @returns {Promise<void>}
|
|
428
|
+
*/
|
|
429
|
+
async function rename(nodeId, name) {
|
|
430
|
+
await setAttribute(nodeId, 0, 'basicInformation', 'nodeLabel', String(name));
|
|
431
|
+
_log?.info(`matter: renamed node ${nodeId} to "${name}"`);
|
|
432
|
+
_broadcast?.({ type: 'matter:deviceList', devices: listPaired() });
|
|
433
|
+
}
|
|
434
|
+
|
|
397
435
|
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
398
436
|
|
|
437
|
+
/**
|
|
438
|
+
* Best-effort: list the mandatory field names (camelCase) of a command's
|
|
439
|
+
* request schema. Handles both shapes matter.js uses: a TLV ObjectSchema
|
|
440
|
+
* (fieldDefinitions, camelCase keys) and a CommandModel (children with
|
|
441
|
+
* PascalCase names and conformance flags). Returns [] when unresolvable.
|
|
442
|
+
* @param {object} cluster the behavior's cluster definition
|
|
443
|
+
* @param {string} commandName
|
|
444
|
+
* @returns {string[]}
|
|
445
|
+
*/
|
|
446
|
+
function _mandatoryCommandFields(cluster, commandName) {
|
|
447
|
+
try {
|
|
448
|
+
const cmd = cluster?.commands?.[commandName];
|
|
449
|
+
const schema = cmd?.schema ?? cmd?.requestSchema;
|
|
450
|
+
if (schema?.fieldDefinitions) {
|
|
451
|
+
return Object.entries(schema.fieldDefinitions)
|
|
452
|
+
.filter(([, def]) => !def?.optional)
|
|
453
|
+
.map(([name]) => name);
|
|
454
|
+
}
|
|
455
|
+
if (schema?.children) {
|
|
456
|
+
return [...schema.children].filter((c) => c.effectiveConformance?.isMandatory !== false).map((c) => c.name.charAt(0).toLowerCase() + c.name.slice(1));
|
|
457
|
+
}
|
|
458
|
+
} catch {
|
|
459
|
+
/* best-effort — fall through */
|
|
460
|
+
}
|
|
461
|
+
return [];
|
|
462
|
+
}
|
|
463
|
+
|
|
399
464
|
/**
|
|
400
465
|
* Invoke a cluster command.
|
|
401
466
|
*
|
|
467
|
+
* Mandatory boilerplate fields that scripts rarely care about are filled with
|
|
468
|
+
* neutral defaults when omitted: optionsMask/optionsOverride → {} (bitmap 0,
|
|
469
|
+
* "obey the device's options") and transitionTime → 0 (instant). So
|
|
470
|
+
* she.matter.send('x', 1, 'levelControl', 'moveToLevel', { level: 128 }) works
|
|
471
|
+
* without spelling out the full Matter command schema.
|
|
472
|
+
*
|
|
402
473
|
* @param {string} nodeId
|
|
403
474
|
* @param {number} endpointId
|
|
404
475
|
* @param {string} clusterName camelCase cluster name, e.g. "onOff"
|
|
@@ -416,10 +487,17 @@ async function sendCommand(nodeId, endpointId, clusterName, commandName, args) {
|
|
|
416
487
|
if (!clusterAgent) throw new Error(`Cluster "${clusterName}" not found on endpoint ${endpointId}`);
|
|
417
488
|
const cmd = clusterAgent[commandName];
|
|
418
489
|
if (typeof cmd !== 'function') throw new Error(`Command "${commandName}" not found in cluster "${clusterName}"`);
|
|
419
|
-
//
|
|
490
|
+
// Fill neutral defaults for mandatory boilerplate fields the caller omitted.
|
|
491
|
+
const merged = args !== undefined && args !== null && typeof args === 'object' ? { ...args } : {};
|
|
492
|
+
for (const field of _mandatoryCommandFields(clusterAgent.cluster, commandName)) {
|
|
493
|
+
if (merged[field] !== undefined) continue;
|
|
494
|
+
if (field === 'optionsMask' || field === 'optionsOverride') merged[field] = {};
|
|
495
|
+
else if (field === 'transitionTime') merged[field] = 0;
|
|
496
|
+
}
|
|
497
|
+
// Only pass args when there is actually a payload.
|
|
420
498
|
// Void commands (e.g. onOff.off) fail TLV validation if passed an empty object.
|
|
421
|
-
const hasArgs =
|
|
422
|
-
return hasArgs ? cmd.call(clusterAgent,
|
|
499
|
+
const hasArgs = Object.keys(merged).length > 0;
|
|
500
|
+
return hasArgs ? cmd.call(clusterAgent, merged) : cmd.call(clusterAgent);
|
|
423
501
|
});
|
|
424
502
|
}
|
|
425
503
|
|
|
@@ -467,7 +545,14 @@ function _broadcastNodeAttributes(node, nodeId) {
|
|
|
467
545
|
}
|
|
468
546
|
}
|
|
469
547
|
}
|
|
470
|
-
|
|
548
|
+
if (count === 0) {
|
|
549
|
+
// Endpoint structure not populated yet (e.g. right after commissioning)
|
|
550
|
+
// — drop the guard so the next lifecycle 'online' event retries.
|
|
551
|
+
_attrBroadcastNodes.delete(nodeId);
|
|
552
|
+
_log?.debug(`matter: no watchable attributes on node ${nodeId} yet, will retry on next online event`);
|
|
553
|
+
} else {
|
|
554
|
+
_log?.info(`matter: watching ${count} attribute(s) on node ${nodeId}`);
|
|
555
|
+
}
|
|
471
556
|
} catch (err) {
|
|
472
557
|
_attrBroadcastNodes.delete(nodeId); // allow retry
|
|
473
558
|
_log?.warn(`matter: attribute broadcast setup failed for ${nodeId}: ${err.message}`);
|
|
@@ -586,6 +671,8 @@ module.exports = {
|
|
|
586
671
|
getEndpoints,
|
|
587
672
|
getDeviceSubtitle,
|
|
588
673
|
getAttribute,
|
|
674
|
+
setAttribute,
|
|
675
|
+
rename,
|
|
589
676
|
sendCommand,
|
|
590
677
|
subscribeAttribute,
|
|
591
678
|
unsubscribe,
|
package/src/web/matter-api.js
CHANGED
|
@@ -58,6 +58,25 @@ router.post('/commission', async (req, res) => {
|
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
// POST /she/matter/devices/:nodeId/rename — { name } → writes basicInformation.nodeLabel
|
|
62
|
+
router.post('/devices/:nodeId/rename', async (req, res) => {
|
|
63
|
+
if (!isReady()) return notReady(res);
|
|
64
|
+
const name = req.body?.name;
|
|
65
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
66
|
+
return res.status(400).json({ error: 'body must contain a non-empty name' });
|
|
67
|
+
}
|
|
68
|
+
if (name.trim().length > 32) {
|
|
69
|
+
return res.status(400).json({ error: 'name must be at most 32 characters (Matter nodeLabel limit)' });
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
await getController().rename(req.params.nodeId, name.trim());
|
|
73
|
+
res.json({ ok: true });
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err.message.includes('not found')) return res.status(404).json({ error: err.message });
|
|
76
|
+
res.status(500).json({ error: err.message });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
61
80
|
// GET /she/matter/devices/:nodeId — node detail
|
|
62
81
|
router.get('/devices/:nodeId', (req, res) => {
|
|
63
82
|
if (!isReady()) return notReady(res);
|