Compare commits

..

1 Commits

Author SHA1 Message Date
c3df0aa619 new pipeline version handling 2024-01-30 12:36:46 +01:00
23 changed files with 238 additions and 3915 deletions

View File

@@ -1,4 +0,0 @@
# ...
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd ./matter-bridge && npm run lint-fix && git add .

View File

@@ -1,3 +0,0 @@
#!/bin/bash
cd matter-bridge
exec < /dev/tty && node_modules/.bin/cz --hook || true

View File

@@ -1,21 +1,14 @@
# HA-Matter-Bridge # HA-Matter-Bridge
[![paypal](https://camo.githubusercontent.com/0283ea90498d8ea623c07906a5e07e9e6c2a5eaa6911d52033687c60cfa8d22f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f446f6e6174652d50617950616c2d677265656e2e737667)](https://www.paypal.com/donate/?business=VHWQSZR2SS898&no_recurring=0&item_name=HA+Matter+Bridge&currency_code=EUR) [![Publish Docker image](https://github.com/Jatus93/ha-matter-bridge/actions/workflows/docker-image.yml/badge.svg)](https://github.com/Jatus93/ha-matter-bridge/actions/workflows/docker-image.yml)
## Purpose ## Purpose
This project serves as a proof of concept to connect HomeAssistant devices to Voice Assistants through the Matter Protocol. This project serves as a proof of concept to connect HomeAssistant devices to Voice Assistants through the Matter Protocol.
## Getting Started ## Getting Started
### Setup ### Prerequisites
- Add this repository to your home assistant install \ - Add this repository to your home assistant install
[![Open your Home Assistant instance and show the add add-on repository dialog with a specific repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2FJatus93%2Fha-matter-bridge)
- click on check for updates
- click on HA Matter Bridge
- install
### Configuration ### Configuration
@@ -33,7 +26,6 @@ This project serves as a proof of concept to connect HomeAssistant devices to Vo
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 3. Add some tests
4. Donation
## Known Issues and Limitations ## Known Issues and Limitations

View File

@@ -1,46 +0,0 @@
module.exports = {
env: {
es2021: true,
node: true,
jest: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
overrides: [
{
env: {
node: true,
},
files: ['.eslintrc.{js,cjs}'],
parserOptions: {
sourceType: 'script',
},
},
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
arrowFunctions: true,
},
},
plugins: ['@stylistic/ts', '@typescript-eslint', 'prettier'],
rules: {
'linebreak-style': ['error', 'unix'],
quotes: ['error', 'single'],
semi: ['error', 'always'],
curly: ['error'],
'no-empty-function': [
'off',
{
allow: null,
},
],
'require-await': ['error'],
},
globals: {},
};

View File

@@ -0,0 +1,15 @@
{
"env": {
"es2021": true,
"node": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
}
}

View File

@@ -1,7 +1,7 @@
{ {
"trailingComma": "es5",
"tabWidth": 4, "tabWidth": 4,
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": true,
"printWidth": 70, "printWidth": 80
"useTabs": false
} }

View File

@@ -1,21 +1,33 @@
ARG BUILD_FROM=ghcr.io/hassio-addons/base:15.0.3 ARG BUILD_FROM=ghcr.io/hassio-addons/base:15.0.3
FROM node:18.19.0-alpine as node
FROM ${BUILD_FROM} as runenv
COPY --from=node /usr/lib /usr/lib FROM node:18.19.0-alpine as buildbase
COPY --from=node /usr/local/lib /usr/local/lib
COPY --from=node /usr/local/include /usr/local/include
COPY --from=node /usr/local/bin /usr/local/bin
COPY ./package.json ./ COPY ./package.json ./
COPY ./package-lock.json ./ COPY ./package-lock.json ./
COPY ./src ./src COPY ./src ./src
COPY ./tsconfig.json ./ COPY ./tsconfig.json ./
RUN npm install
RUN HUSKY=0 && npm install
RUN npm run build RUN npm run build
RUN rm -rf ./node_modules
RUN HUSKY=0 && npm install --production FROM node:18.19.0-alpine as deps
COPY ./package.json ./
COPY ./package-lock.json ./
COPY ./src ./src
COPY ./tsconfig.json ./
RUN npm install --production
FROM ${BUILD_FROM} as runenv
COPY --from=buildbase /usr/lib /usr/lib
COPY --from=buildbase /usr/local/lib /usr/local/lib
COPY --from=buildbase /usr/local/include /usr/local/include
COPY --from=buildbase /usr/local/bin /usr/local/bin
COPY --from=buildbase ./build ./build
COPY --from=deps ./node_modules ./node_modules
COPY --from=deps ./package.json ./package.json
COPY run.sh / COPY run.sh /
RUN chmod a+x /run.sh RUN chmod a+x /run.sh

View File

@@ -1,6 +1,6 @@
--- ---
name: 'HA Matter Bridge' name: 'HA Matter Bridge'
version: '0.0.3-alpha' version: '0.0.1-alpha'
slug: ha-matter-bridge slug: ha-matter-bridge
description: This project serves as a proof of concept to connect HomeAssistant devices to Voice Assistants through the Matter Protocol. description: This project serves as a proof of concept to connect HomeAssistant devices to Voice Assistants through the Matter Protocol.
init: false init: false

View File

@@ -0,0 +1,11 @@
version: '3'
services:
ha-bridge:
build: .
environment:
- HA_HOST=${HA_HOST}
- HA_PORT=${HA_PORT}
- HA_ACCESS_TOKEN=${HA_ACCESS_TOKEN}
volumes:
- ./deviceData:/app/deviceData:rw
network_mode: host

View File

@@ -0,0 +1,12 @@
FROM node:18.19.0-alpine
WORKDIR /app
COPY ./package.json ./
COPY ./package-lock.json ./
COPY ./src ./src
COPY ./tsconfig.json ./
RUN npm install
RUN npm install ts-node
CMD [ "npm", "run", "start" ]

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,11 @@
{ {
"name": "ha-matter-bridge", "name": "ha-matter-bridge",
"version": "1.0.0",
"description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"start": "node ./build/index.js", "start": "node ./build/index.js"
"lint": "eslint .",
"lint-fix": "eslint --fix .",
"prepare": "if [ $HUSKY ]; then cd .. && husky install matter-bridge/.husky; fi",
"commit": "./node_modules/.bin/git-cz"
}, },
"author": "", "author": "",
"license": "GPL-3.0", "license": "GPL-3.0",
@@ -20,28 +18,11 @@
"ws": "^8.16.0" "ws": "^8.16.0"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/eslint-plugin": "^1.5.4",
"@stylistic/eslint-plugin-js": "^1.5.4",
"@stylistic/eslint-plugin-plus": "^1.5.4",
"@stylistic/eslint-plugin-ts": "^1.5.4",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.20.0", "@typescript-eslint/parser": "^6.19.1",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"husky": "^9.0.7",
"prettier": "^3.2.4",
"prettier-eslint": "^16.3.0",
"standard-version": "^9.5.0",
"typescript": "^5.3.3" "typescript": "^5.3.3"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
} }
} }

