first commit
This commit is contained in:
commit
30187250c2
15
.eslintrc.json
Normal file
15
.eslintrc.json
Normal 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": {
|
||||
}
|
||||
}
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
deviceData
|
||||
.env
|
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 80
|
||||
}
|
36
README.md
Normal file
36
README.md
Normal file
@ -0,0 +1,36 @@
|
||||
# HA-Matter-Bridge
|
||||
|
||||
## Purpose
|
||||
|
||||
This project serves as a proof of concept to connect HomeAssistant devices to Voice Assistants through the Matter Protocol.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker installed
|
||||
- Docker Compose installed
|
||||
- Clone this repository
|
||||
|
||||
### Configuration
|
||||
|
||||
Inside the repository, create a `.env` file with the following variables set:
|
||||
|
||||
- `HA_HOST`: HomeAssistant network address
|
||||
- `HA_PORT`: HomeAssistant port
|
||||
- `HA_ACCESS_TOKEN`: HomeAssistant access token
|
||||
|
||||
Once the `.env` file is configured, run the project with `docker-compose up` for the initial setup. You'll need to scan the QR code in the terminal.
|
||||
|
||||
## Key Features
|
||||
|
||||
- Supports two types of elements: lights and dimmable lights.
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. Help organize the project and dependencies.
|
||||
2. Add integrations for other devices.
|
||||
|
||||
## Known Issues and Limitations
|
||||
|
||||
- Currently, only lights (including dimmable lights) are working, and the system may be unstable.
|
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal 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
|
12
dockerfile
Normal file
12
dockerfile
Normal 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" ]
|
5745
package-lock.json
generated
Normal file
5745
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "ha-matter-bridge",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "ts-node ./src/index.ts"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@project-chip/matter-node.js": "^0.7.4",
|
||||
"@project-chip/matter.js-tools": "^0.7.4",
|
||||
"@types/node": "18.18",
|
||||
"crypto-js": "^4.2.0",
|
||||
"homeassistant-ws": "^0.2.2",
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.1",
|
||||
"@typescript-eslint/parser": "^6.19.1",
|
||||
"eslint": "^8.56.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
37
src/index.ts
Normal file
37
src/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { Bridge, HAMiddleware, addAllDevicesToBridge } from './matter/devices';
|
||||
import { serverSetup } from './matter/server';
|
||||
import { Logger } from '@project-chip/matter-node.js/log';
|
||||
|
||||
let LOGGER = new Logger('Main');
|
||||
let BRIDGE: Bridge;
|
||||
let HA_MIDDLEWARE: HAMiddleware;
|
||||
const SERVER_DATA = serverSetup();
|
||||
|
||||
async function run() {
|
||||
HA_MIDDLEWARE = await HAMiddleware.getInstance({
|
||||
host: process.env.HA_HOST,
|
||||
port: Number(process.env.HA_PORT),
|
||||
token: process.env.HA_ACCESS_TOKEN,
|
||||
});
|
||||
BRIDGE = Bridge.getInstance(
|
||||
SERVER_DATA.matterServer,
|
||||
SERVER_DATA.storageManager
|
||||
);
|
||||
await addAllDevicesToBridge(HA_MIDDLEWARE, BRIDGE);
|
||||
BRIDGE.start();
|
||||
}
|
||||
|
||||
run().then().catch(LOGGER.error);
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
HA_MIDDLEWARE.stop();
|
||||
BRIDGE.stop()
|
||||
.then(() => {
|
||||
// Pragmatic way to make sure the storage is correctly closed before the process ends.
|
||||
SERVER_DATA.storageManager
|
||||
.close()
|
||||
.then(() => process.exit(0))
|
||||
.catch((err) => LOGGER.error(err));
|
||||
})
|
||||
.catch((err) => LOGGER.error(err));
|
||||
});
|
140
src/matter/devices/Bridge.ts
Normal file
140
src/matter/devices/Bridge.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import {
|
||||
CommissioningServer,
|
||||
MatterServer,
|
||||
} from '@project-chip/matter-node.js';
|
||||
import {
|
||||
Aggregator,
|
||||
ComposedDevice,
|
||||
Device,
|
||||
DeviceTypes,
|
||||
} from '@project-chip/matter-node.js/device';
|
||||
import { Logger } from '@project-chip/matter-node.js/log';
|
||||
import { StorageManager } from '@project-chip/matter-node.js/storage';
|
||||
import { getIntParameter, getParameter } from '../../utils/utils';
|
||||
import { Time } from '@project-chip/matter-node.js/time';
|
||||
import { VendorId } from '@project-chip/matter-node.js/datatype';
|
||||
import { QrCode } from '@project-chip/matter-node.js/schema';
|
||||
import {
|
||||
AttributeInitialValues,
|
||||
BridgedDeviceBasicInformationCluster,
|
||||
} from '@project-chip/matter-node.js/cluster';
|
||||
|
||||
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 productName = 'node-matter OnOff-Bridge';
|
||||
private static readonly port = getIntParameter('port') ?? 5540;
|
||||
|
||||
private matterServer: MatterServer;
|
||||
private static instace: Bridge;
|
||||
private logger = new Logger('bridge');
|
||||
private storageManager: StorageManager;
|
||||
private aggregator: Aggregator;
|
||||
|
||||
private constructor(
|
||||
matterServer: MatterServer,
|
||||
storageManager: StorageManager
|
||||
) {
|
||||
this.matterServer = matterServer;
|
||||
this.storageManager = storageManager;
|
||||
|
||||
this.aggregator = new Aggregator();
|
||||
}
|
||||
|
||||
public static getInstance(
|
||||
matterServer: MatterServer,
|
||||
storageManager: StorageManager
|
||||
): Bridge {
|
||||
if (!Bridge.instace) {
|
||||
this.instace = new Bridge(matterServer, storageManager);
|
||||
}
|
||||
return Bridge.instace;
|
||||
}
|
||||
|
||||
private async setupContextAndCommissioningServer(): Promise<CommissioningServer> {
|
||||
this.logger.info('setting up context');
|
||||
await this.storageManager.initialize();
|
||||
const deviceContextStorage =
|
||||
this.storageManager.createContext('Bridge-Device');
|
||||
const passcode =
|
||||
getIntParameter('passcode') ??
|
||||
deviceContextStorage.get('passcode', 20202021);
|
||||
const discriminator =
|
||||
getIntParameter('discriminator') ??
|
||||
deviceContextStorage.get('discriminator', 3840);
|
||||
const vendorId =
|
||||
getIntParameter('vendorid') ??
|
||||
deviceContextStorage.get('vendorid', 0xfff1);
|
||||
const productId =
|
||||
getIntParameter('productid') ??
|
||||
deviceContextStorage.get('productid', 0x8000);
|
||||
const uniqueId =
|
||||
getIntParameter('uniqueid') ??
|
||||
deviceContextStorage.get('uniqueid', Time.nowMs());
|
||||
|
||||
deviceContextStorage.set('passcode', passcode);
|
||||
deviceContextStorage.set('discriminator', discriminator);
|
||||
deviceContextStorage.set('vendorid', vendorId);
|
||||
deviceContextStorage.set('productid', productId);
|
||||
deviceContextStorage.set('uniqueid', uniqueId);
|
||||
|
||||
const commissioningServer = new CommissioningServer({
|
||||
port: Bridge.port,
|
||||
deviceName: Bridge.deviceName,
|
||||
deviceType: Bridge.deviceType,
|
||||
passcode,
|
||||
discriminator,
|
||||
basicInformation: {
|
||||
vendorName: Bridge.vendorName,
|
||||
vendorId: VendorId(vendorId),
|
||||
nodeLabel: Bridge.productName,
|
||||
productName: Bridge.productName,
|
||||
productLabel: Bridge.productName,
|
||||
productId,
|
||||
serialNumber: `node-matter-${uniqueId}`,
|
||||
},
|
||||
});
|
||||
|
||||
return commissioningServer;
|
||||
}
|
||||
|
||||
addDevice(
|
||||
device: Device | ComposedDevice,
|
||||
bridgedBasicInformation?: AttributeInitialValues<
|
||||
typeof BridgedDeviceBasicInformationCluster.attributes
|
||||
>
|
||||
) {
|
||||
// const id = getIntParameter('uniqueid');
|
||||
this.aggregator.addBridgedDevice(device, bridgedBasicInformation);
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.logger.info('Starting...');
|
||||
const commissioningServer =
|
||||
await this.setupContextAndCommissioningServer();
|
||||
commissioningServer.addDevice(this.aggregator);
|
||||
this.matterServer.addCommissioningServer(commissioningServer);
|
||||
await this.matterServer.start();
|
||||
this.logger.info('Listening');
|
||||
if (!commissioningServer.isCommissioned()) {
|
||||
const pairingData = 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}`
|
||||
);
|
||||
this.logger.info(`Manual pairing code: ${manualPairingCode}`);
|
||||
} else {
|
||||
this.logger.info(
|
||||
'Device is already commissioned. Waiting for controllers to connect ...'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.matterServer.close();
|
||||
}
|
||||
}
|
109
src/matter/devices/HAmiddleware.ts
Normal file
109
src/matter/devices/HAmiddleware.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { Logger } from '@project-chip/matter-node.js/log';
|
||||
import { HassEntity, HassEvent } 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 connectionOpen: boolean = false;
|
||||
private requestFulfilled: boolean = true;
|
||||
private entities: { [k: string]: HassEntity } = {};
|
||||
private functionsToCallOnChange: {
|
||||
[k: string]: ((data: HassEvent['data']) => 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);
|
||||
}
|
||||
|
||||
subscribe() {
|
||||
this.hassClient.on('state_changed', (stateChangedEvent) => {
|
||||
this.logger.debug(stateChangedEvent.data);
|
||||
const toDo =
|
||||
this.functionsToCallOnChange[stateChangedEvent.data.entity_id];
|
||||
if (toDo) {
|
||||
toDo(stateChangedEvent.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
subscrieToDevice(deviceId: string, fn: (data: HassEvent['data']) => 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 });
|
||||
this.entities = sorted;
|
||||
return this.entities;
|
||||
}
|
||||
|
||||
async getStatesPartitionedByType(): Promise<{ [k: string]: HassEntity[] }> {
|
||||
const states = await this.getStates();
|
||||
const toReturn = Object.keys(states).reduceRight<{
|
||||
[k: string]: HassEntity[];
|
||||
}>((prev, current) => {
|
||||
const key = current.split('.')[0];
|
||||
if (!prev[key] && !Array.isArray(prev[key])) {
|
||||
prev[key] = new Array<HassEntity>();
|
||||
}
|
||||
prev[key].push(states[current]);
|
||||
return prev;
|
||||
}, {});
|
||||
this.logger.debug({ getStatesPartitionedByType: toReturn });
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
async getServices() {
|
||||
this.requestFulfilled = false;
|
||||
const states = await this.hassClient.getServices();
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
private constructor(client: HassApi) {
|
||||
this.hassClient = client;
|
||||
}
|
||||
|
||||
public static async getInstance(
|
||||
callerOptions?: Partial<HassWsOptions> | undefined
|
||||
): Promise<HAMiddleware> {
|
||||
if (!HAMiddleware.instance) {
|
||||
const client = await hass(callerOptions);
|
||||
HAMiddleware.instance = new HAMiddleware(client);
|
||||
let waited = 0;
|
||||
const timeOut = 5000;
|
||||
while (!HAMiddleware.instance.connectionOpen && waited < timeOut) {
|
||||
await sleep(1000);
|
||||
waited += 1000;
|
||||
}
|
||||
}
|
||||
return HAMiddleware.instance;
|
||||
}
|
||||
}
|
133
src/matter/devices/HAssTypes.ts
Normal file
133
src/matter/devices/HAssTypes.ts
Normal file
@ -0,0 +1,133 @@
|
||||
// This file has no imports on purpose
|
||||
// So it can easily be consumed by other TS projects
|
||||
|
||||
export type Error = 1 | 2 | 3 | 4;
|
||||
|
||||
export type UnsubscribeFunc = () => void;
|
||||
|
||||
export type MessageBase = {
|
||||
id?: number;
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type Context = {
|
||||
id: string;
|
||||
user_id: string | null;
|
||||
parent_id: string | null;
|
||||
};
|
||||
|
||||
export type HassEventBase = {
|
||||
origin: string;
|
||||
time_fired: string;
|
||||
context: Context;
|
||||
};
|
||||
|
||||
export type HassEvent = HassEventBase & {
|
||||
event_type: string;
|
||||
data: { [key: string]: unknown };
|
||||
};
|
||||
|
||||
export type StateChangedEvent = HassEventBase & {
|
||||
event_type: 'state_changed';
|
||||
data: {
|
||||
entity_id: string;
|
||||
new_state: HassEntity | null;
|
||||
old_state: HassEntity | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type HassConfig = {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
elevation: number;
|
||||
unit_system: {
|
||||
length: string;
|
||||
mass: string;
|
||||
volume: string;
|
||||
temperature: string;
|
||||
pressure: string;
|
||||
wind_speed: string;
|
||||
accumulated_precipitation: string;
|
||||
};
|
||||
location_name: string;
|
||||
time_zone: string;
|
||||
components: string[];
|
||||
config_dir: string;
|
||||
allowlist_external_dirs: string[];
|
||||
allowlist_external_urls: string[];
|
||||
version: string;
|
||||
config_source: string;
|
||||
recovery_mode: boolean;
|
||||
safe_mode: boolean;
|
||||
state: 'NOT_RUNNING' | 'STARTING' | 'RUNNING' | 'STOPPING' | 'FINAL_WRITE';
|
||||
external_url: string | null;
|
||||
internal_url: string | null;
|
||||
currency: string;
|
||||
country: string | null;
|
||||
language: string;
|
||||
};
|
||||
|
||||
export type HassEntityBase = {
|
||||
entity_id: string;
|
||||
state: string;
|
||||
last_changed: string;
|
||||
last_updated: string;
|
||||
attributes: HassEntityAttributeBase;
|
||||
context: Context;
|
||||
};
|
||||
|
||||
export type HassEntityAttributeBase = {
|
||||
friendly_name?: string;
|
||||
unit_of_measurement?: string;
|
||||
icon?: string;
|
||||
entity_picture?: string;
|
||||
supported_features?: number;
|
||||
hidden?: boolean;
|
||||
assumed_state?: boolean;
|
||||
device_class?: string;
|
||||
state_class?: string;
|
||||
restored?: boolean;
|
||||
};
|
||||
|
||||
export type HassEntity = HassEntityBase & {
|
||||
attributes: { [key: string]: unknown };
|
||||
};
|
||||
|
||||
export type HassEntities = { [entity_id: string]: HassEntity };
|
||||
|
||||
export type HassService = {
|
||||
name?: string;
|
||||
description: string;
|
||||
target?: object | null;
|
||||
fields: {
|
||||
[field_name: string]: {
|
||||
name?: string;
|
||||
description: string;
|
||||
example: string | boolean | number;
|
||||
selector?: object;
|
||||
};
|
||||
};
|
||||
response?: { optional: boolean };
|
||||
};
|
||||
|
||||
export type HassDomainServices = {
|
||||
[service_name: string]: HassService;
|
||||
};
|
||||
|
||||
export type HassServices = {
|
||||
[domain: string]: HassDomainServices;
|
||||
};
|
||||
|
||||
export type HassUser = {
|
||||
id: string;
|
||||
is_admin: boolean;
|
||||
is_owner: boolean;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type HassServiceTarget = {
|
||||
entity_id?: string | string[];
|
||||
device_id?: string | string[];
|
||||
area_id?: string | string[];
|
||||
};
|
179
src/matter/devices/Mapper.ts
Normal file
179
src/matter/devices/Mapper.ts
Normal file
@ -0,0 +1,179 @@
|
||||
/* 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();
|
||||
}
|
3
src/matter/devices/index.ts
Normal file
3
src/matter/devices/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './Bridge';
|
||||
export * from './HAmiddleware';
|
||||
export * from './Mapper';
|
29
src/matter/server/index.ts
Normal file
29
src/matter/server/index.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { MatterServer } from '@project-chip/matter-node.js';
|
||||
import {
|
||||
StorageBackendDisk,
|
||||
StorageManager,
|
||||
} from '@project-chip/matter-node.js/storage';
|
||||
import { getParameter, hasParameter } from '@project-chip/matter-node.js/util';
|
||||
|
||||
let MATTER_SERVER: MatterServer;
|
||||
let STORAGE: StorageBackendDisk;
|
||||
let STORAGE_MANAGER: StorageManager;
|
||||
|
||||
export function serverSetup(): {
|
||||
matterServer: MatterServer;
|
||||
storageManager: StorageManager;
|
||||
} {
|
||||
if (!(MATTER_SERVER && STORAGE && STORAGE_MANAGER)) {
|
||||
const storageLocation = getParameter('store') || './deviceData';
|
||||
|
||||
STORAGE = new StorageBackendDisk(
|
||||
storageLocation,
|
||||
hasParameter('clearstorage')
|
||||
);
|
||||
STORAGE_MANAGER = new StorageManager(STORAGE);
|
||||
MATTER_SERVER = new MatterServer(STORAGE_MANAGER, {
|
||||
mdnsInterface: getParameter('netinterface'),
|
||||
});
|
||||
}
|
||||
return { matterServer: MATTER_SERVER, storageManager: STORAGE_MANAGER };
|
||||
}
|
32
src/utils/utils.ts
Normal file
32
src/utils/utils.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { ValidationError } from '@project-chip/matter.js/common';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const envVar = process.env;
|
||||
|
||||
export function getParameter(name: string) {
|
||||
return envVar[name];
|
||||
}
|
||||
|
||||
export function hasParameter(name: string) {
|
||||
return getIntParameter(name) !== undefined;
|
||||
}
|
||||
|
||||
export function getIntParameter(name: string) {
|
||||
const value = getParameter(name);
|
||||
if (value === undefined) return undefined;
|
||||
const intValue = parseInt(value, 10);
|
||||
if (isNaN(intValue))
|
||||
throw new ValidationError(
|
||||
`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;
|
||||
return () =>
|
||||
console.log(
|
||||
`${scriptParamName}: ${execSync(script).toString().slice(0, -1)}`
|
||||
);
|
||||
}
|
102
tsconfig.json
Normal file
102
tsconfig.json
Normal file
@ -0,0 +1,102 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
/* Language and Environment */
|
||||
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
/* Modules */
|
||||
// "module": "commonjs", /* Specify what module code is generated. */
|
||||
"module": "Node16" /* Specify what module code is generated. */,
|
||||
// // "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node16" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
|
||||
},
|
||||
}
|
Loading…
Reference in New Issue
Block a user