From 8954b88a00259979008a5efff3555880647282f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20=C3=85hs?= Date: Sat, 6 Jan 2018 15:11:37 +0100 Subject: [PATCH] initial commit --- .gitignore | 3 + README.md | 0 dist/node-tradfri.html | 183 +++++++++++++++++++++++ dist/node-tradfri.js | 313 +++++++++++++++++++++++++++++++++++++++ gulpfile.js | 38 +++++ package.json | 34 +++++ src/node-tradfri.html | 183 +++++++++++++++++++++++ src/node-tradfri.ts | 327 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1081 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 dist/node-tradfri.html create mode 100644 dist/node-tradfri.js create mode 100644 gulpfile.js create mode 100644 package.json create mode 100755 src/node-tradfri.html create mode 100644 src/node-tradfri.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b736b4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +build +package-lock.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/dist/node-tradfri.html b/dist/node-tradfri.html new file mode 100755 index 0000000..86fb560 --- /dev/null +++ b/dist/node-tradfri.html @@ -0,0 +1,183 @@ + + + + + + + + + + + diff --git a/dist/node-tradfri.js b/dist/node-tradfri.js new file mode 100644 index 0000000..e8b13c2 --- /dev/null +++ b/dist/node-tradfri.js @@ -0,0 +1,313 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const tradfri = require("node-tradfri-client"); +module.exports = function (RED) { + function lightFromAccessory(accessory) { + return Object.assign({}, { + id: accessory.instanceId, + name: accessory.name, + model: accessory.deviceInfo.modelNumber, + firmware: accessory.deviceInfo.firmwareVersion, + alive: accessory.alive, + on: accessory.lightList[0].onOff, + onTime: accessory.lightList[0].onTime, + brightness: accessory.lightList[0].dimmer, + colorTemperature: accessory.lightList[0].colorTemperature, + color: accessory.lightList[0].color, + hue: accessory.lightList[0].hue, + saturation: accessory.lightList[0].saturation, + colorX: accessory.lightList[0].colorX, + colorY: accessory.lightList[0].colorY, + transition: accessory.lightList[0].transitionTime, + created: accessory.createdAt, + seen: accessory.lastSeen, + type: accessory.type, + power: accessory.deviceInfo.power + }); + } + function toLightOperation(op) { + let props = { on: "onOff", brightness: "dimmer", transition: "transitionTime" }; + for (let k in props) { + if (op.hasOwnProperty(k)) { + op[props[k]] = op[k]; + delete op[k]; + } + } + return op; + } + RED.httpAdmin.get("/tradfri/lights", RED.auth.needsPermission('tradfri.read'), function (req, res) { + let nodeId = req.query.nodeId; + if (nodeId != null) { + let node = RED.nodes.getNode(nodeId); + let lights = node.getLights(); + let ret = []; + for (let k in lights) { + ret.push({ name: lights[k].name, id: k }); + } + res.json(JSON.stringify(ret)); + } + }); + function TradfriConnectionNode(config) { + var node = this; + RED.nodes.createNode(node, config); + node.name = config.name; + node.address = config.address; + node.securityCode = config.securityCode; + node.identity = config.identity; + node.psk = config.psk; + if ((node.identity == null && node.psk != null) || (node.identity != null && node.psk == null)) { + RED.log.error("Must provide both identity and PSK or leave both blank to generate new credentials from security code."); + } + if (node.identity == null && node.psk == null && node.securityCode == null) { + RED.log.error("Must provide either identity and PSK or a security code to connect to the Tradfri hub"); + } + var _lights = {}; + var _listeners = {}; + var _client = null; + var _deviceUpdatedCallback = (accessory) => { + if (accessory.type === tradfri.AccessoryTypes.lightbulb) { + _lights[accessory.instanceId] = accessory; + } + if (_listeners[accessory.instanceId]) { + for (let nodeId in _listeners[accessory.instanceId]) { + _listeners[accessory.instanceId][nodeId](accessory); + } + } + }; + let _setupClient = () => __awaiter(this, void 0, void 0, function* () { + let loggerFunction = (message, severity) => { + RED.log.info(severity + ", " + message); + }; + let client = new tradfri.TradfriClient(node.address); + if (node.identity == null && node.psk == null) { + const { identity, psk } = yield client.authenticate(node.securityCode); + node.identity = identity; + node.psk = psk; + } + if (yield client.connect(node.identity, node.psk)) { + client.on("device updated", _deviceUpdatedCallback); + client.observeDevices(); + _client = client; + } + else { + throw new Error(`Client not available`); + } + }); + let _reconnect = () => __awaiter(this, void 0, void 0, function* () { + let timeout = 5000; + if (_client != null) { + _client.destroy(); + _client = null; + } + while (_client == null) { + try { + yield _setupClient(); + } + catch (e) { + RED.log.trace(`[Tradfri: ${node.id}] ${e.toString()}, reconnecting...`); + } + yield new Promise(resolve => setTimeout(resolve, timeout)); + } + }); + let pingInterval = 30; + let _ping = setInterval(() => __awaiter(this, void 0, void 0, function* () { + try { + let client = yield node.getClient(); + let res = yield client.ping(); + RED.log.trace(`[Tradfri: ${node.id}] ping returned '${res}'`); + } + catch (e) { + RED.log.trace(`[Tradfri: ${node.id}] ping returned '${e.toString()}'`); + } + }), pingInterval * 1000); + _reconnect(); + node.getClient = () => __awaiter(this, void 0, void 0, function* () { + let maxRetries = 5; + let timeout = 2; + for (let i = 0; i < maxRetries; i++) { + if (_client == null) { + yield new Promise(resolve => setTimeout(resolve, timeout * 1000)); + } + else { + return _client; + } + } + throw new Error('Client not available'); + }); + node.getLight = (instanceId) => __awaiter(this, void 0, void 0, function* () { + let maxRetries = 5; + let timeout = 2; + for (let i = 0; i < maxRetries; i++) { + if (_lights[instanceId] == null) { + yield new Promise(resolve => setTimeout(resolve, timeout * 1000)); + } + else { + return _lights[instanceId]; + } + } + throw new Error('Light not available'); + }); + node.getLights = () => { + return _lights; + }; + node.register = (nodeId, instanceId, callback) => { + if (!_listeners[instanceId]) { + _listeners[instanceId] = {}; + } + _listeners[instanceId][nodeId] = callback; + RED.log.info(`[Tradfri: ${nodeId}] registered event listener for ${instanceId}`); + }; + node.unregister = (nodeId) => { + for (let instanceId in _listeners) { + if (_listeners[instanceId].hasOwnProperty(nodeId)) { + delete _listeners[instanceId][nodeId]; + RED.log.debug(`[Tradfri: ${nodeId}] unregistered event listeners`); + } + } + }; + node.on('close', () => { + clearInterval(_ping); + _client.destroy(); + RED.log.debug(`[Tradfri: ${node.id}] Config was closed`); + }); + } + RED.nodes.registerType("tradfri-connection", TradfriConnectionNode); + function TradfriNode(config) { + var node = this; + RED.nodes.createNode(node, config); + node.name = config.name; + node.deviceId = config.deviceId; + node.deviceName = config.deviceName; + node.observe = config.observe; + var _config = RED.nodes.getNode(config.connection); + var _prev = {}; + var _getPayload = (accessory) => { + let light = lightFromAccessory(accessory); + light['prev'] = Object.assign({}, _prev); + return light; + }; + var _deviceUpdated = (accessory) => { + let ret = _getPayload(accessory); + _prev = lightFromAccessory(accessory); + node.send({ payload: { light: ret } }); + RED.log.trace(`[Tradfri: ${node.id}] recieved update for '${accessory.name}' (${accessory.instanceId})`); + }; + var _getTargetId = (msg) => { + let payload = msg.payload; + if (payload.hasOwnProperty('id') && Array.isArray(payload.id)) { + return payload.id; + } + else if (payload.hasOwnProperty('id')) { + return [payload.id]; + } + else if (node.deviceId > 0) { + return [node.deviceId]; + } + else { + throw new Error('No valid target device'); + } + }; + var _handleDirectStatus = (msg) => __awaiter(this, void 0, void 0, function* () { + try { + let client = yield _config.getClient(); + let res = yield client.request('15001/' + node.deviceId, 'get'); + msg.payload = res; + node.send(msg); + } + catch (e) { + msg.payload = e; + node.send(msg); + } + }); + var _handleStatus = (msg) => __awaiter(this, void 0, void 0, function* () { + try { + let accessory = yield _config.getLight(node.deviceId); + msg.payload.light = _getPayload(accessory); + delete msg.payload.status; + node.send(msg); + RED.log.trace(`[Tradfri: ${node.id}] Status request successful`); + } + catch (e) { + RED.log.info(`[Tradfri: ${node.id}] Status request unsuccessful, '${e.toString()}'`); + } + }); + var _handleDirectLightOp = (light) => __awaiter(this, void 0, void 0, function* () { + let clamp = (num, min, max) => { + return num <= min ? min : num >= max ? max : num; + }; + let cmd = { 3311: [Object.assign({}, { + 5851: clamp(light.brightness, 0, 254), + 5711: clamp(light.colorTemperature, 200, 454), + 5712: light.transition, + 5850: light.on, + 5707: light.hue, + 5708: light.saturation, + 5706: light.color // f5faf6, f1e0b5, efd275 are valid for WS bulbs + })] }; + try { + let client = yield _config.getClient(); + let res = yield client.request('15001/' + node.deviceId, 'put', Object.assign({}, cmd)); + RED.log.trace(`[Tradfri: ${node.id}] DirectLightOp '${JSON.stringify(cmd)}' returned '${res}'`); + } + catch (e) { + RED.log.info(`[Tradfri: ${node.id}] DirectLightOp '${JSON.stringify(cmd)}' unsuccessful, '${e.toString()}'`); + } + }); + var _handleLightOp = (light) => __awaiter(this, void 0, void 0, function* () { + let lightOp = toLightOperation(light); + if (!lightOp.hasOwnProperty('transitionTime')) { + lightOp['transitionTime'] = 0; + } + try { + let light = yield _config.getLight(node.deviceId); + if (Object.keys(lightOp).length > 0) { + let client = yield _config.getClient(); + let res = yield client.operateLight(light, lightOp); + RED.log.trace(`[Tradfri: ${node.id}] LightOp '${JSON.stringify(lightOp)}' returned '${res}'`); + } + } + catch (e) { + RED.log.info(`[Tradfri: ${node.id}] LightOp '${JSON.stringify(lightOp)}' unsuccessful, '${e.toString()}'`); + } + }); + if (node.observe) { + _config.register(node.id, node.deviceId, _deviceUpdated); + } + node.on('input', function (msg) { + (() => __awaiter(this, void 0, void 0, function* () { + if (msg.hasOwnProperty('payload')) { + if (msg.payload === "status") { + msg.payload = { status: true }; + } + let isDirect = msg.payload.hasOwnProperty('direct'); + let isStatus = msg.payload.hasOwnProperty('status'); + if (isDirect && isStatus) { + _handleDirectStatus(msg); + } + else if (isStatus) { + _handleStatus(msg); + } + else if (isDirect) { + _handleDirectLightOp(msg.payload); + } + else { + _handleLightOp(msg.payload); + } + } + }))(); + }); + node.on('close', function () { + RED.log.debug(`[Tradfri: ${node.id}] Node was closed`); + _config.unregister(node.id); + }); + } + RED.nodes.registerType("tradfri", TradfriNode); +}; diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..b12b481 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,38 @@ +var gulp = require('gulp'); +var del = require('del'); +var shell = require('gulp-shell'); +var ts = require('gulp-typescript'); + +gulp.task('clean:build', () => { + return del(['./build/**']); +}); + +gulp.task('compile', ['clean:build'], () => { + let numErrors = 0; + let res = gulp.src('src/**/*.ts') + .pipe( + ts({ + module: 'commonjs', + target: 'es2015', + declaration: true + }) + ) + .on('error', () => { + numErrors += 1; + }) + return res.pipe(gulp.dest('build')) +}); + +gulp.task('install', ['compile'], () => { + return gulp.src(['src/*.html', 'build/*.js']) + .pipe(gulp.dest('dist')); +}); + +gulp.task('exec', ['install'], shell.task('docker restart node-red')); +gulp.task('watch', function() { + return gulp.watch(['src/*.html', 'src/*.ts'], ['exec']); +}); + +gulp.task('default', ['install']); + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..87040d7 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "node-red-contrib-node-tradfri", + "version": "0.1.0", + "description": "Node-RED node to utilize IKEA Trådfri devices. Fully implemented in Node.js.", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "github.com/freahs/node-red-contrib-node-tradfri" + }, + "keywords": [ + "node-red", + "tradfri" + ], + "author": "freahs", + "license": "GPL-3.0", + "devDependencies": { + "@types/node": "^8.5.5", + "del": "^3.0.0", + "gulp": "^3.9.1", + "gulp-shell": "^0.6.5", + "gulp-typescript": "^3.2.3", + "typescript": "^2.6.2" + }, + "dependencies": { + "node-tradfri-client": "^0.5.5" + }, + "node-red": { + "nodes": { + "tradfri": "dist/node-tradfri.js" + } + } +} diff --git a/src/node-tradfri.html b/src/node-tradfri.html new file mode 100755 index 0000000..86fb560 --- /dev/null +++ b/src/node-tradfri.html @@ -0,0 +1,183 @@ + + + + + + + + + + + diff --git a/src/node-tradfri.ts b/src/node-tradfri.ts new file mode 100644 index 0000000..a2c2254 --- /dev/null +++ b/src/node-tradfri.ts @@ -0,0 +1,327 @@ +import * as tradfri from "node-tradfri-client"; + +module.exports = function(RED) { + + function lightFromAccessory(accessory: tradfri.Accessory) { + return Object.assign({}, { + id: accessory.instanceId, + name: accessory.name, + model: accessory.deviceInfo.modelNumber, + firmware: accessory.deviceInfo.firmwareVersion, + alive: accessory.alive, + on: accessory.lightList[0].onOff, + onTime: accessory.lightList[0].onTime, + brightness: accessory.lightList[0].dimmer, + colorTemperature: accessory.lightList[0].colorTemperature, + color: accessory.lightList[0].color, + hue: accessory.lightList[0].hue, + saturation: accessory.lightList[0].saturation, + colorX: accessory.lightList[0].colorX, + colorY: accessory.lightList[0].colorY, + transition: accessory.lightList[0].transitionTime, + created: accessory.createdAt, + seen: accessory.lastSeen, + type: accessory.type, + power: accessory.deviceInfo.power + }); + } + + function toLightOperation(op: any): tradfri.LightOperation { + let props = {on:"onOff", brightness:"dimmer", transition:"transitionTime"}; + for (let k in props) { + if (op.hasOwnProperty(k)) { + op[props[k]] = op[k]; + delete op[k]; + } + } + return op; + } + + RED.httpAdmin.get("/tradfri/lights", RED.auth.needsPermission('tradfri.read'), function(req,res) { + let nodeId = req.query.nodeId; + if (nodeId != null) { + let node = RED.nodes.getNode(nodeId); + let lights = node.getLights(); + let ret = []; + for (let k in lights) { + ret.push({name: lights[k].name, id: k}); + } + res.json(JSON.stringify(ret)); + } + }); + + function TradfriConnectionNode(config) { + var node = this; + RED.nodes.createNode(node, config); + node.name = config.name; + node.address = config.address; + node.securityCode = config.securityCode; + node.identity = config.identity; + node.psk = config.psk; + + if ((node.identity == null && node.psk != null) || (node.identity != null && node.psk == null)) { + RED.log.error("Must provide both identity and PSK or leave both blank to generate new credentials from security code."); + } + if (node.identity == null && node.psk == null && node.securityCode == null) { + RED.log.error("Must provide either identity and PSK or a security code to connect to the Tradfri hub"); + } + + var _lights = {}; + var _listeners = {}; + var _client = null; + + var _deviceUpdatedCallback = (accessory: tradfri.Accessory) => { + if (accessory.type === tradfri.AccessoryTypes.lightbulb) { + _lights[accessory.instanceId] = accessory; + } + if (_listeners[accessory.instanceId]) { + for (let nodeId in _listeners[accessory.instanceId]) { + _listeners[accessory.instanceId][nodeId](accessory); + } + } + } + + let _setupClient = async () => { + let loggerFunction = (message: string, severity: string) => { + RED.log.info(severity + ", " + message); + } + let client = new tradfri.TradfriClient(node.address); + if (node.identity == null && node.psk == null) { + const {identity, psk} = await client.authenticate(node.securityCode); + node.identity = identity; + node.psk = psk; + } + if (await client.connect(node.identity, node.psk)) { + client.on("device updated", _deviceUpdatedCallback); + client.observeDevices(); + _client = client; + } else { + throw new Error(`Client not available`); + } + } + + let _reconnect = async () => { + let timeout = 5000; + if (_client != null) { + _client.destroy(); + _client = null; + } + while (_client == null) { + try { + await _setupClient(); + } catch (e) { + RED.log.trace(`[Tradfri: ${node.id}] ${e.toString()}, reconnecting...`); + } + await new Promise(resolve => setTimeout(resolve, timeout)); + } + } + + let pingInterval = 30; + let _ping = setInterval(async () => { + try { + let client = await node.getClient(); + let res = await client.ping(); + RED.log.trace(`[Tradfri: ${node.id}] ping returned '${res}'`); + }catch (e) { + RED.log.trace(`[Tradfri: ${node.id}] ping returned '${e.toString()}'`); + } + }, pingInterval*1000); + + _reconnect(); + + node.getClient = async (): Promise => { + let maxRetries = 5; + let timeout = 2; + for (let i = 0; i < maxRetries; i++) { + if (_client == null) { + await new Promise(resolve => setTimeout(resolve, timeout*1000)); + } else { + return _client; + } + } + throw new Error ('Client not available'); + } + + node.getLight = async (instanceId: number): Promise => { + let maxRetries = 5; + let timeout = 2; + for (let i = 0; i < maxRetries; i++) { + if (_lights[instanceId] == null) { + await new Promise(resolve => setTimeout(resolve, timeout*1000)); + } else { + return _lights[instanceId]; + } + } + throw new Error ('Light not available'); + } + + node.getLights = () => { + return _lights; + } + + node.register = (nodeId: string, instanceId: number, callback: (accessory: tradfri.Accessory) => void): void => { + if (!_listeners[instanceId]) { + _listeners[instanceId] = {}; + } + _listeners[instanceId][nodeId] = callback; + RED.log.info(`[Tradfri: ${nodeId}] registered event listener for ${instanceId}`); + } + + node.unregister = (nodeId: string): void => { + for (let instanceId in _listeners) { + if (_listeners[instanceId].hasOwnProperty(nodeId)) { + delete _listeners[instanceId][nodeId]; + RED.log.debug(`[Tradfri: ${nodeId}] unregistered event listeners`); + } + } + } + + node.on('close', () => { + clearInterval(_ping); + _client.destroy(); + RED.log.debug(`[Tradfri: ${node.id}] Config was closed`); + }); + + } + + RED.nodes.registerType("tradfri-connection", TradfriConnectionNode); + + function TradfriNode(config) { + var node = this; + RED.nodes.createNode(node, config); + node.name = config.name; + node.deviceId = config.deviceId; + node.deviceName = config.deviceName; + node.observe = config.observe; + var _config = RED.nodes.getNode(config.connection); + var _prev = {}; + + var _getPayload = (accessory: tradfri.Accessory) => { + let light = lightFromAccessory(accessory); + light['prev'] = Object.assign({}, _prev); + return light; + } + + var _deviceUpdated = (accessory: tradfri.Accessory) => { + let ret = _getPayload(accessory); + _prev = lightFromAccessory(accessory); + node.send({payload: {light: ret}}); + RED.log.trace(`[Tradfri: ${node.id}] recieved update for '${accessory.name}' (${accessory.instanceId})`); + } + + var _getTargetId = (msg: any): number[] => { + let payload = msg.payload; + if (payload.hasOwnProperty('id') && Array.isArray(payload.id)) { + return payload.id; + } else if (payload.hasOwnProperty('id')) { + return [payload.id]; + } else if (node.deviceId > 0) { + return [node.deviceId] + } else { + throw new Error('No valid target device'); + } + } + + var _handleDirectStatus = async (msg: any) => { + try { + let client = await _config.getClient(); + let res = await client.request('15001/' + node.deviceId, 'get'); + msg.payload = res; + node.send(msg); + } catch (e) { + msg.payload = e; + node.send(msg); + } + } + + var _handleStatus = async (msg: any) => { + try { + let accessory = await _config.getLight(node.deviceId); + msg.payload.light = _getPayload(accessory); + delete msg.payload.status; + node.send(msg); + RED.log.trace(`[Tradfri: ${node.id}] Status request successful`); + } catch (e) { + RED.log.info(`[Tradfri: ${node.id}] Status request unsuccessful, '${e.toString()}'`); + } + } + + var _handleDirectLightOp = async (light: any) => { + let clamp = (num: number, min: number, max: number): number => { + return num <= min ? min : num >= max ? max : num; + } + + let cmd = {3311:[Object.assign({}, { + 5851: clamp(light.brightness, 0, 254), + 5711: clamp(light.colorTemperature, 200, 454), // only for WS bulbs + 5712: light.transition, + 5850: light.on, + 5707: light.hue, // only for CWS bulbs + 5708: light.saturation, // only for CWS bulbs + 5706: light.color // f5faf6, f1e0b5, efd275 are valid for WS bulbs + })]}; + + try { + let client = await _config.getClient(); + let res = await client.request('15001/' + node.deviceId, 'put', Object.assign({}, cmd)); + RED.log.trace(`[Tradfri: ${node.id}] DirectLightOp '${JSON.stringify(cmd)}' returned '${res}'`); + } catch (e) { + RED.log.info(`[Tradfri: ${node.id}] DirectLightOp '${JSON.stringify(cmd)}' unsuccessful, '${e.toString()}'`); + } + } + + + var _handleLightOp = async (light: any) => { + let lightOp = toLightOperation(light); + if (!lightOp.hasOwnProperty('transitionTime')) { + lightOp['transitionTime'] = 0; + } + try { + let light = await _config.getLight(node.deviceId); + if (Object.keys(lightOp).length > 0) { + let client = await _config.getClient(); + let res = await client.operateLight(light, lightOp); + RED.log.trace(`[Tradfri: ${node.id}] LightOp '${JSON.stringify(lightOp)}' returned '${res}'`); + } + } catch (e) { + RED.log.info(`[Tradfri: ${node.id}] LightOp '${JSON.stringify(lightOp)}' unsuccessful, '${e.toString()}'`); + } + } + + + if (node.observe) { + _config.register(node.id, node.deviceId, _deviceUpdated); + } + + node.on('input', function(msg) { + (async() => { + if (msg.hasOwnProperty('payload')) { + if (msg.payload === "status") { + msg.payload = {status:true}; + } + + let isDirect = msg.payload.hasOwnProperty('direct'); + let isStatus = msg.payload.hasOwnProperty('status'); + + if (isDirect && isStatus) { + _handleDirectStatus(msg); + } else if (isStatus) { + _handleStatus(msg); + } else if (isDirect) { + _handleDirectLightOp(msg.payload); + } else { + _handleLightOp(msg.payload); + } + } + })(); + }); + + node.on('close', function() { + RED.log.debug(`[Tradfri: ${node.id}] Node was closed`); + _config.unregister(node.id); + }); + + } + + RED.nodes.registerType("tradfri",TradfriNode); +}