diff --git a/.gitignore b/.gitignore index 40b878d..c2658d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -node_modules/ \ No newline at end of file +node_modules/ diff --git a/README.md b/README.md index a058084..93c9993 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,22 @@ -maxcube -======= +# MaxCube2 [![NPM Version](https://img.shields.io/npm/v/maxcube2.svg)](https://www.npmjs.com/package/maxcube2) -eQ-3 Max! Cube interface library. +eQ-3 Max! Cube interface library for homebridge-platform-maxcube -For a cli, see [maxcube-cli](https://github.com/ivesdebruycker/maxcube-cli). If you want to integrate your MAX! Cube in node-red, use [node-red-node-maxcube](https://github.com/ivesdebruycker/node-red-node-maxcube). +This is a fork of the work first started by https://github.com/ivesdebruycker/maxcube +## Introduction +### History +Why this library is called maxcube_2_? Because the maxcube project seemed to be dead without response to issues or PRs for over half a year and I needed it fixed for my homebridge plugin. So I finally decided to continue its legacy as "maxcube2" but then it suddenly got revived. Now I won't change the name of this library anymore and keep this fork for the homebridge-platform-maxcube project - still as a proper merge-able fork of maxcube however. + +### Changes from maxcube +- More events (error, device_list etc.) +- Getting device configurations (min/max/eco/comfort temperatures etc.) + +The old API didn't change currently so it's a drop-in replacement. ## Example ``` -var MaxCube = require('maxcube'); +var MaxCube = require('maxcube2'); var myMaxCube = new MaxCube('192.168.1.123', 62910); myMaxCube.on('connected', function () { @@ -28,6 +36,11 @@ myMaxCube.on('closed', function () { ## Events * connected * closed +* error +* hello (arg = hello object) +* meta_data (arg = meta data object) +* device_list (arg = list of devices) +* configuration (arg = configuration object for a single device) ## API ### getConnection() @@ -77,6 +90,10 @@ myMaxCube.setTemperature('0dd6b5', 18).then(function (success) { }); ``` -## Related projects -* [maxcube-cli](https://github.com/ivesdebruycker/maxcube-cli): a command-line interface for eQ-3 Max! Cube -* [node-red-node-maxcube](https://github.com/ivesdebruycker/node-red-node-maxcube): a node for interfacing the eQ-3 Max! Cube using [node-red](https://github.com/node-red/node-red) +### setSchedule(rf_address, room_id, weekday, temperaturesArray, timesArray) +Set a schedule for a device. + +- weekday: 0=mo,1=tu,..,6=su +- temperaturesArray: [19.5,21,..] degrees Celsius (max 7) +- timesArray: ['HH:mm',..] 24h format (max 7, same amount as temperatures) +- the first time will be the time (from 00:00 to timesArray[0]) that the first temperature is active. Last possibe time of the day: 00:00 diff --git a/maxcube-commandfactory.js b/maxcube-commandfactory.js index 794a033..d208208 100644 --- a/maxcube-commandfactory.js +++ b/maxcube-commandfactory.js @@ -45,7 +45,6 @@ function generateSetTemperatureCommand (rfAdress, room_id, mode, temperature, un // '00' sets all temperature for all devices var room_id_padded = padLeft(room_id, 2); var hexString = '000440000000' + rfAdress + room_id_padded + reqTempHex + date_until + time_until; - console.log(hexString); var payload = new Buffer(hexString, 'hex').toString('base64'); var data = 's:' + payload + '\r\n'; @@ -146,11 +145,19 @@ function generateSetDayProgramCommand (rfAdress, room_id, weekday, temperaturesA return data; } +function generateResetCommand (rfAdress) { + var hexString = rfAdress + var payload = new Buffer(hexString, 'hex').toString('base64'); + var data = 'r:01,' + payload + '\r\n'; + return data; +} + function padLeft(data, totalLength){ return Array(totalLength - String(data).length + 1).join('0') + data; } module.exports = { generateSetTemperatureCommand: generateSetTemperatureCommand, - generateSetDayProgramCommand: generateSetDayProgramCommand + generateSetDayProgramCommand: generateSetDayProgramCommand, + generateResetCommand:generateResetCommand }; diff --git a/maxcube-commandparser.js b/maxcube-commandparser.js index 42a70b2..ee5fc6c 100644 --- a/maxcube-commandparser.js +++ b/maxcube-commandparser.js @@ -63,7 +63,7 @@ function parseCommandHello (payload) { function parseCommandMetadata (payload) { var payloadArr = payload.split(","); - var decodedPayload = new Buffer(payloadArr[2], 'base64'); + var decodedPayload = Buffer.from(payloadArr[2], 'base64'); var room_count = decodedPayload[2]; var currentIndex = 3; @@ -144,8 +144,47 @@ function parseCommandConfiguration (payload) { The five least significant bits (LSB) are presenting the time (in hours) 27 1 FF Maximum Valve setting; *(100/255) to get in % 1C 1 00 Valve Offset ; *(100/255) to get in % - 1D ? 44 48 ... Weekly program (see The weekly program) - */ + 1d 182 Weekly Program Schedule of 26 bytes for + each day starting with + Saturday. Each schedule + consists of 13 words + (2 bytes) e.g. set points. + 1 set point consist of + 7 MSB bits is temperature + set point (in degrees * 2) + 9 LSB bits is until time + (in minutes * 5) + + +*/ + var weekly_program_days = ['saturday', 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday' ]; + var parseDayProgram = function(dayPayload){ + var temperaturesArray = []; + var timesArray = []; + var debug = []; + for (var i = 1; i <= 13; i++) { + var length = 2; + var offset = i*2; + // Weekly program 41 20 0100000 100100000 -> 16 degrees, until 24:00 + var msb = dayPayload[offset]>>1; + var lsb = (dayPayload[offset]&1)<<8; + + var setpoint = msb/2; + var minutes = (lsb+dayPayload[offset+1])*5; + var time = Math.floor(minutes / 60)+':'+(minutes%60==0?'00':(minutes%60<10?'0'+minutes%60:minutes%60)); + + //if a day has less than 13 setpoints, last one repeats until we reach 13 + if(setpoint > 0 && time !==undefined && setpoint !== temperaturesArray[temperaturesArray.length-1] && time !== timesArray[timesArray.length-1]){ + temperaturesArray.push(setpoint); + timesArray.push(time); + } + } + + return { + temperaturesArray: temperaturesArray, + timesArray: timesArray + }; + } var payloadArr = payload.split(","); var rf_address = payloadArr[0].slice(0, 6).toString('hex'); @@ -165,6 +204,24 @@ function parseCommandConfiguration (payload) { max_valve: decodedPayload[27] * (100/255) }; + try { + if(dataObj.device_type !== 5){ + dataObj.weekly_program = {}; + var length = 26; + var offset = 27; + for (var i = 0; i < 7; i++) { + const bf = Buffer.alloc(length); + var end = offset + length; + decodedPayload.copy(bf, 0, offset, offset+length); + dataObj.weekly_program[weekly_program_days[i]] = parseDayProgram(bf); + offset = end; + } + + } + } catch (e) { + console.log("Error getting weekly program for device "+dataObj.rf_address, dataObj); + } + return dataObj; } @@ -310,41 +367,34 @@ function decodeDeviceThermostat (payload) { function decodeDeviceWallThermostat (payload) { - //regular device parsing + //regular device parsing, only temp is in a different location var deviceStatus = decodeDeviceThermostat (payload); + deviceStatus.temp = (payload[11]?25.5:0) + payload[12] / 10; + + //alternative parsing if setpoint is 60-80°C + if(payload[8]>=128){ + //wall thermostat has different temp and setpoint parsing: + //https://github.com/Bouni/max-cube-protocol/blob/master/L-Message.md#actual-temperature-wallmountedthermostat + + /* + Actual Temperature (WallMountedThermostat) + + 11 Actual Temperature 1 219 + Room temperature measured by the wall mounted thermostat in °C * 10. For example 0xDB = 219 = 21.9°C The temperature is represented by 9 bits; the 9th bit is available as the top bit at offset 8 + + offset| 8 | ... | 12 | + hex | B2 | | 24 | + binary| 1011 0010 | ... | 0010 0100 | + | || |||| |||| |||| + | ++-++++--------------------- temperature (°C*2): 110010 = 25.0°C + | |||| |||| + +-----------------++++-++++--- actual temperature (°C*10): 100100100 = 29.2°C + */ - //wall thermostat has different temp and setpoint parsing: - //https://github.com/Bouni/max-cube-protocol/blob/master/L-Message.md#actual-temperature-wallmountedthermostat - - /* - Actual Temperature (WallMountedThermostat) - - 11 Actual Temperature 1 219 - Room temperature measured by the wall mounted thermostat in °C * 10. For example 0xDB = 219 = 21.9°C The temperature is represented by 9 bits; the 9th bit is available as the top bit at offset 8 - - offset| 8 | ... | 12 | - hex | B2 | | 24 | - binary| 1011 0010 | ... | 0010 0100 | - | || |||| |||| |||| - | ++-++++--------------------- temperature (°C*2): 110010 = 25.0°C - | |||| |||| - +-----------------++++-++++--- actual temperature (°C*10): 100100100 = 29.2°C - - */ - - //offset 8 binary to extract only needed bit - var off8Bin= (payload[8] >>> 0).toString(2); - - //offset8 without top bit (it is used by actual temperature and will corrupt the setpoint value) - var setPoint = parseInt(((off8Bin + '').substring(1)).replace(/[^01]/gi, ''), 2); - //C/2 - deviceStatus.setpoint = setPoint / 2; - - //get the TopBit and zero fill right to use it as 9 bit of offset 12 - var off8TopBit = parseInt(off8Bin.substring(0,1)) << 8; - //Bitwise OR offset 8/offset 12 and finally C/10 to read the actual temperature - deviceStatus.temp = (parseInt(off8TopBit) | parseInt(payload[12])) / 10; - + //removing first and second bit from offset 8 00111111 & 10110010 = 00110010 + deviceStatus.setpoint = (63 & payload[8]) / 2; + deviceStatus.temp = (payload[8]>=128 ? 25.5 : 0) + payload[12] / 10; + } return deviceStatus; } diff --git a/maxcube-lowlevel.js b/maxcube-lowlevel.js index ea42475..aaf9a11 100644 --- a/maxcube-lowlevel.js +++ b/maxcube-lowlevel.js @@ -11,7 +11,7 @@ function MaxCubeLowLevel(ip, port){ this.isConnected = false; process.on('uncaughtException', (err) => { - //console.error('Uncaught exception error'); + console.error('Uncaught exception error: ', err); }); initSocket.call(this); @@ -91,4 +91,4 @@ MaxCubeLowLevel.prototype.close = close; MaxCubeLowLevel.prototype.send = send; MaxCubeLowLevel.prototype.isConnected = isConnected; -module.exports = MaxCubeLowLevel; \ No newline at end of file +module.exports = MaxCubeLowLevel; diff --git a/maxcube.js b/maxcube.js index dfe0542..d48f2ea 100644 --- a/maxcube.js +++ b/maxcube.js @@ -15,6 +15,8 @@ function MaxCube(ip, port) { this.waitForCommandResolver = undefined; this.initialised = false; + this.setMaxListeners(1024); + this.commStatus = { duty_cycle: 0, free_memory_slots: 0, @@ -25,6 +27,7 @@ function MaxCube(ip, port) { } this.roomCache = []; this.deviceCache = {}; + this.configCache = {}; this.maxCubeLowLevel.on('closed', function () { self.initialised = false; @@ -41,32 +44,49 @@ function MaxCube(ip, port) { } }); - this.maxCubeLowLevel.on('command', function (command) { - var parsedCommand = MaxCubeCommandParser.parse(command.type, command.payload); - if (self.waitForCommandType === command.type && self.waitForCommandResolver) { - self.waitForCommandResolver.resolve(parsedCommand); - self.waitForCommandType = undefined; - self.waitForCommandResolver = undefined; - } + this.maxCubeLowLevel.on('error', function () { + self.initialised = false; + self.emit('error'); + }); - switch (command.type) { - case 'H': { - self.commStatus.duty_cycle = parsedCommand.duty_cycle; - self.commStatus.free_memory_slots = parsedCommand.free_memory_slots; - self.metaInfo.serial_number = parsedCommand.serial_number; - self.metaInfo.firmware_version = parsedCommand.firmware_version; - break; - } - case 'M': { - self.roomCache = parsedCommand.rooms; - self.deviceCache = parsedCommand.devices; - self.initialised = true; - break; + this.maxCubeLowLevel.on('command', function (command) { + try { + var parsedCommand = MaxCubeCommandParser.parse(command.type, command.payload); + if (self.waitForCommandType === command.type && self.waitForCommandResolver) { + self.waitForCommandResolver.resolve(parsedCommand); + self.waitForCommandType = undefined; + self.waitForCommandResolver = undefined; } - case 'L': { - self.updateDeviceInfo(parsedCommand); + switch (command.type) { + case 'H': { + self.commStatus.duty_cycle = parsedCommand.duty_cycle; + self.commStatus.free_memory_slots = parsedCommand.free_memory_slots; + self.metaInfo.serial_number = parsedCommand.serial_number; + self.metaInfo.firmware_version = parsedCommand.firmware_version; + self.emit('hello', parsedCommand); + break; + } + case 'M': { + self.roomCache = parsedCommand.rooms; + self.deviceCache = parsedCommand.devices; + self.initialised = true; + self.emit('meta_data', parsedCommand); + break; + } + case 'L': { + self.updateDeviceInfo(parsedCommand); + self.emit('device_list', parsedCommand); + break; + } + case 'C': { + self.updateDeviceConfig(parsedCommand); + self.emit('configuration', parsedCommand); + break; + } } + } catch (e) { + self.emit('error', "Problem while parsing the command '" +command.type+"': " + e.stack); } }); @@ -89,6 +109,13 @@ function MaxCube(ip, port) { } } } + + this.updateDeviceConfig = function(deviceConfig){ + if (typeof deviceConfig != 'undefined'){ + var rf = deviceConfig.rf_address; + self.configCache[rf] = deviceConfig; + } + } } util.inherits(MaxCube, EventEmitter); @@ -140,12 +167,24 @@ MaxCube.prototype.getDeviceStatus = function(rf_address) { }); }; +MaxCube.prototype.updateDeviceStatus = function() { + checkInitialised.call(this); + + send.call(this, 'l:\r\n'); +}; + MaxCube.prototype.getDevices = function() { checkInitialised.call(this); return this.deviceCache; }; +MaxCube.prototype.getDevice = function(rf_address) { + checkInitialised.call(this); + + return this.deviceCache[rf_address]; +}; + MaxCube.prototype.getDeviceInfo = function(rf_address) { checkInitialised.call(this); @@ -171,10 +210,23 @@ MaxCube.prototype.getDeviceInfo = function(rf_address) { deviceInfo.room_id = room.room_id; } } - + return deviceInfo; }; +MaxCube.prototype.getDeviceConfiguration = function(rf_address) { + checkInitialised.call(this); + var deviceConfig = {} + var config = this.configCache[rf_address]; + if (config) { + for(var item in config) { + var val = config[item]; + deviceConfig[item] = val; + } + } + return deviceConfig; +}; + MaxCube.prototype.getRooms = function() { checkInitialised.call(this); @@ -187,6 +239,12 @@ MaxCube.prototype.flushDeviceCache = function() { return send.call(this, 'm:\r\n'); }; +MaxCube.prototype.resetError = function(rf_address) { + checkInitialised.call(this); + + return send.call(this, MaxCubeCommandFactory.generateResetCommand(rf_address, this.deviceCache[rf_address].room_id), 'S'); +}; + MaxCube.prototype.sayHello = function() { checkInitialised.call(this); @@ -214,12 +272,12 @@ MaxCube.prototype.setSchedule = function(rf_address, room_id, weekday, temperatu // weekday: 0=mo,1=tu,..,6=su // temperaturesArray: [19.5,21,..] degrees Celsius (max 7) // timesArray: ['HH:mm',..] 24h format (max 7, same amount as temperatures) - // the first time will be the time (from 00:00 to timesArray[0]) that the first temperature is active. Last possibe time of the day: 00:00 + // the first time will be the time (from 00:00 to timesArray[0]) that the first temperature is active. Last possibe time of the day: 00:00 checkInitialised.call(this); var self = this; - + var command = MaxCubeCommandFactory.generateSetDayProgramCommand (rf_address, room_id, weekday, temperaturesArray, timesArray); return send.call(this, command, 'S').then(function (res) { self.commStatus.duty_cycle = res.duty_cycle; @@ -232,4 +290,4 @@ MaxCube.prototype.close = function() { this.maxCubeLowLevel.close(); }; -module.exports = MaxCube; \ No newline at end of file +module.exports = MaxCube; diff --git a/package.json b/package.json index 24706f2..6ada84a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "maxcube", - "version": "1.0.6", - "description": "eQ-3 Max! Cube interface", + "name": "maxcube2", + "version": "2.1.3", + "description": "eq-3 Max! Cube interface for homebridge-platform-maxcube", "main": "maxcube.js", "dependencies": { "bluebird": "^3.3.4", @@ -9,11 +9,27 @@ }, "repository": { "type": "git", - "url": "https://github.com/ivesdebruycker/maxcube.git" + "url": "https://github.com/normen/maxcube2.git" }, "author": "Ives De Bruycker", + "contributors": [ + "Pim van Gennip", + "Luca Mazzilli", + "Daniel Weeber", + "Normen Hansen" + ], + "keywords": [ + "eq-3", + "eq3", + "maxcube", + "thermostat", + "home", + "heating", + "max", + "elv" + ], "license": "ISC", "bugs": { - "url": "https://github.com/ivesdebruycker/maxcube/issues" + "url": "https://github.com/normen/maxcube2/issues" } }