smart-home-engine 1.20.3 → 1.20.5

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-BbXC7eVn.js";/*!-----------------------------------------------------------------------------
1
+ import{m as O}from"./monaco-langs-Dt8a9eeo.js";import{t as I}from"./index-DvxIhiIs.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-BbXC7eVn.js"></script>
175
+ <script type="module" crossorigin src="/assets/index-DvxIhiIs.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-B4M4MeEk.css">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-home-engine",
3
- "version": "1.20.3",
3
+ "version": "1.20.5",
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": {
@@ -403,6 +403,13 @@ async function getAttribute(nodeId, endpointId, clusterName, attrName) {
403
403
  * Write a single attribute value (a remote write for client nodes — the
404
404
  * attribute must be writable per the device's Matter data model).
405
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
+ *
406
413
  * @param {string} nodeId
407
414
  * @param {number} endpointId
408
415
  * @param {string} clusterName camelCase cluster name, e.g. "basicInformation"
@@ -412,8 +419,14 @@ async function getAttribute(nodeId, endpointId, clusterName, attrName) {
412
419
  */
413
420
  async function setAttribute(nodeId, endpointId, clusterName, attrName, value) {
414
421
  const node = _findClientNode(nodeId);
415
- const endpoint = _resolveEndpoint(node, endpointId);
416
- await endpoint.set({ [_clusterName(clusterName)]: { [attrName]: value } });
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);
417
430
  }
418
431
 
419
432
  /**
@@ -434,18 +447,43 @@ async function rename(nodeId, name) {
434
447
 
435
448
  // ── Commands ──────────────────────────────────────────────────────────────────
436
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
+
437
455
  /**
438
456
  * 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
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])
443
467
  * @param {string} commandName
444
468
  * @returns {string[]}
445
469
  */
446
- function _mandatoryCommandFields(cluster, commandName) {
470
+ function _mandatoryCommandFields(behavior, commandName) {
447
471
  try {
448
- const cmd = cluster?.commands?.[commandName];
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
+ }
449
487
  const schema = cmd?.schema ?? cmd?.requestSchema;
450
488
  if (schema?.fieldDefinitions) {
451
489
  return Object.entries(schema.fieldDefinitions)
@@ -453,7 +491,7 @@ function _mandatoryCommandFields(cluster, commandName) {
453
491
  .map(([name]) => name);
454
492
  }
455
493
  if (schema?.children) {
456
- return [...schema.children].filter((c) => c.effectiveConformance?.isMandatory !== false).map((c) => c.name.charAt(0).toLowerCase() + c.name.slice(1));
494
+ return [...schema.children].filter((c) => c.effectiveConformance?.isMandatory !== false).map((c) => _camelize(c.name));
457
495
  }
458
496
  } catch {
459
497
  /* best-effort — fall through */
@@ -489,7 +527,17 @@ async function sendCommand(nodeId, endpointId, clusterName, commandName, args) {
489
527
  if (typeof cmd !== 'function') throw new Error(`Command "${commandName}" not found in cluster "${clusterName}"`);
490
528
  // Fill neutral defaults for mandatory boilerplate fields the caller omitted.
491
529
  const merged = args !== undefined && args !== null && typeof args === 'object' ? { ...args } : {};
492
- for (const field of _mandatoryCommandFields(clusterAgent.cluster, commandName)) {
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) {
493
541
  if (merged[field] !== undefined) continue;
494
542
  if (field === 'optionsMask' || field === 'optionsOverride') merged[field] = {};
495
543
  else if (field === 'transitionTime') merged[field] = 0;
@@ -540,6 +588,13 @@ function _broadcastNodeAttributes(node, nodeId) {
540
588
  value: _jsonSafe(value),
541
589
  ts: Date.now(),
542
590
  });
591
+ // Device/endpoint labels feed the device list — rebroadcast it
592
+ // when they change. This is also what completes a rename():
593
+ // the local mirror only learns the new nodeLabel when the
594
+ // device's subscription report arrives, i.e. exactly now.
595
+ if (attrName === 'nodeLabel' || attrName === 'productName') {
596
+ _broadcast?.({ type: 'matter:deviceList', devices: listPaired() });
597
+ }
543
598
  });
544
599
  count++;
545
600
  }