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 = {
|
const adapter = new Z2MAdapter({
|
||||||
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 = {
|
|
||||||
bridgeID: "main",
|
bridgeID: "main",
|
||||||
prefix: "zigbee",
|
prefix: "zigbee",
|
||||||
username: 'test',
|
username: 'test',
|
||||||
password: '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 mqtt from "mqtt";
|
||||||
import { Device, Feature, FeatureType } from "./types";
|
import { Device, Feature, FeatureType } from "./types";
|
||||||
import { Config } from "..";
|
import { Config } from "./types";
|
||||||
import { Channel, ChannelType, ItemType, Thing } from "../openhab/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.
|
* 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" });
|
const client = mqtt.connect("mqtt://mqtt.lan", { username, password, clientId: clientId || "zigbee2mqtt->openHAB importer" });
|
||||||
|
|
||||||
client.on("message", (_, message) => {
|
client.on("message", (_, message) => {
|
||||||
client.end();
|
client.end();
|
||||||
resolve(JSON.parse(message.toString()) as Device[]);
|
resolve(JSON.parse(message.toString()) as Device[]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,30 +66,50 @@ const isWriteable = isBitSet(1);
|
|||||||
*/
|
*/
|
||||||
const isActiveReadable = isBitSet(2);
|
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) {
|
if (feature.features) {
|
||||||
const topic = feature.property ? `${parentTopic}/${feature.property}` : parentTopic;
|
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);
|
* Maps Z2M atomic feature (numeric, binary, enum etc.) to list of OpenHAB channels.
|
||||||
const writeable = isWriteable(feature.access);
|
* 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>> = {
|
const channelTypes: Partial<Record<FeatureType, ChannelType|undefined>> = {
|
||||||
binary: writeable ? 'mqtt:switch' : 'mqtt:contact',
|
binary: writeable ? 'mqtt:switch' : 'mqtt:contact',
|
||||||
numeric: 'mqtt:number',
|
numeric: 'mqtt:number',
|
||||||
enum: 'mqtt:string',
|
enum: isTrigger ? 'mqtt:trigger' : 'mqtt:string',
|
||||||
text: 'mqtt:string',
|
text: 'mqtt:string',
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemTypes: Partial<Record<FeatureType, ItemType|undefined>> = {
|
const itemTypes: Partial<Record<FeatureType, ItemType|undefined>> = {
|
||||||
binary: writeable ? 'Switch' : 'Contact',
|
binary: writeable ? 'Switch' : 'Contact',
|
||||||
numeric: 'Number',
|
numeric: 'Number',
|
||||||
enum: 'String',
|
enum: !isTrigger ? 'String' : undefined,
|
||||||
text: 'String',
|
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)}`)
|
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 category = type ?? feature.category;
|
||||||
const prefix = category ? `${category[0].toUpperCase()}${category.slice(1)}: ` : "";
|
const prefix = category ? `${category[0].toUpperCase()}${category.slice(1)}: ` : "";
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
|
id,
|
||||||
channelTypeUID,
|
channelTypeUID,
|
||||||
itemType: itemTypes[feature.type],
|
itemType: itemTypes[feature.type],
|
||||||
id: feature.name,
|
uid: `${parentUID}:${id}`,
|
||||||
uid: `${parentUID}:${feature.name}`,
|
|
||||||
kind: "STATE",
|
kind: "STATE",
|
||||||
label: feature.label,
|
label: feature.label,
|
||||||
description: `${prefix}${feature.description}`,
|
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 toThing = (config: Config) => (device: Device): Thing => {
|
||||||
const UID = `mqtt:topic:${config.bridgeID}:${device.friendly_name}`;
|
const UID = `mqtt:topic:${config.bridgeID}:${device.friendly_name}`;
|
||||||
const thingTopic = `${config.prefix}/${device.friendly_name}`
|
const thingTopic = `${config.prefix}/${device.friendly_name}`
|
||||||
const exposes = device.definition?.exposes?.flatMap(toChannels(thingTopic, UID)) ?? [];
|
const exposes = device.definition?.exposes?.flatMap(toChannels(config, thingTopic, UID)) ?? [];
|
||||||
const options = device.definition?.options?.flatMap(toChannels(thingTopic, UID, "option")) ?? [];
|
const options = device.definition?.options?.flatMap(toChannels(config, thingTopic, UID, "option")) ?? [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
UID,
|
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 = {
|
export type Device = {
|
||||||
definition?: Definition;
|
definition?: Definition;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user