node-red-contrib-node-tradfri/src/node-tradfri.ts

328 lines
12 KiB
TypeScript

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<tradfri.TradfriClient> => {
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<tradfri.Accessory> => {
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);
}