matterbridge-hass
Version:
Matterbridge hass plugin
234 lines (233 loc) • 12.6 kB
JavaScript
import { createHash, randomBytes } from 'node:crypto';
import { colorTemperatureLight, colorTemperatureSwitch, dimmableLight, dimmableOutlet, dimmableSwitch, MatterbridgeEndpoint, onOffLight, onOffOutlet, onOffSwitch, } from 'matterbridge';
import { db, debugStringify, idn, ign, rs } from 'matterbridge/logger';
import { VendorId, ClusterRegistry } from 'matterbridge/matter/types';
import { BridgedDeviceBasicInformation } from 'matterbridge/matter/clusters';
import { BridgedDeviceBasicInformationServer } from 'matterbridge/matter/behaviors';
import { CYAN } from 'node-ansi-logger';
export function getClusterServerObj(clusterId, type, options) {
return { id: clusterId, type, options };
}
export class MutableDevice {
mutableDevice = new Map();
matterbridge;
deviceName;
serialNumber;
vendorId;
vendorName;
productName;
softwareVersion;
softwareVersionString;
hardwareVersion;
hardwareVersionString;
composedType = undefined;
constructor(matterbridge, deviceName, serialNumber, vendorId = 0xfff1, vendorName = 'Matterbridge', productName = 'Matterbridge Device', softwareVersion, softwareVersionString, hardwareVersion, hardwareVersionString) {
this.matterbridge = matterbridge;
this.deviceName = deviceName;
this.serialNumber = serialNumber ?? '0x' + randomBytes(8).toString('hex');
this.vendorId = VendorId(vendorId);
this.vendorName = vendorName;
this.productName = productName;
this.softwareVersion = softwareVersion ?? parseInt(matterbridge.matterbridgeVersion.replace(/\D/g, ''));
this.softwareVersionString = softwareVersionString ?? matterbridge.matterbridgeVersion;
this.hardwareVersion = hardwareVersion ?? parseInt(this.matterbridge.systemInformation.nodeVersion.replace(/\D/g, ''));
this.hardwareVersionString = hardwareVersionString ?? this.matterbridge.systemInformation.nodeVersion;
this.initializeEndpoint('');
}
has(endpoint) {
return this.mutableDevice.has(endpoint);
}
get(endpoint = '') {
if (this.mutableDevice.get(endpoint) === undefined)
throw new Error(`Device ${endpoint} is not defined`);
return this.mutableDevice.get(endpoint);
}
getEndpoint(endpoint = '') {
if (this.mutableDevice.get(endpoint)?.endpoint === undefined)
throw new Error(`Device ${endpoint} endpoint is not defined`);
return this.mutableDevice.get(endpoint)?.endpoint;
}
initializeEndpoint(endpoint) {
if (!this.mutableDevice.has(endpoint)) {
this.mutableDevice.set(endpoint, {
friendlyName: endpoint,
tagList: [],
deviceTypes: [],
clusterServersIds: [],
clusterServersObjs: [],
clusterClientsIds: [],
clusterClientsObjs: [],
});
}
return this.mutableDevice.get(endpoint);
}
setFriendlyName(endpoint, friendlyName) {
const device = this.initializeEndpoint(endpoint);
device.friendlyName = friendlyName;
return this;
}
addTagLists(endpoint, ...tagList) {
const device = this.initializeEndpoint(endpoint);
device.tagList.push(...tagList);
}
addDeviceTypes(endpoint, ...deviceTypes) {
const device = this.initializeEndpoint(endpoint);
device.deviceTypes.push(...deviceTypes);
}
addClusterServerIds(endpoint, ...clusterServerIds) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersIds.push(...clusterServerIds);
}
addClusterServerObjs(endpoint, ...clusterServerObj) {
const device = this.initializeEndpoint(endpoint);
device.clusterServersObjs.push(...clusterServerObj);
}
createUniqueId(param1, param2, param3, param4) {
const hash = createHash('md5');
hash.update(param1 + param2 + param3 + param4);
return hash.digest('hex');
}
addBridgedDeviceBasicInformationClusterServer() {
const device = this.getEndpoint('');
device.log.logName = this.deviceName;
device.deviceName = this.deviceName;
device.serialNumber = this.serialNumber;
device.uniqueId = this.createUniqueId(this.deviceName, this.serialNumber, this.vendorName, this.productName);
device.productId = undefined;
device.productName = this.productName;
device.vendorId = this.vendorId;
device.vendorName = this.vendorName;
device.softwareVersion = this.softwareVersion;
device.softwareVersionString = this.softwareVersionString;
device.hardwareVersion = this.hardwareVersion;
device.hardwareVersionString = this.hardwareVersionString;
this.addClusterServerObjs('', getClusterServerObj(BridgedDeviceBasicInformation.Cluster.id, BridgedDeviceBasicInformationServer, {
vendorId: this.vendorId,
vendorName: this.vendorName.slice(0, 32),
productName: this.productName.slice(0, 32),
productLabel: this.deviceName.slice(0, 64),
nodeLabel: this.deviceName.slice(0, 32),
serialNumber: this.serialNumber.slice(0, 32),
uniqueId: this.createUniqueId(this.deviceName, this.serialNumber, this.vendorName, this.productName),
softwareVersion: this.softwareVersion,
softwareVersionString: this.softwareVersionString.slice(0, 64),
hardwareVersion: this.hardwareVersion,
hardwareVersionString: this.hardwareVersionString.slice(0, 64),
reachable: true,
}));
}
async create() {
await this.createMainEndpoint();
await this.createChildEndpoints();
for (const [endpoint] of this.mutableDevice) {
await this.createClusters(endpoint);
}
const mainDevice = this.mutableDevice.get('');
return mainDevice.endpoint;
}
removeDuplicateAndSupersetDeviceTypes() {
for (const device of this.mutableDevice.values()) {
const deviceTypesMap = new Map();
device.deviceTypes.forEach((deviceType) => {
deviceTypesMap.set(deviceType.code, deviceType);
});
if (deviceTypesMap.has(onOffSwitch.code) && deviceTypesMap.has(dimmableSwitch.code))
deviceTypesMap.delete(onOffSwitch.code);
if (deviceTypesMap.has(dimmableSwitch.code) && deviceTypesMap.has(colorTemperatureSwitch.code))
deviceTypesMap.delete(dimmableSwitch.code);
if (deviceTypesMap.has(onOffOutlet.code) && deviceTypesMap.has(dimmableOutlet.code))
deviceTypesMap.delete(onOffOutlet.code);
if (deviceTypesMap.has(onOffLight.code) && deviceTypesMap.has(dimmableLight.code))
deviceTypesMap.delete(onOffLight.code);
if (deviceTypesMap.has(dimmableLight.code) && deviceTypesMap.has(colorTemperatureLight.code))
deviceTypesMap.delete(dimmableLight.code);
device.deviceTypes = Array.from(deviceTypesMap.values());
}
}
async createMainEndpoint() {
this.removeDuplicateAndSupersetDeviceTypes();
const mainDevice = this.mutableDevice.get('');
mainDevice.friendlyName = this.deviceName;
mainDevice.endpoint = new MatterbridgeEndpoint(mainDevice.deviceTypes, { uniqueStorageKey: this.deviceName }, true);
mainDevice.endpoint.log.logName = this.deviceName;
return mainDevice.endpoint;
}
async createChildEndpoint(endpoint) {
this.removeDuplicateAndSupersetDeviceTypes();
const mainDevice = this.mutableDevice.get('');
if (!mainDevice.endpoint)
throw new Error('Main endpoint is not defined. Call createMainEndpoint() first.');
const device = this.mutableDevice.get(endpoint);
if (!device)
throw new Error(`Device ${endpoint} is not defined.`);
device.endpoint = mainDevice.endpoint.addChildDeviceType(endpoint, device.deviceTypes, device.tagList.length ? { tagList: device.tagList } : {}, true);
device.endpoint.log.logName = device.friendlyName;
return device.endpoint;
}
async createChildEndpoints() {
this.removeDuplicateAndSupersetDeviceTypes();
const mainDevice = this.mutableDevice.get('');
if (!mainDevice.endpoint)
throw new Error('Main endpoint is not defined. Call createMainEndpoint() first.');
for (const [endpoint, device] of Array.from(this.mutableDevice.entries()).filter(([endpoint]) => endpoint !== '')) {
device.endpoint = mainDevice.endpoint.addChildDeviceType(endpoint, device.deviceTypes, device.tagList.length ? { tagList: device.tagList } : {}, true);
}
return this;
}
removeDuplicateClusterServers() {
for (const device of this.mutableDevice.values()) {
const deviceClusterServersIdMap = new Map();
device.clusterServersIds.forEach((clusterServerId) => {
deviceClusterServersIdMap.set(clusterServerId, clusterServerId);
});
const deviceClusterServersObjMap = new Map();
device.clusterServersObjs.forEach((clusterServerObj) => {
deviceClusterServersIdMap.delete(clusterServerObj.id);
deviceClusterServersObjMap.set(clusterServerObj.id, clusterServerObj);
});
device.clusterServersIds = Array.from(deviceClusterServersIdMap.values());
device.clusterServersObjs = Array.from(deviceClusterServersObjMap.values());
}
}
async createClusters(endpoint) {
this.removeDuplicateClusterServers();
if (endpoint === '') {
const mainDevice = this.mutableDevice.get(endpoint);
if (!mainDevice.endpoint)
throw new Error('Main endpoint is not defined');
this.addBridgedDeviceBasicInformationClusterServer();
for (const clusterServerObj of mainDevice.clusterServersObjs) {
mainDevice.endpoint.behaviors.require(clusterServerObj.type, clusterServerObj.options);
}
mainDevice.endpoint.addClusterServers(mainDevice.clusterServersIds);
mainDevice.endpoint.addRequiredClusterServers();
if (this.composedType)
await mainDevice.endpoint.addFixedLabel('composed', this.composedType);
return this;
}
for (const [, device] of Array.from(this.mutableDevice.entries()).filter(([e]) => e === endpoint)) {
if (!device.endpoint)
throw new Error('Child endpoint is not defined');
for (const clusterServerObj of device.clusterServersObjs) {
device.endpoint.behaviors.require(clusterServerObj.type, clusterServerObj.options);
}
device.endpoint.addClusterServers(device.clusterServersIds);
device.endpoint.addRequiredClusterServers();
}
return this;
}
logMutableDevice() {
this.matterbridge.log.debug(`Device ${idn}${this.deviceName}${rs}${db} serial number ${CYAN}${this.serialNumber}${rs}${db} vendor id ${CYAN}${this.vendorId}${rs}${db} ` +
`vendor name ${CYAN}${this.vendorName}${rs}${db} product name ${CYAN}${this.productName}${rs}${db} software version ${CYAN}${this.softwareVersion}${rs}${db} ` +
`software version string ${CYAN}${this.softwareVersionString}${rs}${db} hardware version ${CYAN}${this.hardwareVersion}${rs}${db} hardware version string ${CYAN}${this.hardwareVersionString}`);
for (const [endpoint, device] of this.mutableDevice) {
const deviceTypes = device.deviceTypes.map((d) => '0x' + d.code.toString(16) + '-' + d.name);
const clusterServersIds = device.clusterServersIds.map((clusterServerId) => '0x' + clusterServerId.toString(16) + '-' + ClusterRegistry.get(clusterServerId)?.name);
const clusterServersObjsIds = device.clusterServersObjs.map((clusterServerObj) => '0x' + clusterServerObj.id.toString(16) + '-' + ClusterRegistry.get(clusterServerObj.id)?.name);
this.matterbridge.log.debug(`- endpoint: ${ign}${endpoint === '' ? 'main' : endpoint}${rs}${db} => friendlyName ${CYAN}${device.friendlyName}${db} ` +
`${db}tagList: ${debugStringify(device.tagList)}${db} deviceTypes: ${debugStringify(deviceTypes)}${db} ` +
`clusterServersIds: ${debugStringify(clusterServersIds)}${db} clusterServersObjs: ${debugStringify(clusterServersObjsIds)}${db}`);
}
return this;
}
}