smart-home-engine 1.20.2 → 1.20.4
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-xX-8bkDA.js +234 -0
- package/dist/web/assets/{tsMode-wYM5jYsY.js → tsMode-Bd9DcRmS.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/matter/controller.js +139 -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-xX-8bkDA.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-xX-8bkDA.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,115 @@ 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
|
+
* Uses the interaction-level write rather than Endpoint.set(): set() requires
|
|
407
|
+
* a synthesized client behavior, which matter.js does not create for every
|
|
408
|
+
* cluster (e.g. basicInformation on the root endpoint reports
|
|
409
|
+
* "Behavior ... is not present on this endpoint" although the cluster and its
|
|
410
|
+
* state are there). The interaction write goes over the same wire path that
|
|
411
|
+
* commands use and needs no behavior instance.
|
|
412
|
+
*
|
|
413
|
+
* @param {string} nodeId
|
|
414
|
+
* @param {number} endpointId
|
|
415
|
+
* @param {string} clusterName camelCase cluster name, e.g. "basicInformation"
|
|
416
|
+
* @param {string} attrName camelCase attribute name, e.g. "nodeLabel"
|
|
417
|
+
* @param {*} value
|
|
418
|
+
* @returns {Promise<void>}
|
|
419
|
+
*/
|
|
420
|
+
async function setAttribute(nodeId, endpointId, clusterName, attrName, value) {
|
|
421
|
+
const node = _findClientNode(nodeId);
|
|
422
|
+
const { Write, WriteResult } = require('@matter/protocol');
|
|
423
|
+
const clusters = require('@matter/main/clusters');
|
|
424
|
+
const clusterDef = clusters[clusterName.charAt(0).toUpperCase() + clusterName.slice(1)]?.Complete;
|
|
425
|
+
if (!clusterDef) throw new Error(`Unknown cluster "${clusterName}"`);
|
|
426
|
+
if (!clusterDef.attributes?.[attrName]) throw new Error(`Unknown attribute "${clusterName}.${attrName}"`);
|
|
427
|
+
const request = Write(Write.Attribute({ endpoint: Number(endpointId), cluster: clusterDef, attributes: attrName, value }));
|
|
428
|
+
const result = await node.interaction.write(request);
|
|
429
|
+
WriteResult.assertSuccess(result);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Rename a device by writing basicInformation.nodeLabel on the root endpoint —
|
|
434
|
+
* the Matter-standard writable user label (max 32 chars), persisted on the
|
|
435
|
+
* device itself. she prefers nodeLabel over productName wherever names are
|
|
436
|
+
* shown or matched, so the new name is immediately usable in scripts.
|
|
437
|
+
*
|
|
438
|
+
* @param {string} nodeId
|
|
439
|
+
* @param {string} name
|
|
440
|
+
* @returns {Promise<void>}
|
|
441
|
+
*/
|
|
442
|
+
async function rename(nodeId, name) {
|
|
443
|
+
await setAttribute(nodeId, 0, 'basicInformation', 'nodeLabel', String(name));
|
|
444
|
+
_log?.info(`matter: renamed node ${nodeId} to "${name}"`);
|
|
445
|
+
_broadcast?.({ type: 'matter:deviceList', devices: listPaired() });
|
|
446
|
+
}
|
|
447
|
+
|
|
397
448
|
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
398
449
|
|
|
450
|
+
/** Lowercase the first character (PascalCase model names → camelCase). */
|
|
451
|
+
function _camelize(s) {
|
|
452
|
+
return s.charAt(0).toLowerCase() + s.slice(1);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Best-effort: list the mandatory field names (camelCase) of a command's
|
|
457
|
+
* request schema. Resolution mirrors matter.js's own command lookup:
|
|
458
|
+
* 1. the behavior's cluster definition (unwrapping a `.cluster`-wrapped
|
|
459
|
+
* specifier like @matter/protocol Specifier.clusterFor does), commands as
|
|
460
|
+
* a TLV Command (requestSchema/fieldDefinitions) or a model-based command
|
|
461
|
+
* ({ schema: CommandModel });
|
|
462
|
+
* 2. fallback: the behavior's ClusterModel schema — request-direction
|
|
463
|
+
* CommandModel children matched by camelized name (the shape generated
|
|
464
|
+
* for discovered client clusters).
|
|
465
|
+
* Returns [] when unresolvable.
|
|
466
|
+
* @param {object} behavior the cluster behavior (agent[clusterName])
|
|
467
|
+
* @param {string} commandName
|
|
468
|
+
* @returns {string[]}
|
|
469
|
+
*/
|
|
470
|
+
function _mandatoryCommandFields(behavior, commandName) {
|
|
471
|
+
try {
|
|
472
|
+
let cluster = behavior?.cluster;
|
|
473
|
+
if (cluster && typeof cluster === 'object' && 'cluster' in cluster) cluster = cluster.cluster;
|
|
474
|
+
let cmd = cluster?.commands?.[commandName];
|
|
475
|
+
if (!cmd) {
|
|
476
|
+
// Discovered client clusters: command models live on the ClusterModel schema
|
|
477
|
+
const schemaModel = behavior?.schema ?? behavior?.constructor?.schema;
|
|
478
|
+
if (schemaModel?.children) {
|
|
479
|
+
for (const c of schemaModel.children) {
|
|
480
|
+
if (c.tag === 'command' && c.name && _camelize(c.name) === commandName && c.direction !== 'response') {
|
|
481
|
+
cmd = { schema: c };
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const schema = cmd?.schema ?? cmd?.requestSchema;
|
|
488
|
+
if (schema?.fieldDefinitions) {
|
|
489
|
+
return Object.entries(schema.fieldDefinitions)
|
|
490
|
+
.filter(([, def]) => !def?.optional)
|
|
491
|
+
.map(([name]) => name);
|
|
492
|
+
}
|
|
493
|
+
if (schema?.children) {
|
|
494
|
+
return [...schema.children].filter((c) => c.effectiveConformance?.isMandatory !== false).map((c) => _camelize(c.name));
|
|
495
|
+
}
|
|
496
|
+
} catch {
|
|
497
|
+
/* best-effort — fall through */
|
|
498
|
+
}
|
|
499
|
+
return [];
|
|
500
|
+
}
|
|
501
|
+
|
|
399
502
|
/**
|
|
400
503
|
* Invoke a cluster command.
|
|
401
504
|
*
|
|
505
|
+
* Mandatory boilerplate fields that scripts rarely care about are filled with
|
|
506
|
+
* neutral defaults when omitted: optionsMask/optionsOverride → {} (bitmap 0,
|
|
507
|
+
* "obey the device's options") and transitionTime → 0 (instant). So
|
|
508
|
+
* she.matter.send('x', 1, 'levelControl', 'moveToLevel', { level: 128 }) works
|
|
509
|
+
* without spelling out the full Matter command schema.
|
|
510
|
+
*
|
|
402
511
|
* @param {string} nodeId
|
|
403
512
|
* @param {number} endpointId
|
|
404
513
|
* @param {string} clusterName camelCase cluster name, e.g. "onOff"
|
|
@@ -416,10 +525,27 @@ async function sendCommand(nodeId, endpointId, clusterName, commandName, args) {
|
|
|
416
525
|
if (!clusterAgent) throw new Error(`Cluster "${clusterName}" not found on endpoint ${endpointId}`);
|
|
417
526
|
const cmd = clusterAgent[commandName];
|
|
418
527
|
if (typeof cmd !== 'function') throw new Error(`Command "${commandName}" not found in cluster "${clusterName}"`);
|
|
419
|
-
//
|
|
528
|
+
// Fill neutral defaults for mandatory boilerplate fields the caller omitted.
|
|
529
|
+
const merged = args !== undefined && args !== null && typeof args === 'object' ? { ...args } : {};
|
|
530
|
+
let mandatoryFields = _mandatoryCommandFields(clusterAgent, commandName);
|
|
531
|
+
if (mandatoryFields.length === 0) {
|
|
532
|
+
_log?.debug(`matter: could not resolve request fields for ${clusterName}.${commandName}`);
|
|
533
|
+
// Last resort: every LevelControl/ColorControl command carries the two
|
|
534
|
+
// options bitmaps (and the TLV encoder ignores unknown fields), so
|
|
535
|
+
// injecting them is safe even for the few commands without them.
|
|
536
|
+
if (_clusterName(clusterName) === 'levelControl' || _clusterName(clusterName) === 'colorControl') {
|
|
537
|
+
mandatoryFields = ['optionsMask', 'optionsOverride'];
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
for (const field of mandatoryFields) {
|
|
541
|
+
if (merged[field] !== undefined) continue;
|
|
542
|
+
if (field === 'optionsMask' || field === 'optionsOverride') merged[field] = {};
|
|
543
|
+
else if (field === 'transitionTime') merged[field] = 0;
|
|
544
|
+
}
|
|
545
|
+
// Only pass args when there is actually a payload.
|
|
420
546
|
// Void commands (e.g. onOff.off) fail TLV validation if passed an empty object.
|
|
421
|
-
const hasArgs =
|
|
422
|
-
return hasArgs ? cmd.call(clusterAgent,
|
|
547
|
+
const hasArgs = Object.keys(merged).length > 0;
|
|
548
|
+
return hasArgs ? cmd.call(clusterAgent, merged) : cmd.call(clusterAgent);
|
|
423
549
|
});
|
|
424
550
|
}
|
|
425
551
|
|
|
@@ -467,7 +593,14 @@ function _broadcastNodeAttributes(node, nodeId) {
|
|
|
467
593
|
}
|
|
468
594
|
}
|
|
469
595
|
}
|
|
470
|
-
|
|
596
|
+
if (count === 0) {
|
|
597
|
+
// Endpoint structure not populated yet (e.g. right after commissioning)
|
|
598
|
+
// — drop the guard so the next lifecycle 'online' event retries.
|
|
599
|
+
_attrBroadcastNodes.delete(nodeId);
|
|
600
|
+
_log?.debug(`matter: no watchable attributes on node ${nodeId} yet, will retry on next online event`);
|
|
601
|
+
} else {
|
|
602
|
+
_log?.info(`matter: watching ${count} attribute(s) on node ${nodeId}`);
|
|
603
|
+
}
|
|
471
604
|
} catch (err) {
|
|
472
605
|
_attrBroadcastNodes.delete(nodeId); // allow retry
|
|
473
606
|
_log?.warn(`matter: attribute broadcast setup failed for ${nodeId}: ${err.message}`);
|
|
@@ -586,6 +719,8 @@ module.exports = {
|
|
|
586
719
|
getEndpoints,
|
|
587
720
|
getDeviceSubtitle,
|
|
588
721
|
getAttribute,
|
|
722
|
+
setAttribute,
|
|
723
|
+
rename,
|
|
589
724
|
sendCommand,
|
|
590
725
|
subscribeAttribute,
|
|
591
726
|
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);
|