View File

@@ -2,62 +2,68 @@ import { Logger } from '@project-chip/matter-node.js/log';
import { HassEntity, StateChangedEvent } 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));
export class HAMiddleware { export class HAMiddleware {
private logger = new Logger('HAMiddleware'); private logger = new Logger('HAMiddleware');
private hassClient: HassApi; private hassClient: HassApi;
private static instance: HAMiddleware; private static instance: HAMiddleware;
private static callerOptions: Partial<HassWsOptions> | undefined;
private requestFulfilled: boolean = true;
private entities: { [k: string]: HassEntity } = {}; private entities: { [k: string]: HassEntity } = {};
private functionsToCallOnChange: { private functionsToCallOnChange: {
[k: string]: ((data: StateChangedEvent) => void) | undefined; [k: string]: ((data: StateChangedEvent) => void) | undefined;
} = {}; } = {};
async waitCompletition(): Promise<void> {
let waited = 0;
const timeOut = 5000;
while (!this.requestFulfilled && waited < timeOut) {
await sleep(1000);
waited += 1000;
}
}
stop(): void { stop(): void {
this.hassClient.rawClient.ws.close(); this.hassClient.rawClient.ws.close();
} }
async callAService( async callAService(domain: string, service: string, extraArgs?: unknown) {
domain: string, this.requestFulfilled = false;
service: string, this.hassClient.callService(domain, service, extraArgs);
extraArgs?: unknown,
) {
await this.hassClient.callService(domain, service, extraArgs);
} }
subscribe() { subscribe() {
this.hassClient.on('state_changed', (event) => { this.hassClient.on('state_changed', (event) => {
this.logger.debug(JSON.stringify(event)); this.logger.debug(JSON.stringify(event));
const toDo = const toDo = this.functionsToCallOnChange[event.data.entity_id];
this.functionsToCallOnChange[event.data.entity_id];
if (toDo) { if (toDo) {
toDo(event); toDo(event);
} }
}); });
} }
subscribeToDevice( subscrieToDevice(deviceId: string, fn: (event: StateChangedEvent) => void) {
deviceId: string,
fn: (event: StateChangedEvent) => void,
) {
this.functionsToCallOnChange[deviceId] = fn; this.functionsToCallOnChange[deviceId] = fn;
this.logger.debug(this.functionsToCallOnChange); this.logger.debug(this.functionsToCallOnChange);
} }
async getStates(): Promise<{ [k: string]: HassEntity }> { async getStates(): Promise<{ [k: string]: HassEntity }> {
this.requestFulfilled = false;
const states = await this.hassClient.getStates(); const states = await this.hassClient.getStates();
const sorted = states.reduceRight<{ const sorted = states.reduceRight<{ [k: string]: HassEntity }>(
[k: string]: HassEntity; (last, current) => {
}>((last, current) => { last[current['entity_id']] = current;
last[current['entity_id']] = current; return last;
return last; },
}, {}); {}
);
this.logger.debug(JSON.stringify({ getStates: sorted })); this.logger.debug(JSON.stringify({ getStates: sorted }));
this.entities = sorted; this.entities = sorted;
return this.entities; return this.entities;
} }
async getStatesPartitionedByType(): Promise<{ async getStatesPartitionedByType(): Promise<{ [k: string]: HassEntity[] }> {
[k: string]: HassEntity[];
}> {
const states = await this.getStates(); const states = await this.getStates();
const toReturn = Object.keys(states).reduceRight<{ const toReturn = Object.keys(states).reduceRight<{
[k: string]: HassEntity[]; [k: string]: HassEntity[];
@@ -70,13 +76,15 @@ export class HAMiddleware {
return prev; return prev;
}, {}); }, {});
this.logger.debug( this.logger.debug(
JSON.stringify({ getStatesPartitionedByType: toReturn }), JSON.stringify({ getStatesPartitionedByType: toReturn })
); );
return toReturn; return toReturn;
} }
async getServices() { async getServices() {
this.requestFulfilled = false;
const states = await this.hassClient.getServices(); const states = await this.hassClient.getServices();
return states; return states;
} }
@@ -85,9 +93,10 @@ export class HAMiddleware {
} }
public static async getInstance( public static async getInstance(
callerOptions?: Partial<HassWsOptions> | undefined, callerOptions?: Partial<HassWsOptions> | undefined
): Promise<HAMiddleware> { ): Promise<HAMiddleware> {
if (!HAMiddleware.instance) { if (!HAMiddleware.instance) {
HAMiddleware.callerOptions = callerOptions;
const client = await hass(callerOptions); const client = await hass(callerOptions);
HAMiddleware.instance = new HAMiddleware(client); HAMiddleware.instance = new HAMiddleware(client);
} }

View File

@@ -60,12 +60,7 @@ export type HassConfig = {
config_source: string; config_source: string;
recovery_mode: boolean; recovery_mode: boolean;
safe_mode: boolean; safe_mode: boolean;
state: state: 'NOT_RUNNING' | 'STARTING' | 'RUNNING' | 'STOPPING' | 'FINAL_WRITE';
| 'NOT_RUNNING'
| 'STARTING'
| 'RUNNING'
| 'STOPPING'
| 'FINAL_WRITE';
external_url: string | null; external_url: string | null;
internal_url: string | null; internal_url: string | null;
currency: string; currency: string;

View File

@@ -6,26 +6,22 @@ import { HAMiddleware } from '../home-assistant/HAmiddleware';
const LOGGER = new Logger('Mapper'); const LOGGER = new Logger('Mapper');
async function setHasEntities( async function setHasEnties(
haMiddleware: HAMiddleware, haMiddleware: HAMiddleware,
bridge: Bridge, bridge: Bridge
): Promise<void> { ): Promise<void> {
const entities = await haMiddleware.getStatesPartitionedByType(); const entities = await haMiddleware.getStatesPartitionedByType();
LOGGER.info({ entities }); LOGGER.info({ entities });
if (entities['light']) { if (entities['light']) {
LOGGER.info( LOGGER.info('adding ', entities['light'].length, 'light devices');
'adding ',
entities['light'].length,
'light devices',
);
setLights(entities['light'], haMiddleware, bridge); setLights(entities['light'], haMiddleware, bridge);
} }
} }
export async function addAllDevicesToBridge( export async function addAllDevicesToBridge(
haMiddleware: HAMiddleware, haMiddleware: HAMiddleware,
bridge: Bridge, bridge: Bridge
): Promise<void> { ): Promise<void> {
await setHasEntities(haMiddleware, bridge); await setHasEnties(haMiddleware, bridge);
haMiddleware.subscribe(); haMiddleware.subscribe();
} }

View File

@@ -7,7 +7,7 @@ import { Bridge } from '../../matter';
export type AddHaDeviceToBridge = ( export type AddHaDeviceToBridge = (
haEntity: HassEntity, haEntity: HassEntity,
haMiddleware: HAMiddleware, haMiddleware: HAMiddleware,
bridge: Bridge, bridge: Bridge
) => Device; ) => Device;
export { HAMiddleware } from '../../home-assistant/HAmiddleware'; export { HAMiddleware } from '../../home-assistant/HAmiddleware';

View File

@@ -7,92 +7,57 @@ import {
HassEntity, HassEntity,
StateChangedEvent, StateChangedEvent,
} from '../../../home-assistant/HAssTypes'; } from '../../../home-assistant/HAssTypes';
import { import { AddHaDeviceToBridge, Bridge, HAMiddleware } from '../MapperType';
AddHaDeviceToBridge,
Bridge,
HAMiddleware,
} from '../MapperType';
import { Logger } from '@project-chip/matter-node.js/log'; import { Logger } from '@project-chip/matter-node.js/log';
const LOGGER = new Logger('DimmableLight'); const LOGGER = new Logger('DimmableLight');
export const addDimmableLightDevice: AddHaDeviceToBridge = ( export const addDimmerableLightDevice: AddHaDeviceToBridge = (
haEntity: HassEntity, haEntity: HassEntity,
haMiddleware: HAMiddleware, haMiddleware: HAMiddleware,
bridge: Bridge, bridge: Bridge
): Device => { ): Device => {
LOGGER.debug(
`Building device ${haEntity.entity_id} \n ${JSON.stringify({
haEntity,
})}`,
);
const device = new DimmableLightDevice( const device = new DimmableLightDevice(
{ onOff: haEntity.state === 'on' }, { onOff: haEntity.state === 'on' },
{ {
currentLevel: currentLevel:
Number(haEntity.attributes['brightness']) / 255 || Number(haEntity.attributes['brightness']) / 255 || null,
null,
onLevel: 0.1, onLevel: 0.1,
options: { options: { coupleColorTempToLevel: false, executeIfOff: false },
coupleColorTempToLevel: false, }
executeIfOff: false,
},
},
); );
const serialFromId = MD5(haEntity.entity_id).toString(); const serialFromId = MD5(haEntity.entity_id).toString();
device.addOnOffListener((value, oldValue) => { device.addOnOffListener((value, oldValue) => {
LOGGER.debug(
`OnOff Event for device ${haEntity.entity_id}, ${JSON.stringify(
{
value,
oldValue,
},
)}`,
);
if (value !== oldValue) { if (value !== oldValue) {
haMiddleware.callAService( haMiddleware.callAService('light', value ? 'turn_on' : 'turn_off', {
'light', entity_id: haEntity.entity_id,
value ? 'turn_on' : 'turn_off', });
{
entity_id: haEntity.entity_id,
},
);
} }
}); });
device.addCommandHandler( device.addCommandHandler(
'identify', 'identify',
({ request: { identifyTime } }) => async ({ request: { identifyTime } }) =>
LOGGER.info( LOGGER.info(
`Identify called for OnOffDevice ${haEntity.attributes['friendly_name']} with id: ${serialFromId} and identifyTime: ${identifyTime}`, `Identify called for OnOffDevice ${haEntity.attributes['friendly_name']} with id: ${serialFromId} and identifyTime: ${identifyTime}`
), )
); );
device.addCurrentLevelListener((value) => { device.addCurrentLevelListener((value) => {
LOGGER.debug(
`CurrentLevel Event for device ${haEntity.entity_id} value: ${value}`,
);
let extraArgs = { entity_id: haEntity.entity_id } as object;
if (Number(value) > 0) {
extraArgs = { ...extraArgs, brightness: Number(value) };
}
haMiddleware.callAService( haMiddleware.callAService(
'light', 'light',
Number(value) > 0 ? 'turn_on' : 'turn_off', Number(value) > 0 ? 'turn_on' : 'turn_off',
extraArgs, { entity_id: haEntity.entity_id, brightness: Number(value) }
); );
}); });
haMiddleware.subscrieToDevice(
haMiddleware.subscribeToDevice(
haEntity.entity_id, haEntity.entity_id,
(event: StateChangedEvent) => { (event: StateChangedEvent) => {
LOGGER.debug(`Event for device ${haEntity.entity_id}`); LOGGER.debug(`Event for device ${haEntity.entity_id}`);
LOGGER.debug(JSON.stringify(event)); LOGGER.debug(JSON.stringify(event));
device.setOnOff(event.data.new_state?.state === 'on'); device.setOnOff(event.data.new_state?.state === 'on');
device.setCurrentLevel( device.setCurrentLevel(
(event.data.new_state?.attributes as never)[ (event.data.new_state?.attributes as never)['brightness']
'brightness'
],
); );
}, }
); );
bridge.addDevice(device, { bridge.addDevice(device, {

View File

@@ -1,67 +1,42 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { import { Device, OnOffLightDevice } from '@project-chip/matter-node.js/device';
Device,
OnOffLightDevice,
} from '@project-chip/matter-node.js/device';
import { MD5 } from 'crypto-js'; import { MD5 } from 'crypto-js';
import { import {
HassEntity, HassEntity,
StateChangedEvent, StateChangedEvent,
} from '../../../home-assistant/HAssTypes'; } from '../../../home-assistant/HAssTypes';
import { import { AddHaDeviceToBridge, Bridge, HAMiddleware } from '../MapperType';
AddHaDeviceToBridge,
Bridge,
HAMiddleware,
} from '../MapperType';
import { Logger } from '@project-chip/matter-node.js/log'; import { Logger } from '@project-chip/matter-node.js/log';
const LOGGER = new Logger('OnOffLight'); const LOGGER = new Logger('OnOfflIght');
export const addOnOffLightDevice: AddHaDeviceToBridge = ( export const addOnOffLightDevice: AddHaDeviceToBridge = (
haEntity: HassEntity, haEntity: HassEntity,
haMiddleware: HAMiddleware, haMiddleware: HAMiddleware,
bridge: Bridge, bridge: Bridge
): Device => { ): Device => {
LOGGER.debug(
`Building device ${haEntity.entity_id} \n ${JSON.stringify({
haEntity,
})}`,
);
const device = new OnOffLightDevice(); const device = new OnOffLightDevice();
const serialFromId = MD5(haEntity.entity_id).toString(); const serialFromId = MD5(haEntity.entity_id).toString();
device.addOnOffListener((value, oldValue) => { device.addOnOffListener((value, oldValue) => {
LOGGER.debug(
`OnOff Event for device ${haEntity.entity_id}, ${JSON.stringify(
{
value,
oldValue,
},
)}`,
);
if (value !== oldValue) { if (value !== oldValue) {
haMiddleware.callAService( haMiddleware.callAService('light', value ? 'turn_on' : 'turn_off', {
'light', entity_id: haEntity.entity_id,
value ? 'turn_on' : 'turn_off', });
{
entity_id: haEntity.entity_id,
},
);
} }
}); });
device.addCommandHandler( device.addCommandHandler(
'identify', 'identify',
({ request: { identifyTime } }) => async ({ request: { identifyTime } }) =>
LOGGER.info( LOGGER.info(
`Identify called for OnOffDevice ${haEntity.attributes['friendly_name']} with id: ${serialFromId} and identifyTime: ${identifyTime}`, `Identify called for OnOffDevice ${haEntity.attributes['friendly_name']} with id: ${serialFromId} and identifyTime: ${identifyTime}`
), )
); );
haMiddleware.subscribeToDevice( haMiddleware.subscrieToDevice(
haEntity.entity_id, haEntity.entity_id,
(event: StateChangedEvent) => { (event: StateChangedEvent) => {
LOGGER.debug(`Event for device ${haEntity.entity_id}`); LOGGER.debug(`Event for device ${haEntity.entity_id}`);
LOGGER.debug(JSON.stringify(event)); LOGGER.debug(JSON.stringify(event));
device.setOnOff(event.data.new_state?.state === 'on'); device.setOnOff(event.data.new_state?.state === 'on');
}, }
); );
bridge.addDevice(device, { bridge.addDevice(device, {
nodeLabel: haEntity.attributes['friendly_name'], nodeLabel: haEntity.attributes['friendly_name'],

View File

@@ -2,36 +2,34 @@ import { Logger } from '@project-chip/matter-node.js/log';
import { HAMiddleware } from '../../../home-assistant/HAmiddleware'; import { HAMiddleware } from '../../../home-assistant/HAmiddleware';
import { HassEntity } from '../../../home-assistant/HAssTypes'; import { HassEntity } from '../../../home-assistant/HAssTypes';
import { AddHaDeviceToBridge, Bridge } from '../MapperType'; import { AddHaDeviceToBridge, Bridge } from '../MapperType';
import { addDimmableLightDevice } from './DimmableLightDevice'; import { addDimmerableLightDevice } from './DimmerableLightDevice';
import { addOnOffLightDevice } from './OnOffLightDevice'; import { addOnOffLightDevice } from './OnOffLightDevice';
import { Device } from '@project-chip/matter-node.js/device'; import { Device } from '@project-chip/matter-node.js/device';
export * from './DimmableLightDevice'; export * from './DimmerableLightDevice';
export * from './OnOffLightDevice'; export * from './OnOffLightDevice';
const LOGGER = new Logger('Lights'); const LOGGER = new Logger('Lights');
const LIGHTS_MAP_FUNCTIONS: Map<string, AddHaDeviceToBridge> = const LIGHTS_MAP_FUNCTIONS: Map<string, AddHaDeviceToBridge> = new Map<
new Map<string, AddHaDeviceToBridge>([ string,
['onoff', addOnOffLightDevice], AddHaDeviceToBridge
['rgb', addDimmableLightDevice], >([
['brightness', addDimmableLightDevice], ['onoff', addOnOffLightDevice],
]); ['rgb', addDimmerableLightDevice],
['brightness', addDimmerableLightDevice],
]);
const LIGHTS_MAP: Map<string, Device> = new Map<string, Device>(); const LIGHTS_MAP: Map<string, Device> = new Map<string, Device>();
export function setLights( export function setLights(
lights: HassEntity[], lights: HassEntity[],
haMiddleware: HAMiddleware, haMiddleware: HAMiddleware,
bridge: Bridge, bridge: Bridge
) { ) {
lights.forEach((entity) => { lights.forEach((entity) => {
LOGGER.info({ LOGGER.info({ colormodes: entity.attributes['supported_color_modes'] });
colormodes: entity.attributes['supported_color_modes'], const key = (entity.attributes['supported_color_modes'] as string[])[0];
});
const key = (
entity.attributes['supported_color_modes'] as string[]
)[0];
LOGGER.info({ key }); LOGGER.info({ key });
const lightBuildFunction = LIGHTS_MAP_FUNCTIONS.get(key); const lightBuildFunction = LIGHTS_MAP_FUNCTIONS.get(key);
if (!lightBuildFunction) { if (!lightBuildFunction) {
@@ -39,7 +37,7 @@ export function setLights(
} }
LIGHTS_MAP.set( LIGHTS_MAP.set(
entity.entity_id, entity.entity_id,
lightBuildFunction(entity, haMiddleware, bridge), lightBuildFunction(entity, haMiddleware, bridge)
); );
}); });
} }

View File

@@ -23,13 +23,13 @@ export class Bridge {
private static readonly deviceName = private static readonly deviceName =
getParameter('name') || 'Matter Bridge'; getParameter('name') || 'Matter Bridge';
private static readonly deviceType = DeviceTypes.AGGREGATOR.code; private static readonly deviceType = DeviceTypes.AGGREGATOR.code;
private static readonly vendorName = private static readonly vendorName = getParameter('vendor') || 'Jatus';
getParameter('vendor') || 'Jatus';
private static readonly productName = 'HomeAssistant'; private static readonly productName = 'HomeAssistant';
private static readonly port = getIntParameter('port') ?? 5540; private static readonly port = getIntParameter('port') ?? 5540;
private ready = false;
private matterServer: MatterServer; private matterServer: MatterServer;
private static instance: Bridge; private static instace: Bridge;
private logger = new Logger('bridge'); private logger = new Logger('bridge');
private storageManager: StorageManager; private storageManager: StorageManager;
private aggregator: Aggregator; private aggregator: Aggregator;
@@ -37,7 +37,7 @@ export class Bridge {
private constructor( private constructor(
matterServer: MatterServer, matterServer: MatterServer,
storageManager: StorageManager, storageManager: StorageManager
) { ) {
this.matterServer = matterServer; this.matterServer = matterServer;
this.storageManager = storageManager; this.storageManager = storageManager;
@@ -47,12 +47,12 @@ export class Bridge {
public static getInstance( public static getInstance(
matterServer: MatterServer, matterServer: MatterServer,
storageManager: StorageManager, storageManager: StorageManager
): Bridge { ): Bridge {
if (!Bridge.instance) { if (!Bridge.instace) {
this.instance = new Bridge(matterServer, storageManager); this.instace = new Bridge(matterServer, storageManager);
} }
return Bridge.instance; return Bridge.instace;
} }
private async setupContextAndCommissioningServer(): Promise<CommissioningServer> { private async setupContextAndCommissioningServer(): Promise<CommissioningServer> {
@@ -106,17 +106,12 @@ export class Bridge {
device: Device | ComposedDevice, device: Device | ComposedDevice,
bridgedBasicInformation?: AttributeInitialValues< bridgedBasicInformation?: AttributeInitialValues<
typeof BridgedDeviceBasicInformationCluster.attributes typeof BridgedDeviceBasicInformationCluster.attributes
>, >
) { ) {
if (!this.commissioningServer?.isCommissioned()) { if (!this.commissioningServer?.isCommissioned()) {
this.logger.warn( this.logger.warn('System not initialized, may cause crashes');
'System not initialized, may cause crashes',
);
} }
this.aggregator.addBridgedDevice( this.aggregator.addBridgedDevice(device, bridgedBasicInformation);
device,
bridgedBasicInformation,
);
} }
async start() { async start() {
@@ -124,32 +119,30 @@ export class Bridge {
this.commissioningServer = this.commissioningServer =
await this.setupContextAndCommissioningServer(); await this.setupContextAndCommissioningServer();
this.commissioningServer.addDevice(this.aggregator); this.commissioningServer.addDevice(this.aggregator);
this.matterServer.addCommissioningServer( this.matterServer.addCommissioningServer(this.commissioningServer);
this.commissioningServer,
);
await this.matterServer.start(); await this.matterServer.start();
this.logger.info('Listening'); this.logger.info('Listening');
if (!this.commissioningServer.isCommissioned()) { if (!this.commissioningServer.isCommissioned()) {
const pairingData = const pairingData = this.commissioningServer.getPairingCode();
this.commissioningServer.getPairingCode();
const { qrPairingCode, manualPairingCode } = pairingData; const { qrPairingCode, manualPairingCode } = pairingData;
console.log(QrCode.get(qrPairingCode)); console.log(QrCode.get(qrPairingCode));
this.logger.info( this.logger.info(
`QR Code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=${qrPairingCode}`, `QR Code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=${qrPairingCode}`
);
this.logger.info(
`Manual pairing code: ${manualPairingCode}`,
); );
this.logger.info(`Manual pairing code: ${manualPairingCode}`);
} else { } else {
this.logger.info( this.logger.info(
'Device is already commissioned. Waiting for controllers to connect ...', 'Device is already commissioned. Waiting for controllers to connect ...'
); );
} }
} }
async stop() { async stop() {
await this.matterServer.close(); this.matterServer.close();
await this.storageManager.close(); this.storageManager
.close()
.then(() => process.exit(0))
.catch((err) => this.logger.error(err));
} }
} }

View File

@@ -3,10 +3,7 @@ import {
StorageBackendDisk, StorageBackendDisk,
StorageManager, StorageManager,
} from '@project-chip/matter-node.js/storage'; } from '@project-chip/matter-node.js/storage';
import { import { getParameter, hasParameter } from '@project-chip/matter-node.js/util';
getParameter,
hasParameter,
} from '@project-chip/matter-node.js/util';
export { Bridge } from './Bridge'; export { Bridge } from './Bridge';
import { Bridge } from './Bridge'; import { Bridge } from './Bridge';
@@ -19,29 +16,25 @@ export function serverSetup(): {
storageManager: StorageManager; storageManager: StorageManager;
} { } {
if (!(MATTER_SERVER && STORAGE && STORAGE_MANAGER)) { if (!(MATTER_SERVER && STORAGE && STORAGE_MANAGER)) {
const storageLocation = const storageLocation = getParameter('store') || '/config/deviceData';
getParameter('store') || '/config/deviceData';
STORAGE = new StorageBackendDisk( STORAGE = new StorageBackendDisk(
storageLocation, storageLocation,
hasParameter('clearstorage'), hasParameter('clearstorage')
); );
STORAGE_MANAGER = new StorageManager(STORAGE); STORAGE_MANAGER = new StorageManager(STORAGE);
MATTER_SERVER = new MatterServer(STORAGE_MANAGER, { MATTER_SERVER = new MatterServer(STORAGE_MANAGER, {
mdnsInterface: getParameter('netinterface'), mdnsInterface: getParameter('netinterface'),
}); });
} }
return { return { matterServer: MATTER_SERVER, storageManager: STORAGE_MANAGER };
matterServer: MATTER_SERVER,
storageManager: STORAGE_MANAGER,
};
} }
export function getBridge(): Bridge { export function getBridge(): Bridge {
const serverData = serverSetup(); const serverData = serverSetup();
const bridge = Bridge.getInstance( const bridge = Bridge.getInstance(
serverData.matterServer, serverData.matterServer,
serverData.storageManager, serverData.storageManager
); );
return bridge; return bridge;
} }

View File

@@ -14,25 +14,20 @@ export function hasParameter(name: string) {
export function getIntParameter(name: string) { export function getIntParameter(name: string) {
const value = getParameter(name); const value = getParameter(name);
if (value === undefined) { if (value === undefined) return undefined;
return undefined;
}
const intValue = parseInt(value, 10); const intValue = parseInt(value, 10);
if (isNaN(intValue)) { if (isNaN(intValue))
throw new ValidationError( throw new ValidationError(
`Invalid value for parameter ${name}: ${value} is not a number`, `Invalid value for parameter ${name}: ${value} is not a number`
); );
}
return intValue; return intValue;
} }
export function commandExecutor(scriptParamName: string) { export function commandExecutor(scriptParamName: string) {
const script = getParameter(scriptParamName); const script = getParameter(scriptParamName);
if (script === undefined) { if (script === undefined) return undefined;
return undefined;
}
return () => return () =>
console.log( console.log(
`${scriptParamName}: ${execSync(script).toString().slice(0, -1)}`, `${scriptParamName}: ${execSync(script).toString().slice(0, -1)}`
); );
} }

View File

@@ -1,3 +1 @@
name: ha-matter-bridge name: ha-matter-bridge
maintainer: Jatus93
url: https://github.com/Jatus93/ha-matter-bridge