Create importer scaffolding
This commit is contained in:
7
src/importer/index.ts
Normal file
7
src/importer/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Thing } from "../openhab/types";
|
||||
import { Adapter } from "./types";
|
||||
|
||||
export const importThings = async (adapter: Adapter): Promise<Thing[]> => {
|
||||
const things = await adapter.loadThings();
|
||||
return things;
|
||||
}
|
||||
5
src/importer/types.ts
Normal file
5
src/importer/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Thing } from "../openhab/types";
|
||||
|
||||
export interface Adapter {
|
||||
loadThings(): Promise<Thing[]>;
|
||||
};
|
||||
30
src/index.ts
30
src/index.ts
@@ -1,23 +1,19 @@
|
||||
import { fromZ2M } from "./z2m/loader";
|
||||
import { importThings } from "./importer";
|
||||
import { snakecase } from "./utils/string";
|
||||
import { Z2MAdapter } from "./z2m";
|
||||
|
||||
export type Config = {
|
||||
bridgeID: string;
|
||||
prefix: String;
|
||||
username: string;
|
||||
password: string;
|
||||
clientId?: string;
|
||||
};
|
||||
|
||||
async function run(config: Config) {
|
||||
const things = await fromZ2M(config);
|
||||
console.log(things);
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
const adapter = new Z2MAdapter({
|
||||
bridgeID: "main",
|
||||
prefix: "zigbee",
|
||||
username: 'test',
|
||||
password: 'test',
|
||||
};
|
||||
idTransform: snakecase("-")
|
||||
});
|
||||
|
||||
run(config);
|
||||
|
||||
async function run() {
|
||||
const things = await importThings(adapter);
|
||||
console.log(things);
|
||||
}
|
||||
|
||||
run();
|
||||
5
src/utils/string.ts
Normal file
5
src/utils/string.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const snakecase = (join = "_") => (text: string): string => text
|
||||
.split(/(?<![A-Z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|-|_/)
|
||||
.filter(n => n.trim() !== "")
|
||||
.join(join)
|
||||
.toLowerCase();
|
||||
@@ -1,19 +1,28 @@
|
||||
import mqtt from "mqtt";
|
||||
import { Device, Feature, FeatureType } from "./types";
|
||||
import { Config } from "..";
|
||||
import { Config } from "./types";
|
||||
import { Channel, ChannelType, ItemType, Thing } from "../openhab/types";
|
||||
import { Adapter } from "../importer/types";
|
||||
|
||||
/**
|
||||
* Fetches devices from Exposes API of Zigbee2MQTT and converts them
|
||||
* to OpenHAB Thing payload.
|
||||
* @param config - the configuration of MQTT network
|
||||
* @returns promise with list of OpenHAB things payload
|
||||
*/
|
||||
export const fromZ2M = async (config: Config): Promise<Thing[]> => {
|
||||
const devices = await fetchModel(config);
|
||||
|
||||
return devices.flatMap(toThing(config));
|
||||
}
|
||||
export class Z2MAdapter implements Adapter {
|
||||
_config: Config;
|
||||
|
||||
constructor(config: Config) {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches devices from Exposes API of Zigbee2MQTT and converts them
|
||||
* to OpenHAB Thing payload.
|
||||
* @param config - the configuration of MQTT network
|
||||
* @returns promise with list of OpenHAB things payload
|
||||
*/
|
||||
async loadThings(): Promise<Thing[]> {
|
||||
const devices = await fetchModel(this._config);
|
||||
return devices.flatMap(toThing(this._config));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches devices model from Exposes API of Zigbee2MQTT service.
|
||||
@@ -24,7 +33,7 @@ const fetchModel = ({ username, password, prefix, clientId }: Config): Promise<D
|
||||
const client = mqtt.connect("mqtt://mqtt.lan", { username, password, clientId: clientId || "zigbee2mqtt->openHAB importer" });
|
||||
|
||||
client.on("message", (_, message) => {
|
||||
client.end();
|
||||
client.end();
|
||||
resolve(JSON.parse(message.toString()) as Device[]);
|
||||
});
|
||||
|
||||
@@ -57,30 +66,50 @@ const isWriteable = isBitSet(1);
|
||||
*/
|
||||
const isActiveReadable = isBitSet(2);
|
||||
|
||||
const toChannels = (parentTopic: string, parentUID: string, type?: string) => (feature: Feature): Channel[] => {
|
||||
/**
|
||||
* Maps Z2M feature to list of OpenHAB channels.
|
||||
* It can support both atomic features as well as complex one (like composites,
|
||||
* specific items like fans, switches, lights etc.).
|
||||
* @param parentTopic topic of parent node
|
||||
* @param parentUID UID of parent node
|
||||
* @param type optionakl type which will be used as a prefix for description of channel
|
||||
* @returns list of channels mapped from supported features
|
||||
*/
|
||||
const toChannels = (config: Config, parentTopic: string, parentUID: string, type?: string) => (feature: Feature): Channel[] => {
|
||||
if (feature.features) {
|
||||
const topic = feature.property ? `${parentTopic}/${feature.property}` : parentTopic;
|
||||
return feature.features.flatMap(parseAtomFeature(topic, parentUID, type));
|
||||
return feature.features.flatMap(parseAtomFeature(config, topic, parentUID, type));
|
||||
}
|
||||
|
||||
return parseAtomFeature(parentTopic, parentUID, type)(feature);
|
||||
return parseAtomFeature(config, parentTopic, parentUID, type)(feature);
|
||||
};
|
||||
|
||||
const parseAtomFeature = (parentTopic: string, parentUID: string, type?: string) => (feature: Feature): Channel[] => {
|
||||
const readable = isPassiveReadable(feature.access);
|
||||
const writeable = isWriteable(feature.access);
|
||||
/**
|
||||
* Maps Z2M atomic feature (numeric, binary, enum etc.) to list of OpenHAB channels.
|
||||
* It will always either return a list with single channel or throw an exception for unsupported feature,
|
||||
* as it is not capable to support the complex features.
|
||||
* @param parentTopic topic of parent node
|
||||
* @param parentUID UID of parent node
|
||||
* @param type optionakl type which will be used as a prefix for description of channel
|
||||
* @returns list of channels mapped from atomic features (like numeric, binary, enum etc.)
|
||||
*/
|
||||
const parseAtomFeature = ({ idTransform }: Config, parentTopic: string, parentUID: string, type?: string) => (feature: Feature): Channel[] => {
|
||||
const readable = isPassiveReadable(feature.access);
|
||||
const writeable = isWriteable(feature.access);
|
||||
|
||||
const isTrigger = feature.type === 'enum' && readable && !writeable && !isActiveReadable(feature.access);
|
||||
|
||||
const channelTypes: Partial<Record<FeatureType, ChannelType|undefined>> = {
|
||||
binary: writeable ? 'mqtt:switch' : 'mqtt:contact',
|
||||
numeric: 'mqtt:number',
|
||||
enum: 'mqtt:string',
|
||||
enum: isTrigger ? 'mqtt:trigger' : 'mqtt:string',
|
||||
text: 'mqtt:string',
|
||||
};
|
||||
|
||||
const itemTypes: Partial<Record<FeatureType, ItemType|undefined>> = {
|
||||
binary: writeable ? 'Switch' : 'Contact',
|
||||
numeric: 'Number',
|
||||
enum: 'String',
|
||||
enum: !isTrigger ? 'String' : undefined,
|
||||
text: 'String',
|
||||
};
|
||||
|
||||
@@ -91,14 +120,15 @@ const parseAtomFeature = (parentTopic: string, parentUID: string, type?: string)
|
||||
throw new Error(`Unsupported feature type ${feature.type} for: ${JSON.stringify(feature)}`)
|
||||
}
|
||||
|
||||
const id = idTransform?.(feature.name) || feature.name;
|
||||
const category = type ?? feature.category;
|
||||
const prefix = category ? `${category[0].toUpperCase()}${category.slice(1)}: ` : "";
|
||||
|
||||
return [{
|
||||
id,
|
||||
channelTypeUID,
|
||||
itemType: itemTypes[feature.type],
|
||||
id: feature.name,
|
||||
uid: `${parentUID}:${feature.name}`,
|
||||
itemType: itemTypes[feature.type],
|
||||
uid: `${parentUID}:${id}`,
|
||||
kind: "STATE",
|
||||
label: feature.label,
|
||||
description: `${prefix}${feature.description}`,
|
||||
@@ -117,11 +147,16 @@ const parseAtomFeature = (parentTopic: string, parentUID: string, type?: string)
|
||||
}];
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps Z2M device from Exposes API to OpenHAB Thing model.
|
||||
* @param config MQTT configuration
|
||||
* @returns mapped thing
|
||||
*/
|
||||
const toThing = (config: Config) => (device: Device): Thing => {
|
||||
const UID = `mqtt:topic:${config.bridgeID}:${device.friendly_name}`;
|
||||
const thingTopic = `${config.prefix}/${device.friendly_name}`
|
||||
const exposes = device.definition?.exposes?.flatMap(toChannels(thingTopic, UID)) ?? [];
|
||||
const options = device.definition?.options?.flatMap(toChannels(thingTopic, UID, "option")) ?? [];
|
||||
const exposes = device.definition?.exposes?.flatMap(toChannels(config, thingTopic, UID)) ?? [];
|
||||
const options = device.definition?.options?.flatMap(toChannels(config, thingTopic, UID, "option")) ?? [];
|
||||
|
||||
return {
|
||||
UID,
|
||||
@@ -1,3 +1,35 @@
|
||||
export type Config = {
|
||||
/**
|
||||
* ID of the MQTT bridge in OpenHAB
|
||||
*/
|
||||
bridgeID: string;
|
||||
|
||||
/**
|
||||
* Prefix of the Z2M topic in MQTT network
|
||||
*/
|
||||
prefix: String;
|
||||
|
||||
/**
|
||||
* Username of MQTT user
|
||||
*/
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* Password of MQTT user
|
||||
*/
|
||||
password: string;
|
||||
|
||||
/**
|
||||
* Optional client ID displayed in MQTT broker logs
|
||||
*/
|
||||
clientId?: string;
|
||||
|
||||
/**
|
||||
* Optional transformation applied to ID
|
||||
*/
|
||||
idTransform?: (text: string) => string;
|
||||
};
|
||||
|
||||
export type Device = {
|
||||
definition?: Definition;
|
||||
disabled: boolean;
|
||||
|
||||
Reference in New Issue
Block a user