base refactoring #1

Merged
Jatus merged 1 commits from feat/refactoring into develop 2024-01-26 21:06:33 +01:00
13 changed files with 166 additions and 190 deletions

View File

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

View File

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

View File

@ -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
View 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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

3
src/matter/index.ts Normal file
View File

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