@pmouli/isy-matter-server
Version:
Service to expose an ISY device as a Matter Border router
396 lines (334 loc) • 15.3 kB
text/typescript
import { type Behavior, ClusterBehavior, MutableEndpoint, SupportedBehaviors } from '@matter/main';
import { type FixedLabelBehavior, type UserLabelBehavior, BridgedDeviceBasicInformationBehavior, DescriptorBehavior, DescriptorServer, FixedLabelServer, UserLabelServer } from '@matter/node/behaviors';
import { Devices } from 'isy-nodejs/ISY';
import { Feature } from 'isy-nodejs/Definitions/Global/Features';
import type { CamelCase, JsonPrimitive, Simplify, StringKeyOf } from 'type-fest';
//import type { FanFanDevice } from 'isy-nodejs/Devices/Insteon/InsteonFanDevice';
import { Descriptor } from '@matter/main/clusters';
import type { ClusterType } from '@matter/main/types';
import type { Converter } from 'isy-nodejs/Converters';
import { Devices as DevicesNS, Family } from 'isy-nodejs/ISY';
import { ISYDevice } from 'isy-nodejs/ISYDevice';
import { CommandsOf, DriversOf, ISYNode } from 'isy-nodejs/ISYNode';
import { ISYBridgedDeviceBehavior } from '../Behaviors/ISYBridgedDeviceBehavior.js';
// #region Type aliases (16)
export type AttributeMapping<B, D> = B extends { cluster: { attributes: infer E extends { [K in string]: ClusterType.Attribute } } } ? Partial<Record<keyof E, keyof DriversOf<D> | { driver: keyof DriversOf<D>; converter?: Converter.KnownConverters }>> : never;
export type ClusterMapping<B, T> =
B extends { cluster } ?
{
attributes?: ClusterAttributeMapping<B['cluster'], T>;
commands?: ClusterCommandMapping<B['cluster'], T>;
}
: { attributes?: ClusterAttributeMapping<unknown, T>; commands?: ClusterCommandMapping<unknown, T> };
export type ClusterAttributeMapping<A, K> = {
[key in keyof ClusterType.AttributesOf<A>]?: DriverMapping<K>;
};
export type DriverMapping<K> =
| {
driver: ISYDevice.DriverNamesOf<K>;
converter?: Converter.KnownConverters | ((value: any) => JsonPrimitive);
node?: string;
}
| ISYDevice.DriverNamesOf<K>
| { value: string | number | boolean };
// export type ClusterTypeCommandMapping<A extends ClusterType, K> = {
// [key in keyof Clusters.ClusterType.CommandsOf<ToCompleteClusterByName<A>>]?:
// | { command: CommandsOf<K>; parameters?: parameterMapping }
// | CommandsOf<K>
// };
export type ClusterCommandMapping<A, K> = {
[key in keyof ClusterType.CommandsOf<A>]?: { command: ISYDevice.CommandNamesOf<K>; parameters?: parameterMapping } | ISYDevice.CommandNamesOf<K>;
};
//export type FamilyToDeviceMap<T extends Family> = Record<keyof Devices<T>, DeviceToClusterMap<ISYDevice<T>>>;
// export type ClusterTypeMapping<A extends ClusterType,K> = {
// attributes: ClusterTypeAttributeMapping<A,K>,
// commands: ClusterTypeCommandMapping<A,K>
// };
/*export type ClusterMapping<A, K> = {
attributes: ClusterAttributeMapping<A, K>;
commands: ClusterCommandMapping<A, K>;
};*/
export type CommandMapping<B, D> =
B extends (
{
cluster: { commands: infer E extends { [K in string]: ClusterType.Command } };
}
) ?
Partial<Record<keyof E, keyof ISYNode.CommandsOf<D> | { command: keyof ISYNode.CommandsOf<D>; parameters?: parameterMapping }>>
: never;
// `${const ClusterList = Object.keys(Clusters).filter(p => p instanceof Clusters.Cluster).map(p => p.constructor.name);
// var s = {...ClusterList};
// type ClusterName = ClusterList[0] | ClusterList[1];
// var ColorControl : ClusterName = "ColorControl";}`
// type ChildrenOf<T> = T extends Family.Global ? Family | ISYDevice | string :
// T extends Family ? ISYDevice | string : string;
// export type ClusterMap<T extends Family | ISYDevice<Family> | string> = T extends ISYDevice<Family>
// ? {
// deviceType: Partial<Device>;
// scope?: ChildrenOf<T>;
// mapping: [ClusterTypeMapping<ClusterType,T>];
// behavior?: typeof ClusterBehavior;
// }
// : {
// scope?: ChildrenOf<T>;
// mapping: [ClusterTypeMapping<ClusterType,any>];
// behavior?: typeof ClusterBehavior;
// };
//ype GenericCluster = Clusters.OnOffCluster | Clusters.ColorControl.Complete | Clusters.LevelControl.Cluster;
// export type DeviceToClusterMap<T extends ISYDevice<Family,any,any>> =
// {
// deviceType: Partial<Device>;
// scope?: string;
// mapping: {[Type in ClusterType]?:ClusterTypeMapping<Type,T>};
// behavior?: typeof ClusterBehavior;
// }
export interface DeviceToClusterMap<T, D extends MutableEndpoint> {
deviceType: D;
mapping: Simplify<EndpointMapping<D, T>>;
}
export interface Mapping<T extends ISYNode, D extends MutableEndpoint> {
deviceType: D;
features: Feature;
nodeType?: T;
mapping?: {
[K in keyof D['behaviors']]?: {
attributes?: {
[K2 in keyof ClusterType.AttributesOf<D['behaviors'][K]>]: { driver: keyof DriversOf<T>; converter?: Converter.KnownConverters } | keyof DriversOf<T>;
};
commands?: {
[K2 in keyof ClusterType.CommandsOf<D['behaviors'][K]>]: { command: keyof CommandsOf<T>; converter?: Converter.KnownConverters } | keyof CommandsOf<T>;
};
};
};
}
export type EndpointMapping<A extends MutableEndpoint, D> = {
[K in StringKeyOf<A['requirements']['server']['mandatory']> as CamelCase<K>]?: ClusterMapping<A['requirements']['server']['mandatory'][K], D>;
} & {
[K in StringKeyOf<A['requirements']['server']['optional']> as CamelCase<K>]?: ClusterMapping<A['requirements']['server']['optional'][K], D>;
} & {
[K in StringKeyOf<A['requirements']['client']['mandatory']> as CamelCase<K>]?: ClusterMapping<A['requirements']['client']['mandatory'][K], D>;
} & {
[K in StringKeyOf<A['requirements']['client']['optional']> as CamelCase<K>]?: ClusterMapping<A['requirements']['client']['optional'][K], D>;
};
/*&
{[K in CamelCase<ExtractKeys<A['requirements']['server']['optional'], { cluster }}>>]?: Simplify<
ClusterMapping<
//@ts-expect-error
A['requirements']['server']['optional'],
D
>
>}>*/
//{
/*attributes?: AttributeMapping<A['behaviors'][Uncapitalize<K>], D>;
commands?: CommandMapping<A['behaviors'][Uncapitalize<K>], D>;*/
//}>;
////const ClusterIdentifier = Object.values(Clusters).filter(p=> p instanceof Clusters.MutableCluster && typeof p == "object" && p.constructor.name.endsWith(".Cluster"));
//type clusterList = keyof typeof ClusterIdentifier;
// export type ClusterMappings = {
// OnOffCluster: ClusterTypeMapping<ClusterType.OnOff,ISYDevice<Family>>
// }
//| typeof ISYDevice<any, infer B, any>
// export type ClusterTypeAttributeMapping<A extends ClusterType, K> = {
// [key in keyof Clusters.ClusterType.AttributesOf<ToCompleteClusterByName<A>>]?:
// | { driver: DriversOf<K>; converter?: string }
// | DriversOf<K>;
// };
export type EndpointMapping1<A extends MutableEndpoint, K> = {
attributes?: SBAttributeMapping<A['behaviors'], K>;
commands?: SBCommandMapping<A['behaviors'], K>;
};
//@ts-ignore
type Devices = typeof DevicesNS;
type SupportedFamily = keyof Devices & keyof typeof Family;
export type FamilyToClusterMap<T extends SupportedFamily> = { Family: T } & {
[Type in keyof Devices[T]]?: DeviceToClusterMap<Devices[T][Type], MutableEndpoint>;
} & { add<D extends keyof Devices[T], M extends MutableEndpoint>(mapping: { [x in D]?: DeviceToClusterMap<Devices[T][x], M> }): FamilyToClusterMap<T> };
export function add<const F extends SupportedFamily, const Y extends keyof Devices[F], const D extends MutableEndpoint>(This: FamilyToClusterMap<F> & object, mapping: { [x in Y]?: DeviceToClusterMap<Devices[F][x], D> }): FamilyToClusterMap<F> {
let m = { ...This, ...mapping } as FamilyToClusterMap<F> & object;
//@ts-ignore
let s = {
...m,
add<K extends keyof Devices[F], const M extends MutableEndpoint>(mapping2: { [x in K]?: DeviceToClusterMap<Devices[F][x], M> }) {
return add<F, K, M>(m, mapping2);
}
};
return s;
}
export function hasPath<X extends string, Y>(mapping: DriverMapping<any>): mapping is { driver: X; converter?: Y; node?: string } {
if (typeof mapping == 'object' && 'driver' in mapping) {
const driver = mapping.driver;
if (typeof driver === 'string') {
return true;
}
}
return false;
}
export type SBAttributeMapping<SB extends SupportedBehaviors, D> = {
[K in keyof SB]: Partial<Record<any, DriversOf<D> | { driver: DriversOf<D>; converter?: string }>>;
};
export type SBCommandMapping<SB extends SupportedBehaviors, D> = {
//@ts-expect-error
[K in Capitalize<keyof SB>]?: SB[Uncapitalize<K>] extends { cluster: { commands } } ? Partial<Record<string, CommandsOf<D> | { driver: DriversOf<D>; converter?: string }>> : never;
};
export type parameterMapping = {
[key: string]: { parameter: string; converter?: string };
};
// #endregion Type aliases (16)
// #region Classes (1)
//@ts-ignore
const BaseDescriptorServer = DescriptorServer.withFeatures(Descriptor.Feature.TagList);
export class MappingRegistry {
// #region Properties (1)
public static map: Map<keyof typeof Family, Map<string, DeviceToClusterMap<ISYDevice.Any, MutableEndpoint>>> = new Map();
public static cache: { [x: string]: DeviceToClusterMap<ISYDevice.Any, MutableEndpoint> } = {};
// #endregion Properties (1)
// #region Public Static Methods (3)
public static getMapping<const T extends ISYDevice.Any = ISYDevice.Any, M extends MutableEndpoint = MutableEndpoint>(device: T): DeviceToClusterMap<T, MutableEndpoint.With<M, { isyNode: typeof ISYBridgedDeviceBehavior<T, M>; descriptor: typeof BaseDescriptorServer; bridgedDeviceBasicInformation: typeof BridgedDeviceBasicInformationBehavior; userLabel: typeof UserLabelBehavior; fixedLabel: typeof FixedLabelBehavior }>> {
let m = this.cache[device.address];
if (!m) {
if (device) {
if (MappingRegistry.map.has(Family[device.family] as keyof typeof Family)) {
let g = MappingRegistry.map.get(Family[device.family] as keyof typeof Family);
//let m: DeviceToClusterMap<T,MutableEndpoint>;
if (g.has(device.constructor.name)) {
m = g.get(device.constructor.name);
} else if (g.has(device.type)) {
m = g.get(device.type);
} else if (ISYDevice.isNode(device)) {
if (g.has(device.nodeDefId)) {
m = g.get(device.nodeDefId);
}
if (!m) {
for (var nodeDefId of ISYNode.getImplements(device)) {
if (g.has(nodeDefId)) {
device.logger(`Mapping found to ${Family[device.family]}.${nodeDefId}`, 'info');
m = g.get(nodeDefId);
g.set(device.nodeDefId, m);
break;
}
}
}
}
if (m !== null) this.cache[device.address] = m;
}
}
}
return m as any;
}
public static getMappingForBehavior<T extends ISYDevice.Any, const B extends ClusterBehavior>(device: T, behavior: B): ClusterMapping<B['cluster'], T> {
//var m = MappingRegistry.getMapping(device);
//return m[behavior.cluster.name];
for (var m in MappingRegistry.getMapping(device).mapping) {
if (behavior.cluster.name === m) return MappingRegistry.getMapping(device).mapping[m] as unknown as ClusterMapping<B['cluster'], T>;
}
}
public static add<T extends 'Insteon', D extends keyof Devices[T], M extends MutableEndpoint>(mapping: { [x in D]?: DeviceToClusterMap<Devices[T][x], M> }): typeof MappingRegistry;
public static add<T extends SupportedFamily, D extends keyof Devices[T], M extends MutableEndpoint>(mapping: { [x in D]?: DeviceToClusterMap<Devices[T][x], M> }): typeof MappingRegistry {
MappingRegistry.register<T>(mapping as unknown as FamilyToClusterMap<T>);
return MappingRegistry;
}
//@ts-ignore
public static register<const T extends SupportedFamily>(map: FamilyToClusterMap<T>, family?: T): void {
if ('Family' in map) {
let regMap: Map<string, DeviceToClusterMap<ISYDevice.Any, MutableEndpoint>>;
let Devices = DevicesNS[map.Family];
if (!MappingRegistry.map.has(map.Family)) {
MappingRegistry.map.set(map.Family, new Map());
}
regMap = MappingRegistry.map.get(map.Family);
for (var key in map) {
if (key !== 'Family' && key !== 'add') {
let m = map[key] as DeviceToClusterMap<ISYNode.Factory<any, ISYNode<any, any, any, any>>, MutableEndpoint> & { toJSON: () => any };
m = {
deviceType: m.deviceType.with(BridgedDeviceBasicInformationBehavior, ISYBridgedDeviceBehavior, UserLabelServer, FixedLabelServer),
mapping: m.mapping,
toJSON() {
return { deviceType: this.deviceType.name, mapping: this.mapping };
}
};
if (m.mapping != undefined) {
for (var key1 in m.mapping) {
for (var key2 in m.mapping[key1].attributes) {
let attribute = m.mapping[key1].attributes[key2];
{
let d = attribute;
try {
if (typeof d === 'string') {
let [d1, d2] = d.split('.');
if (d2) {
m.mapping[key1].attributes[key2] = { driver: `${Devices[key]?.Nodes[d1].Drivers[d2]}`, node: d1 };
} else {
m.mapping[key1].attributes[key2] = { driver: Devices[key]?.Drivers[d1] };
}
} else if (hasPath(d)) {
let [d1, d2] = d.driver.split('.');
if (d2) {
//@ts-ignore
m.mapping[key1].attributes[key2].driver = `${Devices[key]?.Nodes[d1].Drivers[d2]}`;
//@ts-ignore
m.mapping[key1].attributes[key2].node = d1;
} else {
//@ts-ignore
m.mapping[key1].attributes[key2].driver = Devices[key]?.Drivers[d1];
}
}
} catch {
console.log('Error', key, key1, key2, d);
}
}
}
}
}
console.log('Registering', JSON.stringify({ keys: [key, Devices[key]?.Class?.name, Devices[key]?.Class?.nodeDefId], mapping: m }, null, 2));
regMap.set(key, m as any);
regMap.set(Devices[key]?.Class?.name, m as any);
if (ISYDevice.isNode(Devices[key])) {
regMap.set(Devices[key]?.Class?.nodeDefId, m as any);
}
}
}
} /*else {
let regMap: Map<string, DeviceToClusterMap<any, any>>;
for (var key in map) {
const keys = key.split('.');
let x = DevicesNS[keys[0]][keys[1]] as typeof ISYNode<any, any, any, any>;
if (!MappingRegistry.map.has(Family[x.family] as keyof typeof Family)) {
MappingRegistry.map.set(Family[x.family] as keyof typeof Family, new Map());
}
//{family, key} = key.split(".")[0]
regMap = MappingRegistry.map.get(Family[x.family] as keyof typeof Family);
let m = map[key] as DeviceToClusterMap<ISYDevice.Any, MutableEndpoint>;
let deviceType = m.deviceType;
deviceType = deviceType.with(BridgedDeviceBasicInformationBehavior, ISYBridgedDeviceBehavior);
m = { deviceType: deviceType, mapping: m.mapping };
regMap.set(keys[1], m);
regMap.set(x.name, m);
}
}
}*/
// #endregion Public Static Methods (3)
}
}
// #endregion Classes (1)
// #region Variables (3)
/*interface SimplyEndpointMapping<T extends ISYNode<Family, any, any, any>, K extends MutableEndpoint> extends SimplifyDeep<ISYtoMatterMapping<T, K>> {}*/
/*
const map = {
deviceType: DimmableLightDevice,
mapping: {
onOff: {
attributes: {
onOff: { driver: 'ST', converter: 'Percent.Boolean' },
},
commands: {
onWithTimedOff: { command: 'DON' },
}
},
levelControl: {
attributes: {
}
}
}
} as Mapping<Insteon.RelayLampNode, DimmableLightDevice>;
*/
// #endregion Variables (3)