adding ha addon support

This commit is contained in:
Jatus 2024-01-26 23:19:14 +01:00
parent 0425f6e8df
commit 6774e14ecd
34 changed files with 6585 additions and 0 deletions

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
ARG BUILD_FROM
FROM $BUILD_FROM
COPY ./package.json ./
COPY ./package-lock.json ./
COPY ./src ./src
COPY ./tsconfig.json ./
RUN npm install
RUN npm install ts-node
COPY run.sh /
RUN chmod a+x /run.sh
CMD [ "/run.sh" ]

9
config.yml Normal file
View File

@ -0,0 +1,9 @@
name: "HA Matter Bridge"
version: "1.0.0"
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
arch:
- amd64

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": {
}
}

3
matter-bridge/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
deviceData
.env

1
matter-bridge/.nvmrc Normal file
View File

@ -0,0 +1 @@
v18.18.2

View File

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

14
matter-bridge/Dockerfile Normal file
View File

@ -0,0 +1,14 @@
ARG BUILD_FROM=ghcr.io/hassio-addons/base:15.0.3
FROM ${BUILD_FROM}
COPY ./package.json ./
COPY ./package-lock.json ./
COPY ./src ./src
COPY ./tsconfig.json ./
RUN npm install
RUN npm install ts-node
COPY run.sh /
RUN chmod a+x /run.sh
CMD [ "/run.sh" ]

5
matter-bridge/Makefile Normal file
View File

@ -0,0 +1,5 @@
-include .env
export
run:
npm run start

12
matter-bridge/config.yml Normal file
View File

@ -0,0 +1,12 @@
---
name: 'HA Matter Bridge'
version: '1.0.0'
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
arch:
- amd64
ports:
5540/tcp: 5540
hassio_role: admin

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" ]

View File

@ -0,0 +1,5 @@
message: "settings"
settings:
- HA_HOST: localhost
HA_PORT: 8123
HA_ACCESS_TOKEN: "your acces token"

5745
matter-bridge/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View 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": "GPL-3.0",
"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"
}
}

View File

@ -0,0 +1 @@
name: ha-matter-bridge

3
matter-bridge/run.sh Normal file
View File

@ -0,0 +1,3 @@
#!/usr/bin/with-contenv bashio
npm run start

5
matter-bridge/schema.yml Normal file
View File

@ -0,0 +1,5 @@
message: str
settings:
- HA_HOST: str
HA_PORT: int
HA_ACCESS_TOKEN: str

View File

@ -0,0 +1,108 @@
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 connectionOpen: boolean = false;
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);
}
subscribe() {
this.hassClient.on('state_changed', (event) => {
this.logger.debug(event);
const toDo = this.functionsToCallOnChange[event.data.entity_id];
if (toDo) {
toDo(event.data);
}
});
}
subscrieToDevice(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 });
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;
}
}

View 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[];
};

View File

@ -0,0 +1,32 @@
import {
getIntParameter,
getParameter,
} from '@project-chip/matter-node.js/util';
import { Bridge, getBridge } from './matter';
import { Logger } from '@project-chip/matter-node.js/log';
import { HAMiddleware } from './home-assistant/HAmiddleware';
import { addAllDevicesToBridge } from './mapper/Mapper';
const LOGGER = new Logger('Main');
let HA_MIDDLEWARE: HAMiddleware;
let BRIDGE: Bridge;
async function run() {
HA_MIDDLEWARE = await HAMiddleware.getInstance({
host: getParameter('HA_HOST'),
port: getIntParameter('HA_PORT'),
token: getParameter('HA_ACCESS_TOKEN'),
});
BRIDGE = getBridge();
await addAllDevicesToBridge(HA_MIDDLEWARE, BRIDGE);
BRIDGE.start();
}
run().then().catch(LOGGER.error);
process.on('SIGINT', () => {
HA_MIDDLEWARE.stop();
BRIDGE.stop()
.then(() => process.exit(0))
.catch((err) => LOGGER.error(err));
});

View File

