base refactoring #2

Merged
Jatus merged 1 commits from develop into main 2024-01-26 21:08:32 +01:00
13 changed files with 166 additions and 190 deletions
Showing only changes of commit 0425f6e8df - Show all commits

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.
2. Add integrations for other devices.
3. Add some tests
## Known Issues and Limitations

View File

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

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 { 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';