diff --git a/README.md b/README.md index e1826d2..1d43c87 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Once the `.env` file is configured, run the project with `docker-compose up` for 1. Help organize the project and dependencies. 2. Add integrations for other devices. +3. Add some tests ## Known Issues and Limitations diff --git a/src/matter/devices/HAmiddleware.ts b/src/HA/HAmiddleware.ts similarity index 87% rename from src/matter/devices/HAmiddleware.ts rename to src/HA/HAmiddleware.ts index 1e6a739..68255d3 100644 --- a/src/matter/devices/HAmiddleware.ts +++ b/src/HA/HAmiddleware.ts @@ -1,5 +1,5 @@ import { Logger } from '@project-chip/matter-node.js/log'; -import { HassEntity, HassEvent } from './HAssTypes'; +import { HassEntity, StateChangedEvent } from './HAssTypes'; import hass, { HassApi, HassWsOptions } from 'homeassistant-ws'; const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); @@ -12,7 +12,7 @@ export class HAMiddleware { private requestFulfilled: boolean = true; private entities: { [k: string]: HassEntity } = {}; private functionsToCallOnChange: { - [k: string]: ((data: HassEvent['data']) => void) | undefined; + [k: string]: ((data: StateChangedEvent) => void) | undefined; } = {}; async waitCompletition(): Promise { @@ -34,17 +34,17 @@ export class HAMiddleware { } subscribe() { - this.hassClient.on('state_changed', (stateChangedEvent) => { - this.logger.debug(stateChangedEvent.data); + this.hassClient.on('state_changed', (event) => { + this.logger.debug(event); const toDo = - this.functionsToCallOnChange[stateChangedEvent.data.entity_id]; + this.functionsToCallOnChange[event.data.entity_id]; if (toDo) { - toDo(stateChangedEvent.data); + toDo(event.data); } }); } - subscrieToDevice(deviceId: string, fn: (data: HassEvent['data']) => void) { + subscrieToDevice(deviceId: string, fn: (event: StateChangedEvent) => void) { this.functionsToCallOnChange[deviceId] = fn; this.logger.debug(this.functionsToCallOnChange); } diff --git a/src/matter/devices/HAssTypes.ts b/src/HA/HAssTypes.ts similarity index 100% rename from src/matter/devices/HAssTypes.ts rename to src/HA/HAssTypes.ts diff --git a/src/index.ts b/src/index.ts index bd9f2fd..b955a4b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { Bridge, HAMiddleware, addAllDevicesToBridge } from './matter/devices'; +import { Bridge, HAMiddleware, addAllDevicesToBridge } from './matter'; import { serverSetup } from './matter/server'; import { Logger } from '@project-chip/matter-node.js/log'; diff --git a/src/matter/Mapper.ts b/src/matter/Mapper.ts new file mode 100644 index 0000000..61d338b --- /dev/null +++ b/src/matter/Mapper.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { addDimmerableLightDevice, addOnOffLightDevice } from './devices/lights'; +import { HassEntity } from '../HA/HAssTypes'; +import { Bridge, HAMiddleware } from '.'; +import { Logger } from '@project-chip/matter-node.js/log'; + +const LOGGER = new Logger('Mapper'); + + +const lightsMap: Map< + string, + (haEntity: HassEntity, haMiddleware: HAMiddleware, bridge: Bridge) => void +> = new Map< + string, + (haEntity: HassEntity, haMiddleware: HAMiddleware, bridge: Bridge) => void +>([ + ['onoff', addOnOffLightDevice], + ['rgb', addDimmerableLightDevice], + ['brightness', addDimmerableLightDevice], +]); + +function setLights( + lights: HassEntity[], + haMiddleware: HAMiddleware, + bridge: Bridge +) { + lights.forEach((entity) => { + LOGGER.info({ colormodes: entity.attributes['supported_color_modes'] }); + const key = (entity.attributes['supported_color_modes'] as string[])[0]; + LOGGER.info({ key }); + const lightBuildFunction = lightsMap.get(key); + if (!lightBuildFunction) { + throw new Error('Missing ' + key); + } + return lightBuildFunction(entity, haMiddleware, bridge); + }); +} + +async function setHasEnties( + haMiddleware: HAMiddleware, + bridge: Bridge +): Promise { + const entities = await haMiddleware.getStatesPartitionedByType(); + LOGGER.info({ entities }); + if (entities['light']) { + LOGGER.info('adding ', entities['light'].length, 'light devices'); + setLights(entities['light'], haMiddleware, bridge); + } +} + +export async function addAllDevicesToBridge( + haMiddleware: HAMiddleware, + bridge: Bridge +): Promise { + await setHasEnties(haMiddleware, bridge); + haMiddleware.subscribe(); +} diff --git a/src/matter/devices/Mapper.ts b/src/matter/devices/Mapper.ts deleted file mode 100644 index 4e0c799..0000000 --- a/src/matter/devices/Mapper.ts +++ /dev/null @@ -1,179 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - Device, - DimmableLightDevice, - OnOffLightDevice, -} from '@project-chip/matter.js/device'; -import { HassEntity, HassEvent } from './HAssTypes'; -import { Bridge, HAMiddleware } from '.'; -import { MD5 } from 'crypto-js'; -import { Logger } from '@project-chip/matter-node.js/log'; - -const DEVICE_ENTITY_MAP: { - [k: string]: { haEntity: HassEntity; device: Device }; -} = {}; - -const LOGGER = new Logger('Mapper'); - -function addRGBLightDeviceToMap( - haEntity: HassEntity, - haMiddleware: HAMiddleware, - bridge: Bridge -): void { - const device = new DimmableLightDevice(); - const serialFromId = MD5(haEntity.entity_id).toString(); - - device.addOnOffListener((value, oldValue) => { - if (value !== oldValue) { - haMiddleware.callAService('light', value ? 'turn_on' : 'turn_off', { - entity_id: haEntity.entity_id, - }); - } - }); - device.addCurrentLevelListener((value) => { - haMiddleware.callAService( - 'light', - Number(value) > 0 ? 'turn_on' : 'turn_off', - { entity_id: haEntity.entity_id, brightness: Number(value) } - ); - }); - bridge.addDevice(device, { - nodeLabel: haEntity.attributes['friendly_name'], - reachable: true, - serialNumber: serialFromId, - }); - - DEVICE_ENTITY_MAP[haEntity.entity_id] = { haEntity, device }; -} - -function addimmerableLightDeviceToMap( - haEntity: HassEntity, - haMiddleware: HAMiddleware, - bridge: Bridge -): void { - const device = new DimmableLightDevice(); - const serialFromId = MD5(haEntity.entity_id).toString(); - device.addOnOffListener((value, oldValue) => { - if (value !== oldValue) { - haMiddleware.callAService('light', value ? 'turn_on' : 'turn_off', { - entity_id: haEntity.entity_id, - }); - } - }); - device.addCommandHandler( - 'identify', - async ({ request: { identifyTime } }) => - LOGGER.info( - `Identify called for OnOffDevice ${haEntity.attributes['friendly_name']} with id: ${serialFromId} and identifyTime: ${identifyTime}` - ) - ); - - device.addCurrentLevelListener((value) => { - haMiddleware.callAService( - 'light', - Number(value) > 0 ? 'turn_on' : 'turn_off', - { entity_id: haEntity.entity_id, brightness: Number(value) } - ); - }); - haMiddleware.subscrieToDevice( - haEntity.entity_id, - (data: HassEvent['data']) => { - device.setOnOff((data.new_state as any).state === 'on'); - device.setCurrentLevel( - (data.new_state as any)['attributes']['brightness'] - ); - } - ); - - bridge.addDevice(device, { - nodeLabel: haEntity.attributes['friendly_name'], - reachable: true, - serialNumber: serialFromId, - }); - - DEVICE_ENTITY_MAP[haEntity.entity_id] = { haEntity, device }; -} - -function addOnOffLightDeviceToMap( - haEntity: HassEntity, - haMiddleware: HAMiddleware, - bridge: Bridge -) { - const device = new OnOffLightDevice(); - const serialFromId = MD5(haEntity.entity_id).toString(); - device.addOnOffListener((value, oldValue) => { - if (value !== oldValue) { - haMiddleware.callAService('light', value ? 'turn_on' : 'turn_off', { - entity_id: haEntity.entity_id, - }); - } - }); - device.addCommandHandler( - 'identify', - async ({ request: { identifyTime } }) => - LOGGER.info( - `Identify called for OnOffDevice ${haEntity.attributes['friendly_name']} with id: ${serialFromId} and identifyTime: ${identifyTime}` - ) - ); - haMiddleware.subscrieToDevice( - haEntity.entity_id, - (data: HassEvent['data']) => { - device.setOnOff((data.new_state as any).state === 'on'); - } - ); - bridge.addDevice(device, { - nodeLabel: haEntity.attributes['friendly_name'], - reachable: true, - serialNumber: serialFromId, - }); - DEVICE_ENTITY_MAP[haEntity.entity_id] = { haEntity, device }; -} - -const lightsMap: Map< - string, - (haEntity: HassEntity, haMiddleware: HAMiddleware, bridge: Bridge) => void -> = new Map< - string, - (haEntity: HassEntity, haMiddleware: HAMiddleware, bridge: Bridge) => void ->([ - ['onoff', addOnOffLightDeviceToMap], - ['rgb', addRGBLightDeviceToMap], - ['brightness', addimmerableLightDeviceToMap], -]); - -function setLights( - lights: HassEntity[], - haMiddleware: HAMiddleware, - bridge: Bridge -) { - lights.forEach((entity) => { - LOGGER.info({ colormodes: entity.attributes['supported_color_modes'] }); - const key = (entity.attributes['supported_color_modes'] as string[])[0]; - LOGGER.info({ key }); - const lightBuildFunction = lightsMap.get(key); - if (!lightBuildFunction) { - throw new Error('Missing ' + key); - } - return lightBuildFunction(entity, haMiddleware, bridge); - }); -} - -async function setHasEnties( - haMiddleware: HAMiddleware, - bridge: Bridge -): Promise { - const entities = await haMiddleware.getStatesPartitionedByType(); - LOGGER.info({ entities }); - if (entities['light']) { - LOGGER.info('adding ', entities['light'].length, 'light devices'); - setLights(entities['light'], haMiddleware, bridge); - } -} - -export async function addAllDevicesToBridge( - haMiddleware: HAMiddleware, - bridge: Bridge -): Promise { - await setHasEnties(haMiddleware, bridge); - haMiddleware.subscribe(); -} diff --git a/src/matter/devices/MapperType.ts b/src/matter/devices/MapperType.ts new file mode 100644 index 0000000..729a1f7 --- /dev/null +++ b/src/matter/devices/MapperType.ts @@ -0,0 +1,7 @@ +import { Device } from "@project-chip/matter-node.js/device"; +import { HAMiddleware, Bridge } from ".."; +import { HassEntity } from "../../HA/HAssTypes"; + +export type AddHaDeviceToBridge = (haEntity: HassEntity, + haMiddleware: HAMiddleware, + bridge: Bridge) => Device diff --git a/src/matter/devices/index.ts b/src/matter/devices/index.ts deleted file mode 100644 index ce6412e..0000000 --- a/src/matter/devices/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Bridge'; -export * from './HAmiddleware'; -export * from './Mapper'; diff --git a/src/matter/devices/lights/DimmerableLightDevice.ts b/src/matter/devices/lights/DimmerableLightDevice.ts new file mode 100644 index 0000000..0be6c79 --- /dev/null +++ b/src/matter/devices/lights/DimmerableLightDevice.ts @@ -0,0 +1,49 @@ +import { Device, DimmableLightDevice } from "@project-chip/matter-node.js/device"; +import { MD5 } from "crypto-js"; +import { HAMiddleware, Bridge } from "../.."; +import { HassEntity, StateChangedEvent } from "../../../HA/HAssTypes"; +import { AddHaDeviceToBridge } from "../MapperType"; +import { Logger } from "@project-chip/matter-node.js/log"; + +const LOGGER = new Logger('DimmableLight'); +export const addDimmerableLightDevice: AddHaDeviceToBridge = (haEntity: HassEntity, haMiddleware: HAMiddleware, bridge: Bridge): Device => { + const device = new DimmableLightDevice(); + const serialFromId = MD5(haEntity.entity_id).toString(); + device.addOnOffListener((value, oldValue) => { + if (value !== oldValue) { + haMiddleware.callAService('light', value ? 'turn_on' : 'turn_off', { + entity_id: haEntity.entity_id, + }); + } + }); + device.addCommandHandler( + 'identify', + async ({ request: { identifyTime } }) => + LOGGER.info( + `Identify called for OnOffDevice ${haEntity.attributes['friendly_name']} with id: ${serialFromId} and identifyTime: ${identifyTime}` + ) + ); + + device.addCurrentLevelListener((value) => { + haMiddleware.callAService( + 'light', + Number(value) > 0 ? 'turn_on' : 'turn_off', + { entity_id: haEntity.entity_id, brightness: Number(value) } + ); + }); + haMiddleware.subscrieToDevice( + haEntity.entity_id, + (event: StateChangedEvent) => { + device.setOnOff(event.data.new_state?.state === 'on'); + device.setCurrentLevel( + (event.data.new_state?.attributes as never)['brightness']); + } + ); + + bridge.addDevice(device, { + nodeLabel: haEntity.attributes['friendly_name'], + reachable: true, + serialNumber: serialFromId, + }); + return device; +} \ No newline at end of file diff --git a/src/matter/devices/lights/OnOffLightDevice.ts b/src/matter/devices/lights/OnOffLightDevice.ts new file mode 100644 index 0000000..953fabb --- /dev/null +++ b/src/matter/devices/lights/OnOffLightDevice.ts @@ -0,0 +1,39 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Device, OnOffLightDevice } from "@project-chip/matter-node.js/device"; +import { MD5 } from "crypto-js"; +import { HAMiddleware, Bridge } from "../.."; +import { HassEntity, StateChangedEvent } from "../../../HA/HAssTypes"; +import { AddHaDeviceToBridge } from "../MapperType"; +import { Logger } from "@project-chip/matter-node.js/log"; + +const LOGGER = new Logger('OnOfflIght'); +export const addOnOffLightDevice: AddHaDeviceToBridge = (haEntity: HassEntity, haMiddleware: HAMiddleware, bridge: Bridge): Device => { + const device = new OnOffLightDevice(); + const serialFromId = MD5(haEntity.entity_id).toString(); + device.addOnOffListener((value, oldValue) => { + if (value !== oldValue) { + haMiddleware.callAService('light', value ? 'turn_on' : 'turn_off', { + entity_id: haEntity.entity_id, + }); + } + }); + device.addCommandHandler( + 'identify', + async ({ request: { identifyTime } }) => + LOGGER.info( + `Identify called for OnOffDevice ${haEntity.attributes['friendly_name']} with id: ${serialFromId} and identifyTime: ${identifyTime}` + ) + ); + haMiddleware.subscrieToDevice( + haEntity.entity_id, + (event: StateChangedEvent) => { + device.setOnOff(event.data.new_state?.state === 'on'); + } + ); + bridge.addDevice(device, { + nodeLabel: haEntity.attributes['friendly_name'], + reachable: true, + serialNumber: serialFromId, + }); + return device; +} \ No newline at end of file diff --git a/src/matter/devices/lights/index.ts b/src/matter/devices/lights/index.ts new file mode 100644 index 0000000..9aade4f --- /dev/null +++ b/src/matter/devices/lights/index.ts @@ -0,0 +1,2 @@ +export * from './DimmerableLightDevice'; +export * from './OnOffLightDevice'; \ No newline at end of file diff --git a/src/matter/index.ts b/src/matter/index.ts new file mode 100644 index 0000000..9f222ee --- /dev/null +++ b/src/matter/index.ts @@ -0,0 +1,3 @@ +export * from './server/Bridge'; +export * from '../HA/HAmiddleware'; +export * from './Mapper'; diff --git a/src/matter/devices/Bridge.ts b/src/matter/server/Bridge.ts similarity index 100% rename from src/matter/devices/Bridge.ts rename to src/matter/server/Bridge.ts