Create importer scaffolding

This commit is contained in:
2024-11-11 22:50:48 +01:00
parent e583bc19ad
commit e791bce02c
6 changed files with 122 additions and 42 deletions

7
src/importer/index.ts Normal file
View 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
View File

@@ -0,0 +1,5 @@
import { Thing } from "../openhab/types";
export interface Adapter {
loadThings(): Promise<Thing[]>;
};

View File

@@ -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
View 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();

View File

@@ -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";
/**
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
*/
export const fromZ2M = async (config: Config): Promise<Thing[]> => {
const devices = await fetchModel(config);
return devices.flatMap(toThing(config));
}
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.
@@ -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[] => {
/**
* 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}`,
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,

View File

@@ -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;