@ -0,0 +1,60 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
addDimmerableLightDevice,
addOnOffLightDevice,
} from './devices/lights';
import { HassEntity } from '../home-assistant/HAssTypes';
import { Bridge } from '../matter';
import { Logger } from '@project-chip/matter-node.js/log';
import { HAMiddleware } from '../home-assistant/HAmiddleware';
const LOGGER = new Logger('Mapper');
const lightsMap: Map<
string,
(haEntity: HassEntity, haMiddleware: HAMiddleware, bridge: Bridge) => void
> = new Map<
string,
(haEntity: HassEntity, haMiddleware: HAMiddleware, bridge: Bridge) => void
>([
['onoff', addOnOffLightDevice],
['rgb', addDimmerableLightDevice],
['brightness', addDimmerableLightDevice],
]);
function setLights(
lights: HassEntity[],
haMiddleware: HAMiddleware,
bridge: Bridge
) {
lights.forEach((entity) => {
LOGGER.info({ colormodes: entity.attributes['supported_color_modes'] });
const key = (entity.attributes['supported_color_modes'] as string[])[0];
LOGGER.info({ key });
const lightBuildFunction = lightsMap.get(key);
if (!lightBuildFunction) {
throw new Error('Missing ' + key);
}
return lightBuildFunction(entity, haMiddleware, bridge);
});
}
async function setHasEnties(
haMiddleware: HAMiddleware,
bridge: Bridge
): Promise<void> {
const entities = await haMiddleware.getStatesPartitionedByType();
LOGGER.info({ entities });
if (entities['light']) {
LOGGER.info('adding ', entities['light'].length, 'light devices');
setLights(entities['light'], haMiddleware, bridge);
}
}
export async function addAllDevicesToBridge(
haMiddleware: HAMiddleware,
bridge: Bridge
): Promise<void> {
await setHasEnties(haMiddleware, bridge);
haMiddleware.subscribe();
}

View File

@ -0,0 +1,14 @@
import { Device } from '@project-chip/matter-node.js/device';
import { HassEntity } from '../../home-assistant/HAssTypes';
import { HAMiddleware } from '../../home-assistant/HAmiddleware';
import { Bridge } from '../../matter';
export type AddHaDeviceToBridge = (
haEntity: HassEntity,
haMiddleware: HAMiddleware,
bridge: Bridge
) => Device;
export { HAMiddleware } from '../../home-assistant/HAmiddleware';
export { Bridge } from '../../matter';

View File

@ -0,0 +1,59 @@
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();
const serialFromId = MD5(haEntity.entity_id).toString();
device.addOnOffListener((value, oldValue) => {
if (value !== oldValue) {
haMiddleware.callAService('light', value ? 'turn_on' : 'turn_off', {
entity_id: haEntity.entity_id,
});
}
});
device.addCommandHandler(
'identify',
async ({ request: { identifyTime } }) =>
LOGGER.info(
`Identify called for OnOffDevice ${haEntity.attributes['friendly_name']} with id: ${serialFromId} and identifyTime: ${identifyTime}`
)
);
device.addCurrentLevelListener((value) => {
haMiddleware.callAService(
'light',
Number(value) > 0 ? 'turn_on' : 'turn_off',
{ entity_id: haEntity.entity_id, brightness: Number(value) }
);
});
haMiddleware.subscrieToDevice(
haEntity.entity_id,
(event: StateChangedEvent) => {
device.setOnOff(event.data.new_state?.state === 'on');
device.setCurrentLevel(
(event.data.new_state?.attributes as never)['brightness']
);
}
);
bridge.addDevice(device, {
nodeLabel: haEntity.attributes['friendly_name'],
reachable: true,
serialNumber: serialFromId,
});
return device;
};

View File

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

View File

@ -0,0 +1,2 @@
export * from './DimmerableLightDevice';
export * from './OnOffLightDevice';

View File

@ -0,0 +1,144 @@
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 = 'HomeAssistant';
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();
this.storageManager
.close()
.then(() => process.exit(0))
.catch((err) => this.logger.error(err));
}
}

View File

@ -0,0 +1,40 @@
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';
export { Bridge } from './Bridge';
import { Bridge } from './Bridge';
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 };
}
export function getBridge(): Bridge {
const serverData = serverSetup();
const bridge = Bridge.getInstance(
serverData.matterServer,
serverData.storageManager
);
return bridge;
}

View 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)}`
);
}

View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"module": "Node16" /* Specify what module code is generated. */,
"rootDir": "./src", /* Specify the root folder within your source files. */
"moduleResolution": "node16" /* Specify how TypeScript looks up a file from a given module specifier. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
},
}

5
options.yml Normal file
View File

@ -0,0 +1,5 @@
message: "settings"
settings:
- HA_HOST: localhost
HA_PORT: 8123
HA_ACCESS_TOKEN: "your acces token"

1
repository.yml Normal file
View File

@ -0,0 +1 @@
name: ha-matter-bridge

3
run.sh Normal file
View File

@ -0,0 +1,3 @@
#!/usr/bin/with-contenv bashio
npm run start

5
schema.yml Normal file
View File

@ -0,0 +1,5 @@
message: str
settings:
- HA_HOST: str
HA_PORT: int
HA_ACCESS_TOKEN: str