Skip to content

Instantly share code, notes, and snippets.

@many20
Last active August 23, 2021 16:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save many20/3a5b047bd221ec107153c0231b575ca3 to your computer and use it in GitHub Desktop.
Save many20/3a5b047bd221ec107153c0231b575ca3 to your computer and use it in GitHub Desktop.
ioBroker VirtualDevice port from https://forum.iobroker.net/topic/7751/virtual-devices/2 to TypeScript
//https://forum.iobroker.net/topic/7751/virtual-devices/2
/*
VirtualDevice v0.3
{1}
Structure of config:
{
name: 'name', //name of device
namespace: '', //create device within this namespace (device ID will be namespace.name)
common: {}, //(optional)
native: {}, //(optional)
copy: objectId, //(optional) ID of device or channel to copy common and native from
onCreate: function(device, callback) {} //called once on device creation
states: {
'stateA': { //State Id will be namespace.name.stateA
common: {}, //(optional)
native: {}, //(optional)
copy: stateId,
read: {
//(optional) states which should write to "stateA"
'stateId1': {
trigger: {ack: true, change: 'any'} //(optional) see https://github.com/ioBroker/ioBroker.javascript#on---subscribe-on-changes-or-updates-of-some-state
convert: function(val) {}, //(optional) functions should return converted value
before: function(device, value, callback) {}, //(optional) called before writing new state. has to call callback or new value will not be written
delay: 0, //(optional) delay in ms before new value gets written
after: function(device, value) {}, //(optional) called after new value has been written
validFor: //(optional) ms, to ignore old data which did not change for a long time.
},
...
},
readLogic: 'last' || 'max' || 'min' || 'average', //(optional) default: last (only last implemented)
write: {
//(optional) states which "stateA" should write to
'stateId1': {
convert: function(val) {}, //(optional) functions should return converted value
before: function(device, value, callback) {}, //(optional) called before writing new state. has to call callback or new value will not be written
delay: 0, //(optional) delay in ms before new value gets written
after: function(device, value) {}, //(optional) called after new value has been written
},
'stateId3': {
convert: function(val) {}, //(optional) functions should return converted value
before: function(device, value, callback) {}, //(optional) called before writing new state. has to call callback or new value will not be written
delay: 0, //(optional) delay in ms before new value gets written
after: function(device, value) {}, //(optional) called after new value has been written
},
...
},
},
...
}
}
*/
/*
new VirtualDevice({
namespace: 'Hue',
name: 'Wohnzimmer', //das Gerät wird unter javascript.0.virtualDevice.Hue.Wohnzimmer erstellt
states: {
//welche States sollen erstellt werden?
'level': {
//State Konfiguration
common: {type: 'number', def: 0, min: 0, max: 100, read: true, write: true, unit: '%'},
read: {
//von welchen States sollen Werte eingelesen werden?
'hue.0.hue_bridge.Wohnzimmer_Decke.bri': {
convert: function (val) { //wert soll konvertiert werden
return Math.floor(val * 100 / 254);
}
},
},
write: {
//in welche States sollen Werte geschrieben werden?
'hue.0.hue_bridge.Wohnzimmer_Decke.bri': {
convert: function (val) { //wert soll konvertiert werden
return Math.ceil(val * 254 / 100);
},
delay: 1500 // schreibe Werte erst nach 1,5 Sekunden in den Adapter (Puffer)
},
}
},
}
});
new VirtualDevice({
namespace: 'Hue',
name: 'Wohnzimmer', //das Gerät wird unter javascript.0.virtualDevice.Hue.Wohnzimmer erstellt
states: {
//welche States sollen erstellt werden?
'level': {
//State Konfiguration
common: {type: 'number', def: 0, min: 0, max: 100, read: true, write: true, unit: '%'},
read: {
//von welchen States sollen Werte eingelesen werden?
'hue.0.hue_bridge.Wohnzimmer_Decke.bri': {
convert: function (val) { //wert soll konvertiert werden
return Math.floor(val * 100 / 254);
}
},
},
write: {
//in welche States sollen Werte geschrieben werden?
'hue.0.hue_bridge.Wohnzimmer_Decke.bri': {
convert: function (val) { //wert soll konvertiert werden
return Math.ceil(val * 254 / 100);
},
delay: 1500, // schreibe Werte erst nach 1,5 Sekunden in den Adapter (Puffer)
before: function (device, value, callback) {
if (value > 0 && getState('zwave.0.NODE10.SWITCH_BINARY.Switch_1').val === false) {
//if switch is off and value is greater 0 turn on switch and set long delay
setStateDelayed(switchId, true, false, 1500, true, function () {
callback(value, 3500);
});
} else if (value <= 0) {
//if level is set to 0 turn off switch and set level 0
setStateDelayed(switchId, false, false, 1500, true, function () {
callback(0, 0);
});
} else {
callback();
}
}
},
}
},
}
});
*/
interface NumberCommon {
type: 'number';
def: number;
min: number;
max: number;
read: boolean;
write: boolean;
unit: '%' | string;
}
//https://github.com/ioBroker/ioBroker.docs/blob/master/docs/en/dev/objectsschema.md
interface Common {
/**
* (optional - (default is mixed==any type) (possible values: number, string, boolean, array, object, mixed, file). As exception the objects with type meta could have common.type=meta.user or meta.folder
*/
type?: 'number' | 'string' | 'boolean' | 'array' | 'object' | 'mixed' | 'file';
/**
* (optional)
*/
min?: number;
/**
* (optional)
*/
max?: number;
/**
* (optional) - increase/decrease interval. E.g. 0.5 for thermostat
*/
step?: number;
/**
* (optional) - unit of the value E.g. C°
*/
unit?: string;
/**
* (optional - the default value)
*/
def?: any;
/**
* (optional - if common.def is set this value is used as ack flag, js-controller 2.0.0+)
*/
defAck?: any;
/**
* (optional, string or object) - description, object for multilingual description
*/
desc?: string | object;
/**
* (optional, string or object) - name of the device
*/
name?: string;
/**
* (boolean, mandatory) - true if state is readable
*/
read: boolean;
/**
* (boolean, mandatory) - true if state is writable
*/
write: boolean;
/**
* (string, mandatory) - role of the state (used in user interfaces to indicate which widget to choose, see below)
* https://github.com/ioBroker/ioBroker.docs/blob/master/docs/en/dev/stateroles.md
*/
role: 'state' | 'text' | 'button' | string;
/**
* (optional) attribute of type number with object of possible states {'value': 'valueName', 'value2': 'valueName2', 0: 'OFF', 1: 'ON'}
*/
states?: Record<string, string>;
/**
* (string, optional) - if this state has helper state WORKING. Here must be written the full name or just the last part if the first parts are the same with actual. Used for HM.LEVEL and normally has value "WORKING"
*/
workingID?: string;
/**
* (optional) - the structure with custom settings for specific adapters. Like {"influxdb.0": {"enabled": true, "alias": "name"}}. enabled attribute is required and if it is not true, the whole attribute will be deleted.
*/
custom?: any;
}
interface VirtualDeviceConfigStateRead {
trigger?: { ack: boolean; change: "eq" | "ne" | "gt" | "ge" | "lt" | "le" | "any" };
convert?: (val: iobJS.StateValue) => iobJS.StateValue;
before?: (device: VirtualDevice, value: iobJS.StateValue, triggerId: string, callback: (newVal?: iobJS.StateValue, newDelay?: number) => void) => void;
delay?: number;
after?: (device: VirtualDevice, value: iobJS.StateValue, triggerId: string) => void;
//validFor?: number;
}
interface VirtualDeviceConfigStateWrite {
convert?: (val: iobJS.StateValue) => iobJS.StateValue;
before?: (device: VirtualDevice, value: iobJS.StateValue, triggerId: string, callback: (newVal?: iobJS.StateValue, newDelay?: number) => void) => void;
delay?: number;
after?: (device: VirtualDevice, value: iobJS.StateValue, triggerId: string) => void;
}
interface VirtualDeviceConfigState {
common?: Partial<Common>;
native?: Record<string, any>;
copy?: string;
read?: Record<string, VirtualDeviceConfigStateRead>; //stateId
readLogic?: 'last' | 'max' | 'min' | 'average';
write?: Record<string, VirtualDeviceConfigStateWrite>; //stateId
}
interface VirtualDeviceConfig {
namespace: string;
name: string;
common?: Partial<Common>;
native?: Record<string, any>;
copy?: string;
onCreate?: (device, callback) => void;
states: Record<string, VirtualDeviceConfigState>; //stateId will be namespace.name.stateA
}
export declare interface VirtualDevice {
config: VirtualDeviceConfig;
namespace: string;
name: string;
createDevice: (callback: () => void) => void;
createStates: (callback: () => void) => void;
normalizeState: (state: string) => void;
connectState: (state: string) => void;
subRead: (trigger: iobJS.SubscribeOptions, readObj: VirtualDeviceConfigStateRead, state: string) => void;
convertValue: (val: iobJS.StateValue, func?: (value: iobJS.StateValue) => iobJS.StateValue) => iobJS.StateValue;
}
//generic virtual device
function VirtualDevice(this: VirtualDevice, config: VirtualDeviceConfig): void {
//sanity check
if (typeof config !== 'object' || typeof config.namespace !== 'string' || typeof config.name !== 'string' || typeof config.states !== 'object') {
log('sanity check failed, no device created', 'warn');
return;
}
this.config = config;
this.namespace = 'virtualDevice.' + config.namespace + '.' + config.name;
this.name = config.name;
//create virtual device
log('creating virtual device ' + this.namespace)
this.createDevice(function (this: VirtualDevice, ) {
this.createStates(function (this: VirtualDevice, ) {
log('created virtual device ' + this.namespace)
}.bind(this));
}.bind(this));
}
VirtualDevice.prototype.createDevice = function (this: VirtualDevice, callback: () => void): void {
log('creating object for device ' + this.namespace, 'debug');
//create device object
const obj = this.config.copy ? getObject(this.config.copy) : { common: {}, native: {} };
if ((obj.common as any).custom) delete (obj.common as any).custom;
if (typeof this.config.common === 'object') {
obj.common = Object.assign(obj.common, this.config.common);
}
if (typeof this.config.native === 'object') {
obj.native = Object.assign(obj.native, this.config.native);
}
extendObject('javascript.' + instance + '.' + this.namespace, {
type: "device",
common: obj.common,
native: obj.native
}, function (err) {
if (err) {
log(err, 'warn');
log('could not create virtual device: ' + this.namespace, 'warn');
return;
}
log('created object for device ' + this.namespace, 'debug');
if (typeof this.config.onCreate === 'function') {
this.config.onCreate(this, callback);
} else {
callback();
}
}.bind(this));
}
VirtualDevice.prototype.createStates = function (this: VirtualDevice, callback: () => void): void {
"use strict";
log('creating states for device ' + this.namespace, 'debug');
const stateIds = Object.keys(this.config.states);
log('creating states ' + JSON.stringify(stateIds), 'debug');
let countCreated = 0;
for (let i = 0; i < stateIds.length; i++) {
let stateId = stateIds[i];
this.normalizeState(stateId);
const id = this.namespace + '.' + stateId;
log('creating state ' + id, 'debug');
const obj = this.config.states[stateId].copy ? getObject(this.config.states[stateId].copy) : { common: {}, native: {} };
if ((obj.common as any).custom) delete (obj.common as any).custom;
if (typeof this.config.states[stateId].common === 'object') {
obj.common = Object.assign(obj.common, this.config.states[stateId].common);
}
if (typeof this.config.states[stateId].native === 'object') {
obj.native = Object.assign(obj.native, this.config.states[stateId].native);
}
createState(id, obj.common, obj.native, function (err) {
if (err) {
log('skipping creation of state ' + id, 'debug');
} else {
log('created state ' + id, 'debug');
}
this.connectState(stateId);
countCreated++;
if (countCreated >= stateIds.length) {
log('created ' + countCreated + ' states for device ' + this.namespace, 'debug');
callback();
}
}.bind(this));
}
}
VirtualDevice.prototype.normalizeState = function (this: VirtualDevice, state: string): void {
log('normalizing state ' + state, 'debug');
if (typeof this.config.states[state].read !== 'object') {
this.config.states[state].read = {};
}
if (typeof this.config.states[state].write !== 'object') {
this.config.states[state].write = {};
}
const readIds = Object.keys(this.config.states[state].read);
for (let i = 0; i < readIds.length; i++) {
const readId = this.config.states[state].read[readIds[i]];
if (typeof readId.before !== 'function') {
this.config.states[state].read[readIds[i]].before = function (device, value, triggerId, callback) {
callback()
};
}
if (typeof readId.after !== 'function') {
this.config.states[state].read[readIds[i]].after = function (device, value, triggerId) {
};
}
}
const writeIds = Object.keys(this.config.states[state].write);
for (let i = 0; i < writeIds.length; i++) {
const writeId = this.config.states[state].write[writeIds[i]];
if (typeof writeId.before !== 'function') {
this.config.states[state].write[writeIds[i]].before = function (device, value, triggerId, callback) {
callback()
};
}
if (typeof writeId.after !== 'function') {
this.config.states[state].write[writeIds[i]].after = function (device, value, triggerId) {
};
}
}
log('normalized state ' + state, 'debug');
}
VirtualDevice.prototype.connectState = function (this: VirtualDevice, state: string): void {
log('connecting state ' + state, 'debug');
const id = this.namespace + '.' + state;
//subscribe to read ids
const readIds = Object.keys(this.config.states[state].read);
for (let i = 0; i < readIds.length; i++) {
if (getState(readIds[i]).notExist === true) { //check if state exists
log('cannot connect to not existing state: ' + readIds[i], 'warn');
continue;
}
const readObj = this.config.states[state].read[readIds[i]];
const trigger: iobJS.SubscribeOptions = readObj.trigger || {change: 'any'};
trigger.ack = true;
trigger.id = readIds[i];
this.subRead(trigger, readObj, state);
log('connected ' + readIds[i] + ' to ' + id, 'debug');
}
//subscribe to this state and write to write ids
const writeIds = Object.keys(this.config.states[state].write);
const trigger: iobJS.SubscribeOptions = {id: 'javascript.' + instance + '.' + this.namespace + '.' + state, change: 'any', ack: false};
on(trigger, function (this: VirtualDevice, obj: { state: ReturnType<typeof getState> }) {
"use strict";
log('detected change of ' + state, 'debug');
for (let i = 0; i < writeIds.length; i++) {
const writeObj = this.config.states[state].write[writeIds[i]];
let val: iobJS.StateValue = this.convertValue(obj.state.val, writeObj.convert);
const writeId = writeIds[i];
log('executing function before for ' + writeId, 'debug');
writeObj.before(this, val, trigger.id.toString(), function (this: VirtualDevice, newVal?: iobJS.StateValue, newDelay?: number) {
if (newVal !== undefined && newVal !== null) val = newVal;
let delay = writeObj.delay;
if (newDelay !== undefined && newDelay !== null) delay = newDelay;
log(newVal + 'writing value ' + val + ' to ' + writeId + ' with delay ' + delay, 'debug');
setStateDelayed(writeId, val, false, delay || 0, true, function () {
log('executing function after for ' + writeId, 'debug');
writeObj.after(this, val, trigger.id.toString());
}.bind(this));
}.bind(this));
}
}.bind(this));
log('connected ' + state + ' to ' + JSON.stringify(writeIds), 'debug');
}
VirtualDevice.prototype.subRead = function (this: VirtualDevice, trigger: iobJS.SubscribeOptions, readObj: VirtualDeviceConfigStateRead, state: string): void {
const func = function (this: VirtualDevice, obj: { state: ReturnType<typeof getState> }) {
let val: iobJS.StateValue = this.convertValue(obj.state.val, readObj.convert);
//@todo aggregations
log('executing function before for ' + trigger.id.toString(), 'debug');
readObj.before(this, val, trigger.id.toString(), function (this: VirtualDevice, newVal?: iobJS.StateValue, newDelay?: number) {
if (newVal !== undefined && newVal !== null) val = newVal;
if (newDelay !== undefined && newDelay !== null) readObj.delay = newDelay;
log('reading value ' + val + ' to ' + this.namespace + '.' + state, 'debug');
setStateDelayed(this.namespace + '.' + state, val, true, readObj.delay || 0, true, function () {
log('executing function after for ' + trigger.id, 'debug');
readObj.after(this, val, trigger.id.toString());
}.bind(this));
}.bind(this));
}.bind(this);
func({ state: getState(trigger.id.toString()) });
on(trigger, func);
}
VirtualDevice.prototype.convertValue = function (this: VirtualDevice, val: iobJS.StateValue, func: (value: iobJS.StateValue) => iobJS.StateValue): iobJS.StateValue {
if (typeof func !== 'function') {
return val;
}
return func(val);
}
//global
function createVirtualDevice(config: VirtualDeviceConfig): VirtualDevice {
return new VirtualDevice(config)
}
const createVirtualThermostat = (name: string, id: string, isGroup?: boolean) => createVirtualDevice({
namespace: 'thermostat',
name: name,
common: { name: id },
states: {
name: {
common: { type: 'string', role: 'text', read: true, write: false },
read: {
[id + '.name']: {},
},
},
manufacturer: {
common: { type: 'string', role: 'text', read: true, write: false },
read: {
[id + '.manufacturer']: {},
},
},
id: {
common: { type: 'string', role: 'text', read: true, write: false },
read: {
[id + '.id']: {},
},
},
...(!isGroup
? {
productname: {
common: { type: 'string', role: 'text', read: true, write: false },
read: {
[id + '.productname']: {},
},
},
battery: {
common: { type: 'number', role: 'state', read: true, write: false },
read: {
[id + '.battery']: {},
},
},
batterylow: {
common: { type: 'boolean', role: 'state', read: true, write: false },
read: {
[id + '.batterylow']: {},
},
},
batterylow_homekit: {
common: { type: 'number', role: 'state', read: true, write: false },
read: {
[id + '.batterylow']: {
convert: d => d ? 1 : 0,
},
},
},
actualtemp: {
common: { type: 'number', role: 'state', read: true, write: false },
read: {
[id + '.tist']: {},
},
}
}
: {}
),
targettemp: {
common: { type: 'number', role: 'state', read: true, write: true },
read: {
[id + '.tsoll']: {},
},
write: {
[id + '.tsoll']: {},
},
},
windowopenactiv: {
common: { type: 'boolean', role: 'state', read: true, write: false },
read: {
[id + '.windowopenactiv']: {},
},
},
errorcode: {
common: { type: 'number', role: 'state', read: true, write: false },
read: {
[id + '.errorcode']: {},
},
},
error: {
common: { type: 'boolean', role: 'state', read: true, write: false },
read: {
[id + '.errorcode']: {
convert: d => !!d,
},
},
},
present: {
common: { type: 'boolean', role: 'state', read: true, write: false },
read: {
[id + '.present']: {},
},
},
operationmode: {
common: { type: 'string', role: 'state', read: true, write: false },
read: {
[id + '.operationmode']: {},
},
},
hkrmode: {
common: { type: 'number', role: 'state', read: true, write: true },
read: {
[id + '.hkrmode']: {},
},
write: {
[id + '.hkrmode']: {},
},
},
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment