homebridge
Version:
HomeKit support for the impatient
177 lines • 9.32 kB
JavaScript
/**
* WindowCovering Cluster Behavior
*
* Handles window covering commands for blinds, shades, and curtains
*/
import { WindowCoveringServer } from '@matter/main/behaviors/window-covering';
import { Status, StatusResponseError } from '@matter/main/types';
import { MatterStatus } from '../errors.js';
import { getRegistryManager } from './EndpointContext.js';
/**
* Feature-rich variant of the public WindowCoveringServer.
*
* We extend the public `WindowCoveringServer` (not the internal
* `WindowCoveringBaseServer`) per apollon77's guidance on homebridge#3905 — the
* Base class exists so matter.js's default-implementation methods have a concrete
* feature set to compile against, and consumers shouldn't depend on it. Instead,
* we declare the superset of features our overrides need via `.with(...)`. At
* endpoint-attachment time, matter.js still narrows the effective feature set to
* whatever the device type declares, so an endpoint that declares only Lift won't
* advertise Tilt commands.
*/
const FeatureRichWindowCoveringServer = WindowCoveringServer.with('Lift', 'Tilt', 'PositionAwareLift', 'PositionAwareTilt');
/**
* WindowCovering state property names
* These correspond to the Matter.js WindowCovering cluster attribute names
*/
const WindowCoveringStateProps = {
targetPositionLiftPercent100ths: 'targetPositionLiftPercent100ths',
currentPositionLiftPercent100ths: 'currentPositionLiftPercent100ths',
targetPositionTiltPercent100ths: 'targetPositionTiltPercent100ths',
currentPositionTiltPercent100ths: 'currentPositionTiltPercent100ths',
};
/**
* Custom WindowCovering Server that calls plugin handlers.
*/
export class HomebridgeWindowCoveringServer extends FeatureRichWindowCoveringServer {
/**
* Get the registry for this behavior's endpoint
*/
getRegistry() {
return getRegistryManager(this.endpoint).getRegistry(this.endpoint.id);
}
/**
* Sync window covering position state to cache
* @param endpointId - The endpoint ID
* @param targetProperty - Target position property name (e.g., 'targetPositionLiftPercent100ths')
* @param currentProperty - Current position property name (e.g., 'currentPositionLiftPercent100ths')
*/
syncPositionStateToCache(endpointId, targetProperty, currentProperty) {
const registry = this.getRegistry();
const currentState = this.state;
const stateUpdate = {};
if (currentState[targetProperty] !== undefined) {
stateUpdate[targetProperty] = currentState[targetProperty];
}
if (currentState[currentProperty] !== undefined) {
stateUpdate[currentProperty] = currentState[currentProperty];
}
registry.syncStateToCache(endpointId, 'windowCovering', stateUpdate);
}
async upOrOpen() {
const endpointId = this.endpoint.id;
const registry = this.getRegistry();
try {
// Execute user handler
await registry.executeHandler(endpointId, 'windowCovering', 'upOrOpen');
// Only reached if handler succeeded - update Matter state
await super.upOrOpen();
// Sync state to cache - window covering opening
this.syncPositionStateToCache(endpointId, WindowCoveringStateProps.targetPositionLiftPercent100ths, WindowCoveringStateProps.currentPositionLiftPercent100ths);
}
catch (error) {
// If user handler already threw a StatusResponseError, propagate it as-is
// This sends a proper Matter protocol error response to the controller
if (MatterStatus.isMatterProtocolError(error)) {
throw error;
}
// For other errors, wrap in appropriate StatusResponseError
// This prevents the endpoint from crashing and keeps the device online
const message = error instanceof Error ? error.message : String(error);
throw new StatusResponseError(`Failed to open window covering: ${message}`, Status.Failure);
}
}
async downOrClose() {
const endpointId = this.endpoint.id;
const registry = this.getRegistry();
try {
// Execute user handler
await registry.executeHandler(endpointId, 'windowCovering', 'downOrClose');
// Only reached if handler succeeded - update Matter state
await super.downOrClose();
// Sync state to cache - window covering closing
this.syncPositionStateToCache(endpointId, WindowCoveringStateProps.targetPositionLiftPercent100ths, WindowCoveringStateProps.currentPositionLiftPercent100ths);
}
catch (error) {
// If user handler already threw a StatusResponseError, propagate it as-is
// This sends a proper Matter protocol error response to the controller
if (MatterStatus.isMatterProtocolError(error)) {
throw error;
}
// For other errors, wrap in appropriate StatusResponseError
// This prevents the endpoint from crashing and keeps the device online
const message = error instanceof Error ? error.message : String(error);
throw new StatusResponseError(`Failed to close window covering: ${message}`, Status.Failure);
}
}
async stopMotion() {
const endpointId = this.endpoint.id;
const registry = this.getRegistry();
try {
// Execute user handler
await registry.executeHandler(endpointId, 'windowCovering', 'stopMotion');
// Only reached if handler succeeded - update Matter state
await super.stopMotion();
// Sync state to cache - window covering stopped
this.syncPositionStateToCache(endpointId, WindowCoveringStateProps.targetPositionLiftPercent100ths, WindowCoveringStateProps.currentPositionLiftPercent100ths);
}
catch (error) {
// If user handler already threw a StatusResponseError, propagate it as-is
// This sends a proper Matter protocol error response to the controller
if (MatterStatus.isMatterProtocolError(error)) {
throw error;
}
// For other errors, wrap in appropriate StatusResponseError
// This prevents the endpoint from crashing and keeps the device online
const message = error instanceof Error ? error.message : String(error);
throw new StatusResponseError(`Failed to stop window covering: ${message}`, Status.Failure);
}
}
async goToLiftPercentage(request) {
const endpointId = this.endpoint.id;
const registry = this.getRegistry();
try {
// Execute user handler
await registry.executeHandler(endpointId, 'windowCovering', 'goToLiftPercentage', request);
// Only reached if handler succeeded - update Matter state
await super.goToLiftPercentage(request);
// Sync state to cache - window covering moving to target position
this.syncPositionStateToCache(endpointId, WindowCoveringStateProps.targetPositionLiftPercent100ths, WindowCoveringStateProps.currentPositionLiftPercent100ths);
}
catch (error) {
// If user handler already threw a StatusResponseError, propagate it as-is
// This sends a proper Matter protocol error response to the controller
if (MatterStatus.isMatterProtocolError(error)) {
throw error;
}
// For other errors, wrap in appropriate StatusResponseError
// This prevents the endpoint from crashing and keeps the device online
const message = error instanceof Error ? error.message : String(error);
throw new StatusResponseError(`Failed to set window covering position: ${message}`, Status.Failure);
}
}
async goToTiltPercentage(request) {
const endpointId = this.endpoint.id;
const registry = this.getRegistry();
try {
// Execute user handler
await registry.executeHandler(endpointId, 'windowCovering', 'goToTiltPercentage', request);
// Only reached if handler succeeded - update Matter state
await super.goToTiltPercentage(request);
// Sync state to cache - window covering tilting to target angle
this.syncPositionStateToCache(endpointId, WindowCoveringStateProps.targetPositionTiltPercent100ths, WindowCoveringStateProps.currentPositionTiltPercent100ths);
}
catch (error) {
// If user handler already threw a StatusResponseError, propagate it as-is
// This sends a proper Matter protocol error response to the controller
if (MatterStatus.isMatterProtocolError(error)) {
throw error;
}
// For other errors, wrap in appropriate StatusResponseError
// This prevents the endpoint from crashing and keeps the device online
const message = error instanceof Error ? error.message : String(error);
throw new StatusResponseError(`Failed to set window covering tilt: ${message}`, Status.Failure);
}
}
}
//# sourceMappingURL=WindowCoveringBehavior.js.map