Compare commits
5 Commits
c3df0aa619
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 007d9c4f98 | |||
| 031d729af0 | |||
| fa3cd39674 | |||
| 9a3f622941 | |||
| aca5de8cff |
28
.github/workflows/docker-image.yml
vendored
28
.github/workflows/docker-image.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
push
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
@@ -10,22 +9,35 @@ jobs:
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Run read-yaml action
|
||||
id: yaml-data
|
||||
uses: jbutcher5/read-yaml@1.6
|
||||
with:
|
||||
file: "./matter-bridge/config.yml"
|
||||
key-path: '["version"]'
|
||||
|
||||
- name: Display read-yaml output
|
||||
run: echo "${{ steps.yaml-data.outputs.data }}"
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR }}
|
||||
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
|
||||
tags: |
|
||||
type=raw,value=${{ steps.yaml-data.outputs.data }}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ./matter-bridge
|
||||
file: ./matter-bridge/Dockerfile
|
||||
|
||||
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
||||
# ...
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
cd ./matter-bridge && npm run lint-fix && git add .
|
||||
3
.husky/prepare-commit-msg
Normal file
3
.husky/prepare-commit-msg
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd matter-bridge
|
||||
exec < /dev/tty && node_modules/.bin/cz --hook || true
|
||||
12
README.md
12
README.md
@@ -1,14 +1,21 @@
|
||||
# HA-Matter-Bridge
|
||||
|
||||
[](https://www.paypal.com/donate/?business=VHWQSZR2SS898&no_recurring=0&item_name=HA+Matter+Bridge¤cy_code=EUR) [](https://github.com/Jatus93/ha-matter-bridge/actions/workflows/docker-image.yml)
|
||||
|
||||
## Purpose
|
||||
|
||||
This project serves as a proof of concept to connect HomeAssistant devices to Voice Assistants through the Matter Protocol.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
### Setup
|
||||
|
||||
- Add this repository to your home assistant install
|
||||
- Add this repository to your home assistant install \
|
||||
[](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
|
||||
|
||||
@@ -26,6 +33,7 @@ This project serves as a proof of concept to connect HomeAssistant devices to Vo
|
||||
1. Help organize the project and dependencies.
|
||||
2. Add integrations for other devices.
|
||||
3. Add some tests
|
||||
4. Donation
|
||||
|
||||
## Known Issues and Limitations
|
||||
|
||||
|
||||
46
matter-bridge/.eslintrc.js
Normal file
46
matter-bridge/.eslintrc.js
Normal file
@@ -0,0 +1,46 @@
|
||||
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: {},
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"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": {
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 80
|
||||
"printWidth": 70,
|
||||
"useTabs": false
|
||||
}
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
ARG BUILD_FROM=ghcr.io/hassio-addons/base:15.0.3
|
||||
|
||||
FROM node:18.19.0-alpine as buildbase
|
||||
|
||||
COPY ./package.json ./
|
||||
COPY ./package-lock.json ./
|
||||
COPY ./src ./src
|
||||
COPY ./tsconfig.json ./
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
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 node:18.19.0-alpine as node
|
||||
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 --from=node /usr/lib /usr/lib
|
||||
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-lock.json ./
|
||||
COPY ./src ./src
|
||||
COPY ./tsconfig.json ./
|
||||
|
||||
RUN HUSKY=0 && npm install
|
||||
RUN npm run build
|
||||
RUN rm -rf ./node_modules
|
||||
RUN HUSKY=0 && npm install --production
|
||||
|
||||
COPY run.sh /
|
||||
RUN chmod a+x /run.sh
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: 'HA Matter Bridge'
|
||||
version: '0.0.1-alpha'
|
||||
version: '0.0.3-alpha'
|
||||
slug: ha-matter-bridge
|
||||
description: This project serves as a proof of concept to connect HomeAssistant devices to Voice Assistants through the Matter Protocol.
|
||||
init: false
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
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
|
||||
@@ -1,12 +0,0 @@
|
||||
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" ]
|
||||
3674
matter-bridge/package-lock.json
generated
3674
matter-bridge/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"name": "ha-matter-bridge",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"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": "",
|
||||
"license": "GPL-3.0",
|
||||
@@ -18,11 +20,28 @@
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"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/ws": "^8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.1",
|
||||
"@typescript-eslint/parser": "^6.19.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
"commitizen": "^4.3.0",
|
||||
"cz-conventional-changelog": "^3.3.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"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "cz-conventional-changelog"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,68 +2,62 @@ import { Logger } from '@project-chip/matter-node.js/log';
|
||||
import { HassEntity, StateChangedEvent } from './HAssTypes';
|
||||
import hass, { HassApi, HassWsOptions } from 'homeassistant-ws';
|
||||
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
export class HAMiddleware {
|
||||
private logger = new Logger('HAMiddleware');
|
||||
private hassClient: HassApi;
|
||||
private static instance: HAMiddleware;
|
||||
private static callerOptions: Partial<HassWsOptions> | undefined;
|
||||
private requestFulfilled: boolean = true;
|
||||
private entities: { [k: string]: HassEntity } = {};
|
||||
private functionsToCallOnChange: {
|
||||
[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 {
|
||||
this.hassClient.rawClient.ws.close();
|
||||
}
|
||||
|
||||
async callAService(domain: string, service: string, extraArgs?: unknown) {
|
||||
this.requestFulfilled = false;
|
||||
this.hassClient.callService(domain, service, extraArgs);
|
||||
async callAService(
|
||||
domain: string,
|
||||
service: string,
|
||||
extraArgs?: unknown,
|
||||
) {
|
||||
await this.hassClient.callService(domain, service, extraArgs);
|
||||
}
|
||||
|
||||
subscribe() {
|
||||
this.hassClient.on('state_changed', (event) => {
|
||||
this.logger.debug(event);
|
||||
const toDo = this.functionsToCallOnChange[event.data.entity_id];
|
||||
this.logger.debug(JSON.stringify(event));
|
||||
const toDo =
|
||||
this.functionsToCallOnChange[event.data.entity_id];
|
||||
if (toDo) {
|
||||
toDo(event.data);
|
||||
toDo(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
subscrieToDevice(deviceId: string, fn: (event: StateChangedEvent) => void) {
|
||||
subscribeToDevice(
|
||||
deviceId: string,
|
||||
fn: (event: StateChangedEvent) => void,
|
||||
) {
|
||||
this.functionsToCallOnChange[deviceId] = fn;
|
||||
this.logger.debug(this.functionsToCallOnChange);
|
||||
}
|
||||
|
||||
async getStates(): Promise<{ [k: string]: HassEntity }> {
|
||||
this.requestFulfilled = false;
|
||||
const states = await this.hassClient.getStates();
|
||||
const sorted = states.reduceRight<{ [k: string]: HassEntity }>(
|
||||
(last, current) => {
|
||||
last[current['entity_id']] = current;
|
||||
return last;
|
||||
},
|
||||
{}
|
||||
);
|
||||
this.logger.debug({ getStates: sorted });
|
||||
const sorted = states.reduceRight<{
|
||||
[k: string]: HassEntity;
|
||||
}>((last, current) => {
|
||||
last[current['entity_id']] = current;
|
||||
return last;
|
||||
}, {});
|
||||
this.logger.debug(JSON.stringify({ getStates: sorted }));
|
||||
this.entities = sorted;
|
||||
return this.entities;
|
||||
}
|
||||
|
||||
async getStatesPartitionedByType(): Promise<{ [k: string]: HassEntity[] }> {
|
||||
async getStatesPartitionedByType(): Promise<{
|
||||
[k: string]: HassEntity[];
|
||||
}> {
|
||||
const states = await this.getStates();
|
||||
const toReturn = Object.keys(states).reduceRight<{
|
||||
[k: string]: HassEntity[];
|
||||
@@ -75,14 +69,14 @@ export class HAMiddleware {
|
||||
prev[key].push(states[current]);
|
||||
return prev;
|
||||
}, {});
|
||||
this.logger.debug({ getStatesPartitionedByType: toReturn });
|
||||
this.logger.debug(
|
||||
JSON.stringify({ getStatesPartitionedByType: toReturn }),
|
||||
);
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
async getServices() {
|
||||
this.requestFulfilled = false;
|
||||
const states = await this.hassClient.getServices();
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
@@ -91,10 +85,9 @@ export class HAMiddleware {
|
||||
}
|
||||
|
||||
public static async getInstance(
|
||||
callerOptions?: Partial<HassWsOptions> | undefined
|
||||
callerOptions?: Partial<HassWsOptions> | undefined,
|
||||
): Promise<HAMiddleware> {
|
||||
if (!HAMiddleware.instance) {
|
||||
HAMiddleware.callerOptions = callerOptions;
|
||||
const client = await hass(callerOptions);
|
||||
HAMiddleware.instance = new HAMiddleware(client);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,12 @@ export type HassConfig = {
|
||||
config_source: string;
|
||||
recovery_mode: boolean;
|
||||
safe_mode: boolean;
|
||||
state: 'NOT_RUNNING' | 'STARTING' | 'RUNNING' | 'STOPPING' | 'FINAL_WRITE';
|
||||
state:
|
||||
| 'NOT_RUNNING'
|
||||
| 'STARTING'
|
||||
| 'RUNNING'
|
||||
| 'STOPPING'
|
||||
| 'FINAL_WRITE';
|
||||
external_url: string | null;
|
||||
internal_url: string | null;
|
||||
currency: string;
|
||||
|
||||
@@ -6,22 +6,26 @@ import { HAMiddleware } from '../home-assistant/HAmiddleware';
|
||||
|
||||
const LOGGER = new Logger('Mapper');
|
||||
|
||||
async function setHasEnties(
|
||||
async function setHasEntities(
|
||||
haMiddleware: HAMiddleware,
|
||||
bridge: Bridge
|
||||
bridge: Bridge,
|
||||
): Promise<void> {
|
||||
const entities = await haMiddleware.getStatesPartitionedByType();
|
||||
LOGGER.info({ entities });
|
||||
if (entities['light']) {
|
||||
LOGGER.info('adding ', entities['light'].length, 'light devices');
|
||||
LOGGER.info(
|
||||
'adding ',
|
||||
entities['light'].length,
|
||||
'light devices',
|
||||
);
|
||||
setLights(entities['light'], haMiddleware, bridge);
|
||||
}
|
||||
}
|
||||
|
||||
export async function addAllDevicesToBridge(
|
||||
haMiddleware: HAMiddleware,
|
||||
bridge: Bridge
|
||||
bridge: Bridge,
|
||||
): Promise<void> {
|
||||
await setHasEnties(haMiddleware, bridge);
|
||||
await setHasEntities(haMiddleware, bridge);
|
||||
haMiddleware.subscribe();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Bridge } from '../../matter';
|
||||
export type AddHaDeviceToBridge = (
|
||||
haEntity: HassEntity,
|
||||
haMiddleware: HAMiddleware,
|
||||
bridge: Bridge
|
||||
bridge: Bridge,
|
||||
) => Device;
|
||||
|
||||
export { HAMiddleware } from '../../home-assistant/HAmiddleware';
|
||||
|
||||
104
matter-bridge/src/mapper/devices/lights/DimmableLightDevice.ts
Normal file
104
matter-bridge/src/mapper/devices/lights/DimmableLightDevice.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
Device,
|
||||
DimmableLightDevice,
|
||||
} from '@project-chip/matter-node.js/device';
|
||||
import { MD5 } from 'crypto-js';
|
||||
import {
|
||||
HassEntity,
|
||||
StateChangedEvent,
|
||||
} from '../../../home-assistant/HAssTypes';
|
||||
import {
|
||||
AddHaDeviceToBridge,
|
||||
Bridge,
|
||||
HAMiddleware,
|
||||
} from '../MapperType';
|
||||
import { Logger } from '@project-chip/matter-node.js/log';
|
||||
|
||||
const LOGGER = new Logger('DimmableLight');
|
||||
export const addDimmableLightDevice: AddHaDeviceToBridge = (
|
||||
haEntity: HassEntity,
|
||||
haMiddleware: HAMiddleware,
|
||||
bridge: Bridge,
|
||||
): Device => {
|
||||
LOGGER.debug(
|
||||
`Building device ${haEntity.entity_id} \n ${JSON.stringify({
|
||||
haEntity,
|
||||
})}`,
|
||||
);
|
||||
const device = new DimmableLightDevice(
|
||||
{ onOff: haEntity.state === 'on' },
|
||||
{
|
||||
currentLevel:
|
||||
Number(haEntity.attributes['brightness']) / 255 ||
|
||||
null,
|
||||
onLevel: 0.1,
|
||||
options: {
|
||||
coupleColorTempToLevel: false,
|
||||
executeIfOff: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
const serialFromId = MD5(haEntity.entity_id).toString();
|
||||
device.addOnOffListener((value, oldValue) => {
|
||||
LOGGER.debug(
|
||||
`OnOff Event for device ${haEntity.entity_id}, ${JSON.stringify(
|
||||
{
|
||||
value,
|
||||
oldValue,
|
||||
},
|
||||
)}`,
|
||||
);
|
||||
if (value !== oldValue) {
|
||||
haMiddleware.callAService(
|
||||
'light',
|
||||
value ? 'turn_on' : 'turn_off',
|
||||
{
|
||||
entity_id: haEntity.entity_id,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
device.addCommandHandler(
|
||||
'identify',
|
||||
({ request: { identifyTime } }) =>
|
||||
LOGGER.info(
|
||||
`Identify called for OnOffDevice ${haEntity.attributes['friendly_name']} with id: ${serialFromId} and identifyTime: ${identifyTime}`,
|
||||
),
|
||||
);
|
||||
|
||||
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(
|
||||
'light',
|
||||
Number(value) > 0 ? 'turn_on' : 'turn_off',
|
||||
extraArgs,
|
||||
);
|
||||
});
|
||||
|
||||
haMiddleware.subscribeToDevice(
|
||||
haEntity.entity_id,
|
||||
(event: StateChangedEvent) => {
|
||||
LOGGER.debug(`Event for device ${haEntity.entity_id}`);
|
||||
LOGGER.debug(JSON.stringify(event));
|
||||
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;
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
import {
|
||||
Device,
|
||||
DimmableLightDevice,
|
||||
} from '@project-chip/matter-node.js/device';
|
||||
import { MD5 } from 'crypto-js';
|
||||
import {
|
||||
HassEntity,
|
||||
StateChangedEvent,
|
||||
} from '../../../home-assistant/HAssTypes';
|
||||
import { AddHaDeviceToBridge, Bridge, HAMiddleware } 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(
|
||||
{ onOff: haEntity.state === 'on' },
|
||||
{
|
||||
currentLevel: Number(haEntity.attributes['brightness']) || null,
|
||||
onLevel: 0.1,
|
||||
options: { coupleColorTempToLevel: false, executeIfOff: false },
|
||||
}
|
||||
);
|
||||
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;
|
||||
};
|
||||
@@ -1,40 +1,67 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Device, OnOffLightDevice } from '@project-chip/matter-node.js/device';
|
||||
import {
|
||||
Device,
|
||||
OnOffLightDevice,
|
||||
} from '@project-chip/matter-node.js/device';
|
||||
import { MD5 } from 'crypto-js';
|
||||
import {
|
||||
HassEntity,
|
||||
StateChangedEvent,
|
||||
} from '../../../home-assistant/HAssTypes';
|
||||
import { AddHaDeviceToBridge, Bridge, HAMiddleware } from '../MapperType';
|
||||
import {
|
||||
AddHaDeviceToBridge,
|
||||
Bridge,
|
||||
HAMiddleware,
|
||||
} from '../MapperType';
|
||||
import { Logger } from '@project-chip/matter-node.js/log';
|
||||
|
||||
const LOGGER = new Logger('OnOfflIght');
|
||||
const LOGGER = new Logger('OnOffLight');
|
||||
export const addOnOffLightDevice: AddHaDeviceToBridge = (
|
||||
haEntity: HassEntity,
|
||||
haMiddleware: HAMiddleware,
|
||||
bridge: Bridge
|
||||
bridge: Bridge,
|
||||
): Device => {
|
||||
LOGGER.debug(
|
||||
`Building device ${haEntity.entity_id} \n ${JSON.stringify({
|
||||
haEntity,
|
||||
})}`,
|
||||
);
|
||||
const device = new OnOffLightDevice();
|
||||
const serialFromId = MD5(haEntity.entity_id).toString();
|
||||
device.addOnOffListener((value, oldValue) => {
|
||||
LOGGER.debug(
|
||||
`OnOff Event for device ${haEntity.entity_id}, ${JSON.stringify(
|
||||
{
|
||||
value,
|
||||
oldValue,
|
||||
},
|
||||
)}`,
|
||||
);
|
||||
|
||||
if (value !== oldValue) {
|
||||
haMiddleware.callAService('light', value ? 'turn_on' : 'turn_off', {
|
||||
entity_id: haEntity.entity_id,
|
||||
});
|
||||
haMiddleware.callAService(
|
||||
'light',
|
||||
value ? 'turn_on' : 'turn_off',
|
||||
{
|
||||
entity_id: haEntity.entity_id,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
device.addCommandHandler(
|
||||
'identify',
|
||||
async ({ request: { identifyTime } }) =>
|
||||
({ request: { identifyTime } }) =>
|
||||
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.subscrieToDevice(
|
||||
haMiddleware.subscribeToDevice(
|
||||
haEntity.entity_id,
|
||||
(event: StateChangedEvent) => {
|
||||
LOGGER.debug(`Event for device ${haEntity.entity_id}`);
|
||||
LOGGER.debug(JSON.stringify(event));
|
||||
device.setOnOff(event.data.new_state?.state === 'on');
|
||||
}
|
||||
},
|
||||
);
|
||||
bridge.addDevice(device, {
|
||||
nodeLabel: haEntity.attributes['friendly_name'],
|
||||
|
||||
@@ -2,34 +2,36 @@ import { Logger } from '@project-chip/matter-node.js/log';
|
||||
import { HAMiddleware } from '../../../home-assistant/HAmiddleware';
|
||||
import { HassEntity } from '../../../home-assistant/HAssTypes';
|
||||
import { AddHaDeviceToBridge, Bridge } from '../MapperType';
|
||||
import { addDimmerableLightDevice } from './DimmerableLightDevice';
|
||||
import { addDimmableLightDevice } from './DimmableLightDevice';
|
||||
import { addOnOffLightDevice } from './OnOffLightDevice';
|
||||
import { Device } from '@project-chip/matter-node.js/device';
|
||||
|
||||
export * from './DimmerableLightDevice';
|
||||
export * from './DimmableLightDevice';
|
||||
export * from './OnOffLightDevice';
|
||||
|
||||
const LOGGER = new Logger('Lights');
|
||||
|
||||
const LIGHTS_MAP_FUNCTIONS: Map<string, AddHaDeviceToBridge> = new Map<
|
||||
string,
|
||||
AddHaDeviceToBridge
|
||||
>([
|
||||
['onoff', addOnOffLightDevice],
|
||||
['rgb', addDimmerableLightDevice],
|
||||
['brightness', addDimmerableLightDevice],
|
||||
]);
|
||||
const LIGHTS_MAP_FUNCTIONS: Map<string, AddHaDeviceToBridge> =
|
||||
new Map<string, AddHaDeviceToBridge>([
|
||||
['onoff', addOnOffLightDevice],
|
||||
['rgb', addDimmableLightDevice],
|
||||
['brightness', addDimmableLightDevice],
|
||||
]);
|
||||
|
||||
const LIGHTS_MAP: Map<string, Device> = new Map<string, Device>();
|
||||
|
||||
export function setLights(
|
||||
lights: HassEntity[],
|
||||
haMiddleware: HAMiddleware,
|
||||
bridge: Bridge
|
||||
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({
|
||||
colormodes: entity.attributes['supported_color_modes'],
|
||||
});
|
||||
const key = (
|
||||
entity.attributes['supported_color_modes'] as string[]
|
||||
)[0];
|
||||
LOGGER.info({ key });
|
||||
const lightBuildFunction = LIGHTS_MAP_FUNCTIONS.get(key);
|
||||
if (!lightBuildFunction) {
|
||||
@@ -37,7 +39,7 @@ export function setLights(
|
||||
}
|
||||
LIGHTS_MAP.set(
|
||||
entity.entity_id,
|
||||
lightBuildFunction(entity, haMiddleware, bridge)
|
||||
lightBuildFunction(entity, haMiddleware, bridge),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,13 +23,13 @@ export class Bridge {
|
||||
private static readonly deviceName =
|
||||
getParameter('name') || 'Matter Bridge';
|
||||
private static readonly deviceType = DeviceTypes.AGGREGATOR.code;
|
||||
private static readonly vendorName = getParameter('vendor') || 'Jatus';
|
||||
private static readonly vendorName =
|
||||
getParameter('vendor') || 'Jatus';
|
||||
private static readonly productName = 'HomeAssistant';
|
||||
private static readonly port = getIntParameter('port') ?? 5540;
|
||||
private ready = false;
|
||||
|
||||
private matterServer: MatterServer;
|
||||
private static instace: Bridge;
|
||||
private static instance: Bridge;
|
||||
private logger = new Logger('bridge');
|
||||
private storageManager: StorageManager;
|
||||
private aggregator: Aggregator;
|
||||
@@ -37,7 +37,7 @@ export class Bridge {
|
||||
|
||||
private constructor(
|
||||
matterServer: MatterServer,
|
||||
storageManager: StorageManager
|
||||
storageManager: StorageManager,
|
||||
) {
|
||||
this.matterServer = matterServer;
|
||||
this.storageManager = storageManager;
|
||||
@@ -47,12 +47,12 @@ export class Bridge {
|
||||
|
||||
public static getInstance(
|
||||
matterServer: MatterServer,
|
||||
storageManager: StorageManager
|
||||
storageManager: StorageManager,
|
||||
): Bridge {
|
||||
if (!Bridge.instace) {
|
||||
this.instace = new Bridge(matterServer, storageManager);
|
||||
if (!Bridge.instance) {
|
||||
this.instance = new Bridge(matterServer, storageManager);
|
||||
}
|
||||
return Bridge.instace;
|
||||
return Bridge.instance;
|
||||
}
|
||||
|
||||
private async setupContextAndCommissioningServer(): Promise<CommissioningServer> {
|
||||
@@ -106,12 +106,17 @@ export class Bridge {
|
||||
device: Device | ComposedDevice,
|
||||
bridgedBasicInformation?: AttributeInitialValues<
|
||||
typeof BridgedDeviceBasicInformationCluster.attributes
|
||||
>
|
||||
>,
|
||||
) {
|
||||
if (!this.commissioningServer?.isCommissioned()) {
|
||||
this.logger.warn('System not initialized, may cause crashes');
|
||||
this.logger.warn(
|
||||
'System not initialized, may cause crashes',
|
||||
);
|
||||
}
|
||||
this.aggregator.addBridgedDevice(device, bridgedBasicInformation);
|
||||
this.aggregator.addBridgedDevice(
|
||||
device,
|
||||
bridgedBasicInformation,
|
||||
);
|
||||
}
|
||||
|
||||
async start() {
|
||||
@@ -119,30 +124,32 @@ export class Bridge {
|
||||
this.commissioningServer =
|
||||
await this.setupContextAndCommissioningServer();
|
||||
this.commissioningServer.addDevice(this.aggregator);
|
||||
this.matterServer.addCommissioningServer(this.commissioningServer);
|
||||
this.matterServer.addCommissioningServer(
|
||||
this.commissioningServer,
|
||||
);
|
||||
await this.matterServer.start();
|
||||
this.logger.info('Listening');
|
||||
if (!this.commissioningServer.isCommissioned()) {
|
||||
const pairingData = this.commissioningServer.getPairingCode();
|
||||
const pairingData =
|
||||
this.commissioningServer.getPairingCode();
|
||||
const { qrPairingCode, manualPairingCode } = pairingData;
|
||||
|
||||
console.log(QrCode.get(qrPairingCode));
|
||||
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 {
|
||||
this.logger.info(
|
||||
'Device is already commissioned. Waiting for controllers to connect ...'
|
||||
'Device is already commissioned. Waiting for controllers to connect ...',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.matterServer.close();
|
||||
this.storageManager
|
||||
.close()
|
||||
.then(() => process.exit(0))
|
||||
.catch((err) => this.logger.error(err));
|
||||
await this.matterServer.close();
|
||||
await this.storageManager.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ import {
|
||||
StorageBackendDisk,
|
||||
StorageManager,
|
||||
} from '@project-chip/matter-node.js/storage';
|
||||
import { getParameter, hasParameter } from '@project-chip/matter-node.js/util';
|
||||
import {
|
||||
getParameter,
|
||||
hasParameter,
|
||||
} from '@project-chip/matter-node.js/util';
|
||||
export { Bridge } from './Bridge';
|
||||
import { Bridge } from './Bridge';
|
||||
|
||||
@@ -16,25 +19,29 @@ export function serverSetup(): {
|
||||
storageManager: StorageManager;
|
||||
} {
|
||||
if (!(MATTER_SERVER && STORAGE && STORAGE_MANAGER)) {
|
||||
const storageLocation = getParameter('store') || '/config/deviceData';
|
||||
const storageLocation =
|
||||
getParameter('store') || '/config/deviceData';
|
||||
|
||||
STORAGE = new StorageBackendDisk(
|
||||
storageLocation,
|
||||
hasParameter('clearstorage')
|
||||
hasParameter('clearstorage'),
|
||||
);
|
||||
STORAGE_MANAGER = new StorageManager(STORAGE);
|
||||
MATTER_SERVER = new MatterServer(STORAGE_MANAGER, {
|
||||
mdnsInterface: getParameter('netinterface'),
|
||||
});
|
||||
}
|
||||
return { matterServer: MATTER_SERVER, storageManager: STORAGE_MANAGER };
|
||||
return {
|
||||
matterServer: MATTER_SERVER,
|
||||
storageManager: STORAGE_MANAGER,
|
||||
};
|
||||
}
|
||||
|
||||
export function getBridge(): Bridge {
|
||||
const serverData = serverSetup();
|
||||
const bridge = Bridge.getInstance(
|
||||
serverData.matterServer,
|
||||
serverData.storageManager
|
||||
serverData.storageManager,
|
||||
);
|
||||
return bridge;
|
||||
}
|
||||
|
||||
@@ -14,20 +14,25 @@ export function hasParameter(name: string) {
|
||||
|
||||
export function getIntParameter(name: string) {
|
||||
const value = getParameter(name);
|
||||
if (value === undefined) return undefined;
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const intValue = parseInt(value, 10);
|
||||
if (isNaN(intValue))
|
||||
if (isNaN(intValue)) {
|
||||
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;
|
||||
}
|
||||
|
||||
export function commandExecutor(scriptParamName: string) {
|
||||
const script = getParameter(scriptParamName);
|
||||
if (script === undefined) return undefined;
|
||||
if (script === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return () =>
|
||||
console.log(
|
||||
`${scriptParamName}: ${execSync(script).toString().slice(0, -1)}`
|
||||
`${scriptParamName}: ${execSync(script).toString().slice(0, -1)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
name: ha-matter-bridge
|
||||
maintainer: Jatus93
|
||||
url: https://github.com/Jatus93/ha-matter-bridge
|
||||
Reference in New Issue
Block a user