Skip to content

Commit 2b46663

Browse files
committed
Add the optional ventilation, pedestrian, and calibration options to the Closure constructor.
1 parent cb2ad7e commit 2b46663

4 files changed

Lines changed: 32 additions & 4 deletions

File tree

apps/frontend/src/components/DevicesIcons.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,20 @@ const outletDeviceTypes = [0x010a, 0x010b];
135135
const switchDeviceTypes = [0x010f, 0x0110];
136136
const currentLevelDeviceTypes = [0x0100, 0x0101, 0x010c, 0x010d, 0x010a, 0x010b, 0x0110];
137137
const fanControlDeviceTypes = [0x002b, 0x002d]; // Fan, AirPurifier
138+
const closurePositionNames = ['Closed', 'Open', 'Partial', 'Pedestrian', 'Ventilation', 'Signature'] as const;
139+
140+
/**
141+
* Gets the ClosureControl current position name from an OverallCurrentState value.
142+
*
143+
* @param {unknown} value ClosureControl OverallCurrentState attribute value.
144+
* @returns {string | undefined} The current position name, or undefined when unavailable or invalid.
145+
*/
146+
function getClosurePositionName(value: unknown): string | undefined {
147+
if (typeof value !== 'object' || value === null || !('position' in value)) return undefined;
148+
const position = value.position;
149+
if (typeof position !== 'number' || !Number.isInteger(position) || position < 0 || position >= closurePositionNames.length) return undefined;
150+
return closurePositionNames[position];
151+
}
138152

