Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,21 @@ The main work (all changes without a GitHub username in brackets in the below li

- @matter/model
- Enhancement: `MatterModel.withClusters()` returns a copy of a model with clusters added or replaced by ID
- Enhancement: `FeatureSelectionErrors()` assesses a cluster's selected features against the conformance of its FeatureMap
- Fix: A cluster's own feature table no longer enables features

- @matter/node
- Enhancement: Controllers accept a custom Matter model via the `matter` option so a commissioned peer's custom or extended cluster elements resolve to real names instead of synthetic `attr$…`/`command$…` identifiers
- Enhancement: `SoftwareUpdateManager.checkForUpdates()` forces an immediate OTA update check and cleanup of obsolete stored updates
- Enhancement: Custom server session intervals (idle/active interval, active threshold) are now configurable via `sessions.intervals`
- Enhancement: Adding a server cluster to an endpoint fails when its selected features violate the conformance of its FeatureMap
- Fix: Optimize Cluster data updates when structures change for ClientNodes
- Fix: Prevent subscriptions from being activated on a closing session
- Fix: Thermostat adjusts the coupled setpoint limit to preserve the AutoMode deadband instead of rejecting the write
- Fix: Default server exports no longer inherit the features their base implementation enables internally
- Fix: `ClusterBehavior.with()` rejects a feature the cluster does not define
- Fix: A LongIdleTimeSupport ICD must select CheckInProtocolSupport and UserActiveModeTrigger
- Fix: `EnergyEvseServer` selects the mandatory ChargingPreferences feature
- Fix: Choice conformance no longer requires a member gated by an inapplicable condition (e.g. `[ICTL].b+` when the feature is unsupported)
- Fix: Support the `!=` (not-equal) operator in value conformance expressions
- Fix: Ensure `FabricAuthority` is fully constructed before it is used in the WebRTC Transport Requestor, OTA Software Update Provider and Software Update clusters
Expand Down
161 changes: 161 additions & 0 deletions packages/model/src/logic/FeatureSelectionErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* @license
* Copyright 2022-2026 Matter.js Authors
* SPDX-License-Identifier: Apache-2.0
*/

import { Conformance } from "../aspects/Conformance.js";
import { ClusterModel } from "../models/ClusterModel.js";
import { FieldModel } from "../models/FieldModel.js";

interface Choice {
target: number;
orMore?: boolean;
orLess?: boolean;
members: string[];
count: number;
}

/**
* Assess a cluster's supported features against the conformance of its FeatureMap.
*
* The Matter specification constrains feature selection in three ways: a feature may be mandatory given other features,
* disallowed given other features, or a member of a choice group with a required cardinality. Applications select
* features so these constraints are only knowable once a selection is made.
*
* Conformance we cannot evaluate yields no error. This is advisory validation of an application's choices, so a
* conformance form we do not understand must not prevent the cluster from operating.
*
* @param cluster the cluster whose {@link ClusterModel.supportedFeatures} to assess
* @returns one message per violation, empty if the selection conforms
*/
export function FeatureSelectionErrors(cluster: ClusterModel): string[] {
const features = cluster.features;
if (!features.length) {
return [];
}

const context = { definedFeatures: cluster.definedFeatures, supportedFeatures: cluster.supportedFeatures };

const errors = Array<string>();
const choices = {} as Record<string, Choice>;

for (const feature of features) {
const isSupported = context.supportedFeatures.has(feature.name);
const conformance = feature.effectiveConformance;

// Provisional and deprecated elements assess as inapplicable but remain selectable, as elsewhere in conformance
// handling
if (!isProvisionalOrDeprecated(conformance.ast)) {
switch (applicabilityOf(conformance, context)) {
case Conformance.Applicability.Mandatory:
if (!isSupported) {
errors.push(
`feature ${titleOf(feature)} is mandatory for the selected features but is not selected`,
);
}
break;

case Conformance.Applicability.None:
if (isSupported) {
errors.push(`feature ${titleOf(feature)} is not allowed with the selected features`);
}
break;
}
}

collectChoices(feature, conformance.ast, context, choices, isSupported);
}

for (const name in choices) {
const { target, orMore, orLess, members, count } = choices[name];

if ((count < target && !orLess) || (count > target && !orMore)) {
errors.push(
`select ${describeCardinality(target, orMore, orLess)} of ${members.join(", ")} (${count} selected)`,
);
}
}

return errors;
}

function titleOf(feature: FieldModel) {
return feature.title ?? feature.name;
}

function describeCardinality(target: number, orMore?: boolean, orLess?: boolean) {
if (orMore) {
return `at least ${target}`;
}
if (orLess) {
return `at most ${target}`;
}
return `exactly ${target}`;
}

function isProvisionalOrDeprecated(ast: Conformance.Ast) {
return ast.type === Conformance.Flag.Provisional || ast.type === Conformance.Flag.Deprecated;
}

/**
* Conformance rejects some legal expressions it cannot assess statically, such as a choice nested in an optional group.
*/
function applicabilityOf(conformance: Conformance, context: Conformance.FeatureContext) {
try {
return conformance.applicabilityFor(context);
} catch {
return Conformance.Applicability.Conditional;
}
}

/**
* Register the choice groups a feature participates in.
*
* A feature only joins a group when its gating expression applies; otherwise it neither counts toward the group nor
* constrains it. For the same reason only the first applicable branch of an "otherwise" list contributes, as later
* branches do not govern the feature.
*/
function collectChoices(
feature: FieldModel,
ast: Conformance.Ast,
context: Conformance.FeatureContext,
choices: Record<string, Choice>,
isSupported: boolean,
) {
switch (ast.type) {
case Conformance.Special.Otherwise:
for (const node of ast.param) {
if (applicabilityOf(new Conformance({ ast: node }), context) !== Conformance.Applicability.None) {
collectChoices(feature, node, context, choices, isSupported);
return;
}
}
return;

case Conformance.Special.Choice:
break;

default:
return;
}

const { name, num, orMore, orLess, expr } = ast.param;

if (applicabilityOf(new Conformance({ ast: expr }), context) === Conformance.Applicability.None) {
return;
}

const choice = (choices[name] ??= { target: num, members: [], count: 0 });

// Members of one group should agree on cardinality. If they do not we take the most permissive reading rather than
// rejecting a selection based on declaration order
choice.target = Math.max(choice.target, num);
choice.orMore ||= orMore;
choice.orLess ||= orLess;

choice.members.push(titleOf(feature));
if (isSupported) {
choice.count++;
}
}
1 change: 1 addition & 0 deletions packages/model/src/logic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from "./ClusterVariance.js";
export * from "./DecodedBitmap.js";
export * from "./DefaultValue.js";
export * from "./EncodedBitmap.js";
export * from "./FeatureSelectionErrors.js";
export * from "./GeneratorScope.js";
export * from "./MergedModel.js";
export * from "./ModelDiff.js";
Expand Down
2 changes: 1 addition & 1 deletion packages/model/src/standard/elements/groups.element.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions packages/model/src/standard/elements/level-control.element.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading