base refactoring #2
@ -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
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();
|
|
||||||
}
|
|
7
src/matter/devices/MapperType.ts
Normal file
7
src/matter/devices/MapperType.ts
Normal 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
|
@ -1,3 +0,0 @@
|
|||||||
export * from './Bridge';
|
|
||||||
export * from './HAmiddleware';
|
|
||||||
export * from './Mapper';
|
|
49
src/matter/devices/lights/DimmerableLightDevice.ts
Normal file
49
src/matter/devices/lights/DimmerableLightDevice.ts
Normal 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;
|
||||||
|
}
|
39
src/matter/devices/lights/OnOffLightDevice.ts
Normal file
39
src/matter/devices/lights/OnOffLightDevice.ts
Normal 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;
|
||||||
|
}
|
2
src/matter/devices/lights/index.ts
Normal file
2
src/matter/devices/lights/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './DimmerableLightDevice';
|
||||||
|
export * from './OnOffLightDevice';
|
3
src/matter/index.ts
Normal file
3
src/matter/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './server/Bridge';
|
||||||
|
export * from '../HA/HAmiddleware';
|
||||||
|
export * from './Mapper';
|
Loading…
Reference in New Issue
Block a user