Last active
August 23, 2021 16:16
ioBroker VirtualDevice port from https://forum.iobroker.net/topic/7751/virtual-devices/2 to TypeScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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) | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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