homebridge
Version:
HomeKit support for the impatient
166 lines • 6.05 kB
JavaScript
/**
* Accessory Query
*
* Handles accessory lookup and UI display collection methods.
*/
import { Logger } from '../../logger.js';
const log = Logger.withPrefix('Matter/Server');
/**
* Add each cluster's featureMap to the declared cluster state.
*
* The declared clusters are what the PLUGIN asked for, so on their own they
* cannot tell a consumer which features the cluster ended up with - a
* thermostat with and without AutoMode declares the same setpoints, and the
* deadband is optional either way. The featureMap (named booleans, e.g.
* `{ heating: true, cooling: true, autoMode: false }`) comes off the live
* endpoint and settles that.
*
* Features never change after registration and the UI merges later state
* updates into the cluster object rather than replacing it, so sending the map
* once is enough.
*/
function withFeatureMaps(clusters, endpoint) {
const endpointState = endpoint?.state;
if (!endpointState) {
// Not registered yet (or restored without a live endpoint) - serve the
// declared state as-is and let the consumer fall back
return clusters;
}
const enriched = {};
for (const [clusterName, state] of Object.entries(clusters)) {
try {
const featureMap = endpointState[clusterName]?.featureMap;
enriched[clusterName] = featureMap !== undefined ? { featureMap, ...state } : state;
}
catch {
// Reading a behavior's state can throw while the endpoint is still
// initialising - the declared state alone is still correct
enriched[clusterName] = state;
}
}
return enriched;
}
export class AccessoryQuery {
accessories;
getAccessoryCache;
constructor(accessories, getAccessoryCache) {
this.accessories = accessories;
this.getAccessoryCache = getAccessoryCache;
}
/**
* Get all registered accessories (Plugin API)
*/
getAccessories() {
return Array.from(this.accessories.values(), (acc) => {
// eslint-disable-next-line unused-imports/no-unused-vars
const { endpoint, registered, ...publicAccessory } = acc;
return publicAccessory;
});
}
/**
* Get a specific accessory by UUID (Plugin API)
*/
getAccessory(uuid) {
const accessory = this.accessories.get(uuid);
if (!accessory) {
return undefined;
}
// eslint-disable-next-line unused-imports/no-unused-vars
const { endpoint, registered, ...publicAccessory } = accessory;
return publicAccessory;
}
/**
* Get all cached accessories (Internal - for restore process)
* @internal
*/
getAllCachedAccessories() {
const cache = this.getAccessoryCache();
if (!cache) {
log.debug('getAllCachedAccessories: No cache available');
return [];
}
const cached = cache.getAllCached();
log.debug(`getAllCachedAccessories: Returning ${cached.length} accessories`);
return cached;
}
/**
* Look up a cached accessory by UUID. O(1) Map lookup — prefer this over
* scanning getAllCachedAccessories() when only one accessory is needed.
* @internal
*/
getCachedAccessory(uuid) {
return this.getAccessoryCache()?.getCached(uuid);
}
/**
* Extract device type name from EndpointType
*/
getDeviceTypeName(deviceType) {
return deviceType.name || 'Unknown';
}
/**
* Get current cluster state for an accessory or part, enriched with each
* cluster's featureMap - see {@link withFeatureMaps}
*/
getCurrentState(uuid, partId) {
const accessory = this.accessories.get(uuid);
if (!accessory) {
return {};
}
if (partId) {
// A part carries its own endpoint, so its clusters are enriched the same
// way - a thermostat exposed as a part needs the features just as much
const part = accessory._parts?.find(p => p.id === partId);
return part ? withFeatureMaps(part.clusters || {}, part.endpoint) : {};
}
return withFeatureMaps(accessory.clusters || {}, accessory.endpoint);
}
/**
* Collect all accessories for UI display
*/
collectAccessories(bridgeUsername, bridgeType, bridgeName) {
const accessories = [];
for (const [uuid, accessory] of this.accessories.entries()) {
const transformed = {
uuid,
displayName: accessory.displayName,
deviceType: this.getDeviceTypeName(accessory.deviceType),
clusters: this.getCurrentState(uuid),
parts: accessory._parts?.map(part => ({
id: part.id,
displayName: part.displayName,
deviceType: this.getDeviceTypeName(part.deviceType),
clusters: this.getCurrentState(uuid, part.id),
})),
bridge: {
username: bridgeUsername,
type: bridgeType,
name: bridgeName,
},
};
accessories.push(transformed);
}
return accessories;
}
/**
* Get detailed info for a specific accessory
*/
getAccessoryInfo(uuid) {
const accessory = this.accessories.get(uuid);
if (!accessory) {
return undefined;
}
return {
uuid,
displayName: accessory.displayName,
deviceType: this.getDeviceTypeName(accessory.deviceType),
clusters: this.getCurrentState(uuid),
parts: accessory._parts?.map(part => ({
id: part.id,
displayName: part.displayName,
deviceType: this.getDeviceTypeName(part.deviceType),
clusters: this.getCurrentState(uuid, part.id),
})),
};
}
}
//# sourceMappingURL=AccessoryQuery.js.map