base refactoring

This commit is contained in:
Jatus 2024-01-26 17:26:02 +01:00
parent 30187250c2
commit 0425f6e8df
13 changed files with 166 additions and 190 deletions

@ -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. 1. Help organize the project and dependencies.
2. Add integrations for other devices. 2. Add integrations for other devices.
3. Add some tests
## Known Issues and Limitations ## Known Issues and Limitations

@ -1,5 +1,5 @@
import { Logger } from '@project-chip/matter-node.js/log'; 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'; import hass, { HassApi, HassWsOptions } from 'homeassistant-ws';
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
@ -12,7 +12,7 @@ export class HAMiddleware {
private requestFulfilled: boolean = true; private requestFulfilled: boolean = true;
private entities: { [k: string]: HassEntity } = {}; private entities: { [k: string]: HassEntity } = {};
private functionsToCallOnChange: { private functionsToCallOnChange: {
[k: string]: ((data: HassEvent['data']) => void) | undefined; [k: string]: ((data: StateChangedEvent) => void) | undefined;
} = {}; } = {};
async waitCompletition(): Promise<void> { async waitCompletition(): Promise<void> {
@ -34,17 +34,17 @@ export class HAMiddleware {
} }
subscribe() { subscribe() {
this.hassClient.on('state_changed', (stateChangedEvent) => { this.hassClient.on('state_changed', (event) => {
this.logger.debug(stateChangedEvent.data); this.logger.debug(event);
const toDo = const toDo =
this.functionsToCallOnChange[stateChangedEvent.data.entity_id]; this.functionsToCallOnChange[event.data.entity_id];
if (toDo) { 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.functionsToCallOnChange[deviceId] = fn;
this.logger.debug(this.functionsToCallOnChange); this.logger.debug(this.functionsToCallOnChange);
} }

@ -1,4 +1,4 @@
import { Bridge, HAMiddleware, addAllDevicesToBridge } from './matter/devices'; import { Bridge, HAMiddleware, addAllDevicesToBridge } from './matter';
import { serverSetup } from './matter/server'; import { serverSetup } from './matter/server';
import { Logger } from '@project-chip/matter-node.js/log'; import { Logger } from '@project-chip/matter-node.js/log';

57
src/matter/Mapper.ts Normal file

@ -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<void> {
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<void> {
await setHasEnties(haMiddleware, bridge);
haMiddleware.subscribe();
}

@ -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<void> {
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<void> {
await setHasEnties(haMiddleware, bridge);
haMiddleware.subscribe();
}

@ -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

@ -1,3 +0,0 @@
export * from './Bridge';
export * from './HAmiddleware';
export * from './Mapper';

@ -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;
}

@ -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;
}

@ -0,0 +1,2 @@
export * from './DimmerableLightDevice';
export * from './OnOffLightDevice';

3
src/matter/index.ts Normal file

@ -0,0 +1,3 @@
export * from './server/Bridge';
export * from '../HA/HAmiddleware';
export * from './Mapper';