139153
interface RenderProps {
140154
icon: React.JSX.Element;
@@ -384,7 +398,7 @@ function Device({ device, endpoint, id, deviceType, clusters }: DeviceProps): Re
384398
))}
385399
{/* Closure */}
386400
{deviceType===0x0230 && clusters.filter(cluster => cluster.clusterName === 'ClosureControl' && cluster.attributeName === 'overallCurrentState').map(cluster => (
387-
<Render key={`${cluster.clusterId}-${cluster.attributeId}`} icon={<BlindsIcon/>} cluster={cluster} value='Closure' />
401+
<Render key={`${cluster.clusterId}-${cluster.attributeId}`} icon={<BlindsIcon/>} cluster={cluster} value={getClosurePositionName(cluster.attributeLocalValue)} />
388402
))}
389403
{/* ClosureController */}
390404
{deviceType===0x023e && clusters.filter(cluster => cluster.clusterName === 'Descriptor' && cluster.attributeName === 'clusterRevision').map(cluster => (

packages/core/src/demoDevices.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,15 @@ export async function createDemoDevices(matterbridge: Matterbridge): Promise<voi
303303
ep = new Closure('Closure', 'ENTRY-08-05', { id: 'Closure', number: EndpointNumber(8_05) });
304304
await registerDevice(ep, 'Closure', 'ENTRY-08-05');
305305

306+
ep = new Closure('Closure Pedestrian', 'ENTRY-08-05-1', { id: 'ClosurePedestrian', number: EndpointNumber(8_05_1), pedestrian: true });
307+
await registerDevice(ep, 'Closure Pedestrian', 'ENTRY-08-05-1');
308+
309+
ep = new Closure('Closure Ventilation', 'ENTRY-08-05-2', { id: 'ClosureVentilation', number: EndpointNumber(8_05_2), ventilation: true });
310+
await registerDevice(ep, 'Closure Ventilation', 'ENTRY-08-05-2');
311+
312+
ep = new Closure('Closure Calibrate', 'ENTRY-08-05-3', { id: 'ClosureCalibrate', number: EndpointNumber(8_05_3), calibration: true });
313+
await registerDevice(ep, 'Closure Calibrate', 'ENTRY-08-05-3');
314+
306315
ep = new MatterbridgeEndpoint([getSupportedDeviceType('ClosureController')!, bridgedNode, powerSource], { id: 'ClosureController', number: EndpointNumber(8_07) });
307316
await registerDevice(ep, 'Closure Controller', 'ENTRY-08-07');
308317

packages/core/src/frontend.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { SoilMeasurement } from '@matter/types/clusters/soil-measurement';
6262
import { ValveConfigurationAndControl } from '@matter/types/clusters/valve-configuration-and-control';
6363
import { CommissioningOptions } from '@matter/types/commissioning';
6464
import { type ClusterId, type EndpointNumber, FabricIndex } from '@matter/types/datatype';
65+
import { ThreeLevelAuto } from '@matter/types/globals';
6566
// @matterbridge
6667
import { BroadcastServer } from '@matterbridge/thread/server';
6768
import type {
@@ -1502,11 +1503,12 @@ export class Frontend extends EventEmitter<FrontendEvents> {
15021503
attributes += `Cover position: ${attributeValue / 100}% `;
15031504
if (clusterName === 'doorLock' && attributeName === 'lockState') attributes += `State: ${attributeValue === 1 ? 'Locked' : 'Not locked'} `;
15041505
if (clusterName === 'closureControl' && attributeName === 'overallCurrentState' && attributeValue === null)
1505-
attributes += `Position: unknown Latch: unknown SecureState: unknown `;
1506+
attributes += `Position: unknown Latch: unknown Speed: unknown SecureState: unknown `;
15061507
if (clusterName === 'closureControl' && attributeName === 'overallCurrentState' && isValidObject(attributeValue)) {
15071508
const overallCurrentState = attributeValue as ClosureControl.OverallCurrentState;
15081509
attributes += `Position: ${getEnumDescription(ClosureControl.CurrentPosition, overallCurrentState.position, { fallback: 'unknown' })} `;
15091510
attributes += `Latch: ${overallCurrentState.latch ?? 'unknown'} `;
1511+
attributes += `Speed: ${getEnumDescription(ThreeLevelAuto, overallCurrentState.speed, { fallback: 'unknown' })} `;
15101512
attributes += `SecureState: ${overallCurrentState.secureState ?? 'unknown'} `;
15111513
}
15121514
if (clusterName === 'thermostat' && attributeName === 'localTemperature' && isValidNumber(attributeValue)) attributes += `Temperature: ${attributeValue / 100}°C `;

packages/core/vitest/frontend.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { OperationalState } from '@matter/types/clusters/operational-state';
2828
import { PowerSource } from '@matter/types/clusters/power-source';
2929
import { RvcOperationalState } from '@matter/types/clusters/rvc-operational-state';
3030
import { EndpointNumber } from '@matter/types/datatype';
31+
import { ThreeLevelAuto } from '@matter/types/globals';
3132
import { BroadcastServer } from '@matterbridge/thread/server';
3233
import { BridgeStatus } from '@matterbridge/types';
3334
import { wait, waiter } from '@matterbridge/utils/wait';
@@ -473,7 +474,7 @@ describe('Matterbridge frontend', () => {
473474
['windowCovering', 0x102, 'currentPositionLiftPercent100ths', 0, 5000],
474475
['doorLock', 0x101, 'lockState', 0, 1],
475476
['doorLock', 0x101, 'lockState', 0, 2],
476-
['closureControl', 0x104, 'overallCurrentState', 0, { position: 0, latch: true, secureState: true }],
477+
['closureControl', 0x104, 'overallCurrentState', 0, { position: 0, latch: true, speed: ThreeLevelAuto.Auto, secureState: true }],
477478
['thermostat', 0x201, 'localTemperature', 0, 2000],
478479
['thermostat', 0x201, 'occupiedHeatingSetpoint', 0, 2100],
479480
['thermostat', 0x201, 'occupiedCoolingSetpoint', 0, 2500],
@@ -529,6 +530,7 @@ describe('Matterbridge frontend', () => {
529530
expect(text).toContain('State: Not locked');
530531
expect(text).toContain('Position: FullyClosed');
531532
expect(text).toContain('Latch: true');
533+
expect(text).toContain('Speed: Auto');
532534
expect(text).toContain('SecureState: true');
533535
expect(text).toContain('Temperature: 20°C');
534536
expect(text).toContain('Heat to: 21°C');
@@ -565,7 +567,7 @@ describe('Matterbridge frontend', () => {
565567
const nullMeasurements = runTuples([
566568
['illuminanceMeasurement', 0x400, 'measuredValue', 0, null],
567569
['closureControl', 0x104, 'overallCurrentState', 0, null],
568-
['closureControl', 0x104, 'overallCurrentState', 0, { position: null, latch: null, secureState: null }],
570+
['closureControl', 0x104, 'overallCurrentState', 0, { position: null, latch: null, speed: null, secureState: null }],
569571
['valveConfigurationAndControl', 0x81, 'currentState', 0, null],
570572
['totalVolatileOrganicCompoundsConcentrationMeasurement', 0x42e, 'measuredValue', 0, null],
571573
['pm1ConcentrationMeasurement', 0x42c, 'measuredValue', 0, null],
@@ -583,6 +585,7 @@ describe('Matterbridge frontend', () => {
583585
expect(nullMeasurements).toContain('Illuminance: unknown');
584586
expect(nullMeasurements).toContain('Position: unknown');
585587
expect(nullMeasurements).toContain('Latch: unknown');
588+
expect(nullMeasurements).toContain('Speed: unknown');
586589
expect(nullMeasurements).toContain('SecureState: unknown');
587590
expect(nullMeasurements).toContain('State: unknown');
588591
expect(nullMeasurements).toContain('Voc: unknown');

0 commit comments

Comments
 (0)