├── lib ├── uniroll.js ├── esa.js ├── fht.js ├── tx.js ├── em.js ├── ws.js ├── hms.js ├── fs20.js └── moritz.js ├── .gitignore ├── .travis.yml ├── Gruntfile.js ├── package.json ├── tests └── fs20.js ├── cul.js ├── README.md └── LICENSE /lib/uniroll.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | - '4' 5 | - '0.12' 6 | - '0.10' -------------------------------------------------------------------------------- /lib/esa.js: -------------------------------------------------------------------------------- 1 | module.exports.parse = function (raw) { 2 | var message = { 3 | protocol: 'ESA' 4 | }; 5 | 6 | return message; 7 | }; -------------------------------------------------------------------------------- /lib/fht.js: -------------------------------------------------------------------------------- 1 | module.exports.parse = function (raw) { 2 | var message = {} 3 | message.protocol = 'FHT'; 4 | 5 | return message; 6 | }; 7 | 8 | module.exports.cmd = function () { 9 | 10 | return false; 11 | }; -------------------------------------------------------------------------------- /lib/tx.js: -------------------------------------------------------------------------------- 1 | // http://sourceforge.net/p/fhem/code/HEAD/tree/trunk/fhem/FHEM/14_CUL_TX.pm 2 | // http://www.f6fbb.org/domo/sensors/tx3_th.php 3 | 4 | module.exports.parse = function (raw) { 5 | var message = {}; 6 | message.protocol = 'TX'; 7 | 8 | return message; 9 | }; -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | simplemocha: { 6 | options: { 7 | globals: ['expect'], 8 | timeout: 3000, 9 | ignoreLeaks: false, 10 | ui: 'bdd', 11 | reporter: 'tap' 12 | }, 13 | all: { src: ['tests/*.js'] } 14 | } 15 | }); 16 | 17 | grunt.loadNpmTasks('grunt-simple-mocha'); 18 | grunt.registerTask('test', ['simplemocha']); 19 | 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /lib/em.js: -------------------------------------------------------------------------------- 1 | // http://sourceforge.net/p/fhem/code/HEAD/tree/trunk/fhem/FHEM/15_CUL_EM.pm#l92 2 | 3 | module.exports.parse = function (raw) { 4 | var message = {}; 5 | message.protocol = 'EM'; 6 | message.address = raw.slice(1, 5); 7 | 8 | var type = raw.slice(1, 3); 9 | 10 | message.data = {}; 11 | message.data.seq = parseInt((raw[5] + raw[6]), 16); 12 | message.data.total = parseInt((raw[9] + raw[10] + raw[7] + raw[8]), 16); 13 | message.data.current = parseInt((raw[13] + raw[13] + raw[11] + raw[12]), 16); 14 | message.data.peak = parseInt((raw[17] + raw[18] + raw[15] + raw[16]), 16); 15 | 16 | switch (type) { 17 | case '01': 18 | message.device = 'EM1000'; 19 | break; 20 | case '02': 21 | message.device = 'EM1000-EM'; 22 | message.data.current *= 10; 23 | 24 | break; 25 | case '03': 26 | message.device = 'EM1000-GZ'; 27 | break; 28 | } 29 | 30 | 31 | 32 | return message; 33 | }; 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.2", 3 | "name": "cul", 4 | "description": "Module to interact with Busware CUL / culfw", 5 | "homepage": "http://github.com/hobbyquaker/cul", 6 | "author": "hobbyquaker ", 7 | "keywords": [ 8 | "Smarthome", 9 | "Home Automation", 10 | "CUL", 11 | "FS20", 12 | "HomeMatic", 13 | "Max", 14 | "Busware", 15 | "culfw", 16 | "fhem" 17 | ], 18 | "contributors": [ 19 | "Michel Verbraak https://github.com/1stsetup", 20 | "Bluefox https://github.com/GermanBluefox" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git://github.com/hobbyquaker/cul.git" 25 | }, 26 | "main": "./cul.js", 27 | "dependencies": { 28 | "serialport": "^4.0.1" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/hobbyquaker/cul/issues" 32 | }, 33 | "scripts": { 34 | "test": "grunt test" 35 | }, 36 | "license": "GPL-2.0", 37 | "devDependencies": { 38 | "grunt": "latest", 39 | "grunt-simple-mocha": "latest", 40 | "mocha": "latest", 41 | "should": "latest" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/ws.js: -------------------------------------------------------------------------------- 1 | // http://sourceforge.net/p/fhem/code/HEAD/tree/trunk/fhem/FHEM/14_CUL_WS.pm 2 | 3 | module.exports.parse = function (raw) { 4 | var message = {}; 5 | 6 | message.address = raw[1] & 7; 7 | message.protocol = 'WS'; 8 | 9 | var firstByte = parseInt(raw[1], 16); 10 | var typByte = parseInt(raw[2], 16) & 7; 11 | var sign = 1; 12 | 13 | if (firstByte & 7 === 7) { 14 | 15 | if (typByte === 0 && raw.length > 6) { 16 | 17 | message.device = 'Temp'; 18 | sign = firstByte & 8 ? -1 : 1; 19 | message.data = {}; 20 | message.data.temperature = sign * parseFloat(raw[6] + raw[3] + '.' + raw[4]); 21 | 22 | } else if (typByte === 1 && raw.length > 8) { 23 | 24 | message.device = 'WS300'; 25 | sign = firstByte & 8 ? -1 : 1; 26 | message.data = {}; 27 | message.data.temperature = sign * parseFloat(raw[6] + raw[3] + '.' + raw[4]); 28 | message.data.humidity = sign * parseFloat(raw[7] + raw[8] + '.' + raw[5]); 29 | 30 | } 31 | 32 | } else { 33 | 34 | if (raw.length > 8) { 35 | 36 | message.device = 'S300TH'; 37 | sign = firstByte & 8 ? -1 : 1; 38 | message.data = {}; 39 | message.data.temperature = sign * parseFloat(raw[6] + raw[3] + '.' + raw[4]); 40 | message.data.humidity = sign * parseFloat(raw[7] + raw[8] + '.' + raw[5]); 41 | 42 | } 43 | 44 | } 45 | 46 | return message; 47 | 48 | }; 49 | -------------------------------------------------------------------------------- /lib/hms.js: -------------------------------------------------------------------------------- 1 | // http://sourceforge.net/p/fhem/code/HEAD/tree/trunk/fhem/FHEM/12_HMS.pm 2 | 3 | var devices = { 4 | "0": "HMS100TF", 5 | "1": "HMS100T", 6 | "2": "HMS100WD", 7 | "3": "RM100-2", 8 | "4": "HMS100TFK", // Depending on the onboard jumper it is 4 or 5 9 | "5": "HMS100TFK", 10 | "6": "HMS100MG", 11 | "8": "HMS100CO", 12 | "e": "HMS100FIT" 13 | }; 14 | 15 | module.exports.parse = function (raw) { 16 | var message = {}; 17 | message.protocol = 'HMS'; 18 | message.address = raw.slice(1, 5); 19 | 20 | var val = raw.slice(5); 21 | 22 | // TODO make sure that val[1] really contains the device type! (I can't test, only got one HMS100T) 23 | message.device = devices[val[1]]; 24 | 25 | var status1 = parseInt(val[0], 16); 26 | var sign = status1 & 8 ? -1 : 1; 27 | 28 | 29 | if (message.device === 'HMS100T') { 30 | 31 | message.data = {}; 32 | message.data.temperature = sign * parseFloat(val[5] + val[2] + '.' + val[3]); 33 | message.data.battery = 0; 34 | if (status1 & 2) data.battery = 1; 35 | if (status1 & 4) data.battery = 2; 36 | 37 | } else if (message.device === 'HMS100TF') { 38 | 39 | var status1 = parseInt(val[0], 16); 40 | var sign = status1 & 8 ? -1 : 1; 41 | 42 | message.data = {}; 43 | // Codierung 44 | message.data.temperature = sign * parseFloat(val[5] + val[2] + '.' + val[3]); 45 | message.data.humidity = parseFloat(val[6] + val[7] + '.' + val[4]); 46 | message.data.battery = 0; 47 | if (status1 & 2) message.data.battery = 1; 48 | if (status1 & 4) message.data.battery = 2; 49 | 50 | } 51 | 52 | return message; 53 | }; 54 | -------------------------------------------------------------------------------- /tests/fs20.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | var fs20 = require('./../lib/fs20.js'); 3 | 4 | 5 | describe('fs20.cmd', function () { 6 | 7 | it("('1A10', '01', '00') should return the command string 'F1A100100'", function () { 8 | var cmd = fs20.cmd('1A10', '01', '00'); 9 | cmd.should.equal('F1A100100'); 10 | }); 11 | 12 | it("('1A10', '01', 'dim06') should return the command string 'F1A100100'", function () { 13 | var cmd = fs20.cmd('1A10', '01', 'dim06'); 14 | cmd.should.equal('F1A100101'); 15 | }); 16 | 17 | it("('1A10', '01', 'dim12%') should return the command string 'F1A100100'", function () { 18 | var cmd = fs20.cmd('1A10', '01', 'dim12%'); 19 | cmd.should.equal('F1A100102'); 20 | }); 21 | 22 | it("('1A10', '01', '00') should return the command string 'F1A100100'", function () { 23 | var cmd = fs20.cmd('1A10', '01', '00'); 24 | cmd.should.equal('F1A100100'); 25 | }); 26 | 27 | it("('1112 1314', '1114', 'on') should return the command string 'F01230311'", function () { 28 | var cmd = fs20.cmd('1112 1314', '1114', 'on'); 29 | cmd.should.equal('F01230317'); 30 | }); 31 | 32 | it("('32324444', '1112', 'off') should return the command string 'F99FF0100'", function () { 33 | var cmd = fs20.cmd('32324444', '1112', 'off'); 34 | cmd.should.equal('F99FF0100'); 35 | }); 36 | 37 | it("('32524444', '1112', 'off') should return false", function () { 38 | var cmd = fs20.cmd('32524444', '1112', 'off'); 39 | cmd.should.equal(false); 40 | }); 41 | 42 | it("('G721', '01', 'off') should return false", function () { 43 | var cmd = fs20.cmd('G721', '01', 'off'); 44 | cmd.should.equal(false); 45 | }); 46 | it("('A721', 0x12, 0x00) should return 'FA7210C00'", function () { 47 | var cmd = fs20.cmd('A721', 0x12, 0x00); 48 | cmd.should.equal('FA7211200'); 49 | }); 50 | 51 | }); 52 | 53 | 54 | describe('fs20.parse', function () { 55 | 56 | it("('F99FF0100') should return have several mandatory properties", function () { 57 | var obj = fs20.parse('F99FF0100'); 58 | obj.should.have.property('protocol', 'FS20'); 59 | obj.should.have.property('address', '99FF01'); 60 | }); 61 | 62 | it("('F99FF0100') should have several data properties", function () { 63 | var obj = fs20.parse('F99FF0100'); 64 | 65 | obj.should.have.property('data'); 66 | obj.data.should.have.properties({ 67 | addressCode: '99FF', 68 | addressCodeElv: '3232 4444', 69 | addressDevice: '01', 70 | addressDeviceElv: '1112', 71 | extended: false, 72 | bidirectional: false, 73 | response: false, 74 | cmd: 'off', 75 | cmdRaw: '00' 76 | }); 77 | }); 78 | 79 | 80 | }); -------------------------------------------------------------------------------- /cul.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CUL/COC / culfw Node.js module 3 | * https://github.com/hobbyquaker/cul 4 | * 5 | * Licensed under GPL v2 6 | * Copyright (c) 2014 hobbyquaker 7 | * 8 | */ 9 | 10 | var util = require('util'); 11 | var EventEmitter = require('events').EventEmitter; 12 | 13 | var SerialPort = require("serialport"); 14 | 15 | var protocol = { 16 | em: require('./lib/em.js'), 17 | //esa: require('./lib/esa.js'), 18 | //fht: require('./lib/fht.js'), 19 | fs20: require('./lib/fs20.js'), 20 | hms: require('./lib/hms.js'), 21 | moritz: require('./lib/moritz.js'), 22 | //tx: require('./lib/tx.js'), 23 | //uniroll: require('./lib/uniroll.js'), 24 | ws: require('./lib/ws.js') 25 | }; 26 | 27 | // http://culfw.de/commandref.html 28 | var commands = { 29 | 'F': 'FS20', 30 | 'T': 'FHT', 31 | 'E': 'EM', 32 | 'W': 'WS', 33 | 'H': 'HMS', 34 | 'S': 'ESA', 35 | 'R': 'Hoermann', 36 | 'A': 'AskSin', 37 | 'V': 'MORITZ', 38 | 'Z': 'MORITZ', 39 | 'o': 'Obis', 40 | 't': 'TX', 41 | 'U': 'Uniroll', 42 | 'K': 'WS' 43 | }; 44 | 45 | var modes = { 46 | 'slowrf': {}, 47 | 'moritz': {start: 'Zr', stop: 'Zx'}, 48 | 'asksin': {start: 'Ar', stop: 'Ax'} 49 | }; 50 | 51 | 52 | var Cul = function (options) { 53 | var that = this; 54 | 55 | options.initCmd = 0x01; 56 | options.mode = options.mode || 'SlowRF'; 57 | options.init = options.init || true; 58 | options.parse = options.parse || true; 59 | options.coc = options.coc || false; 60 | options.scc = options.scc || false; 61 | options.rssi = options.rssi || true; 62 | 63 | if (options.coc) { 64 | options.baudrate = options.baudrate || 38400; 65 | options.serialport = options.serialport || '/dev/ttyACM0'; 66 | } else if (options.scc) { 67 | options.baudrate = options.baudrate || 38400; 68 | options.serialport = options.serialport || '/dev/ttyAMA0'; 69 | } else { 70 | options.baudrate = options.baudrate || 9600; 71 | options.serialport = options.serialport || '/dev/ttyAMA0'; 72 | } 73 | 74 | if (options.rssi) { 75 | // Set flag, binary or 76 | options.initCmd = options.initCmd | 0x20; 77 | } 78 | options.initCmd = 'X' + ('0' + options.initCmd.toString(16)).slice(-2); 79 | 80 | var modeCmd = modes[options.mode.toLowerCase()] ? modes[options.mode.toLowerCase()].start : undefined; 81 | var stopCmd; 82 | 83 | if (modes[options.mode.toLowerCase()] && modes[options.mode.toLowerCase()].stop) { 84 | stopCmd = modes[options.mode.toLowerCase()].stop; 85 | } 86 | 87 | var spOptions = {baudrate: options.baudrate}; 88 | if (options.coc || options.scc) spOptions.parser = SerialPortModule.parsers.readline('\r\n'); 89 | var serialPort = new SerialPort(options.serialport, spOptions); 90 | 91 | this.close = function (callback) { 92 | if (options.init && stopCmd) { 93 | that.write(stopCmd, function () { 94 | serialPort.close(callback); 95 | }); 96 | } else { 97 | serialPort.close(callback); 98 | } 99 | }; 100 | 101 | serialPort.on('close', function () { 102 | that.emit('close'); 103 | }); 104 | 105 | serialPort.on("open", function () { 106 | 107 | if (options.init) { 108 | that.write(options.initCmd, function (err) { 109 | if (err) throw err; 110 | }); 111 | serialPort.drain(function(err){ 112 | if (modeCmd) { 113 | that.write(modeCmd, function (err) { 114 | if (err) throw err; 115 | }); 116 | serialPort.drain(function(err){ 117 | if (err) throw err; 118 | ready(); 119 | }); 120 | } else { 121 | ready(); 122 | } 123 | }); 124 | } else { 125 | ready(); 126 | } 127 | 128 | function ready() { 129 | serialPort.on('data', function (data) { 130 | data = data.toString(); 131 | var tmp = data.split('\r\n'); 132 | for (var i = 0, l = tmp.length; i < l; i++) { 133 | if (typeof tmp[i] === 'string' && tmp[i] !== '') parse(tmp[i]); 134 | } 135 | }); 136 | that.emit('ready'); 137 | } 138 | 139 | }); 140 | 141 | 142 | this.write = function send(data, callback) { 143 | //console.log('->', data) 144 | serialPort.write(data + '\r\n'); 145 | serialPort.drain(callback); 146 | }; 147 | 148 | this.cmd = function cmd() { 149 | var args = Array.prototype.slice.call(arguments); 150 | 151 | if (typeof args[args.length-1] === 'function') { 152 | var callback = args.pop(); 153 | } 154 | 155 | var c = args[0].toLowerCase(); 156 | args = args.slice(1); 157 | 158 | if (commands[c.toUpperCase()]) c = commands[c.toUpperCase()].toLowerCase(); 159 | 160 | if (protocol[c] && typeof protocol[c].cmd === 'function') { 161 | var msg = protocol[c].cmd.apply(null, args); 162 | if (msg) { 163 | that.write(msg, callback); 164 | return true; 165 | } else { 166 | if (typeof callback === 'function') callback('cmd ' + c + ' ' + JSON.stringify(args) + ' failed'); 167 | return false; 168 | } 169 | } else { 170 | if (typeof callback === 'function') callback('cmd ' + c + ' not implemented'); 171 | return false; 172 | } 173 | 174 | }; 175 | 176 | function parse(data) { 177 | 178 | var message; 179 | var command; 180 | var p; 181 | var rssi; 182 | 183 | if (options.parse) { 184 | command = data[0]; 185 | message = {}; 186 | if (commands[command]) { 187 | p = commands[command].toLowerCase(); 188 | if (protocol[p] && typeof protocol[p].parse === 'function') { 189 | message = protocol[p].parse(data); 190 | } 191 | } 192 | if (options.rssi) { 193 | rssi = parseInt(data.slice(-2), 16); 194 | message.rssi = (rssi >= 128 ? ((rssi - 256) / 2 - 74) : (rssi / 2 - 74)); 195 | } 196 | } 197 | that.emit('data', data, message); 198 | } 199 | 200 | return this; 201 | }; 202 | 203 | util.inherits(Cul, EventEmitter); 204 | 205 | module.exports = Cul; 206 | -------------------------------------------------------------------------------- /lib/fs20.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * FS20 parse and cmd 4 | * https://github.com/hobbyquaker/cul 5 | * 6 | * 8'2014 hobbyquaker 7 | * GPL v2 8 | * 9 | * based on 10 | * http://fhem.de (GPL v2 License) 11 | * https://github.com/netAction/CUL_FS20 (MIT License) Copyright (c) 2013 Thomas Schmidt (netaction.de) 12 | * Uwe Langhammers Javascript implementation of hex2elv() and elv2hex() 13 | * 14 | */ 15 | 16 | 17 | // http://sourceforge.net/p/fhem/code/HEAD/tree/trunk/fhem/FHEM/10_FS20.pm 18 | 19 | // List of commands 20 | // http://fhz4linux.info/tiki-index.php?page=FS20%20Protocol 21 | // http://www.eecs.iu-bremen.de/archive/bsc-2008/stefanovIvan.pdf 22 | var commands = [ 23 | 'off', // 0x00 0 24 | 'dim06%', // 0x01 1 25 | 'dim12%', // 0x02 2 26 | 'dim18%', // 0x03 3 27 | 'dim25%', // 0x04 4 28 | 'dim31%', // 0x05 5 29 | 'dim37%', // 0x06 6 30 | 'dim43%', // 0x07 7 31 | 'dim50%', // 0x08 8 32 | 'dim56%', // 0x09 9 33 | 'dim62%', // 0x0A 10 34 | 'dim68%', // 0x0B 11 35 | 'dim75%', // 0x0C 12 36 | 'dim81%', // 0x0D 13 37 | 'dim87%', // 0x0E 14 38 | 'dim93%', // 0x0F 15 39 | 'dim100%', // 0x10 16 40 | 'on', // 0x11 17 Set to previous dim value (before switching it off) 41 | 'toggle', // 0x12 18 between off and previous dim val 42 | 'dimup', // 0x13 19 43 | 'dimdown', // 0x14 20 44 | 'dimupdown', // 0x15 21 45 | 'sendstate', // 0x17 22 46 | 'off-for-timer', // 0x18 23 47 | 'on-for-timer', // 0x19 24 48 | 'on-old-for-timer', // 0x1A 25 49 | 'reset', // 0x1B 26 50 | 'ramp-on-time', // 0x1C 27 time to reach the desired dim value on dimmers 51 | 'ramp-off-time', // 0x1D 28 time to reach the off state on dimmers 52 | 'on-old-for-timer-prev', // 0x1E 29 old val for timer, then go to prev. state 53 | 'on-100-for-timer-prev' // 0x1F 30 100% for timer, then go to previous state 54 | ]; 55 | 56 | 57 | module.exports.parse = function (raw) { 58 | 59 | var message = {}; 60 | message.protocol = 'FS20'; 61 | 62 | var command = raw.slice(7, 9); 63 | 64 | message.address = raw.slice(1, 7); 65 | message.data = {}; 66 | message.data.addressCode = message.address.slice(0, 4); 67 | message.data.addressCodeElv = hex2elv(message.data.addressCode); 68 | message.data.addressDevice = message.address.slice(4, 6); 69 | message.data.addressDeviceElv = hex2elv(message.data.addressDevice); 70 | 71 | var commandNum = parseInt(command, 16); 72 | 73 | message.data.extended = (commandNum & 32 ? true : false); 74 | message.data.bidirectional = (commandNum & 64 ? true : false); 75 | message.data.response = (commandNum & 128 ? true : false); 76 | 77 | message.data.cmd = commands[parseInt(command, 16)]; 78 | 79 | if (message.isExtended) { 80 | message.time = 0.25 * (parseInt(raw.slice(9, 11), 16) & 15) * (2 ^ parseInt(raw.slice(9, 11), 16) & 240); 81 | command = raw.slice(7, 11) 82 | } 83 | 84 | message.data.cmdRaw = command; 85 | 86 | return message; 87 | }; 88 | 89 | /** 90 | * 91 | * fs20.cmd 92 | * 93 | * @param code string, the 'house code' - 4 digits hex string or 8 digits elv-notation string 94 | * @param address string, device address - 2 digits hex string or 4 digits elv-notation string 95 | * @param command string, cmd text or 2 or 4 digits hex string 96 | * @param time integer, optional, seconds, automatically sets extended flag 97 | * @param bidi boolean, optional, bidirectional flag 98 | * @param res boolean, optional, bidirectional response flag 99 | * @returns object string (the raw message) or boolean false (on error) 100 | * 101 | */ 102 | module.exports.cmd = function (code, address, command, time, bidi, res) { 103 | 104 | if (typeof code === 'number') { 105 | // code given as number, convert to 4 digit hexstring 106 | code = ('000' + code.toString(16)).slice(-4); 107 | } else if (typeof code !== 'string') { 108 | return false; 109 | } 110 | 111 | if (code.length > 4) { 112 | code = elv2hex(code); 113 | if (!code) return false; 114 | } 115 | 116 | code = code.toUpperCase(); 117 | 118 | if (!code.match(/^[A-F0-9]{4}$/)) { 119 | return false; 120 | } 121 | 122 | if (typeof address === 'number') address = ('0' + address.toString(16)).slice(-2); 123 | 124 | address = address.toUpperCase(); 125 | 126 | if (address.length > 2) { 127 | address = elv2hex(address); 128 | } 129 | 130 | if (!address.match(/^[A-F0-9]{2}$/)) { 131 | return false; 132 | } 133 | 134 | if (typeof command === 'number') { 135 | command = ('0' + command.toString(16)).slice(-2); 136 | } else if (typeof command === 'string') { 137 | // Text commands 138 | if (command.match(/^dim[0-9]+/) && command.slice(-1) !== '%') command += '%'; 139 | if (commands.indexOf(command) !== -1) { 140 | command = ('0' + commands.indexOf(command)).slice(-2).toString(16); 141 | } 142 | command = command.toUpperCase(); 143 | } else { 144 | return false; 145 | } 146 | 147 | if (!command.match(/^[0-9A-F]{2}$/)) return false; 148 | 149 | if (bidi) command = (parseInt(command, 16) | 64).toString(16).toUpperCase(); 150 | if (res) command = (parseInt(command, 16) | 128).toString(16).toUpperCase(); 151 | 152 | if (time) { 153 | // set extended flag in first commandbyte 154 | command = (parseInt(command, 16) | 32).toString(16).toUpperCase(); 155 | // append 2nd byte 156 | command = command + seconds2time(time); 157 | if (!command.match(/^[0-9A-F]{4}$/)) return false; 158 | } 159 | 160 | return 'F' + code + address + command; 161 | }; 162 | 163 | function seconds2time(sec) { 164 | if (!sec) return('00'); 165 | if (sec > 15360) sec = 15360; 166 | var tmp; 167 | for (var i = 0; i <= 12; i++) { 168 | for (var j = 0; j <= 15; j++) { 169 | tmp = 0.25 * j * (2 ^ i); 170 | if (tmp >= sec) { 171 | return i.toString(16) + j.toString(16); 172 | } 173 | } 174 | } 175 | } 176 | 177 | 178 | // elv2hex() by Uwe Langhammer 179 | function elv2hex(val) { 180 | var i = 0; 181 | var ret = ''; 182 | while (i < val.length) { 183 | var ch = val.substr(i, 1); 184 | if (ch != ' ') { 185 | var cl = val.substr(i + 1, 1); 186 | if (!(ch > 0 && ch < 5)) { 187 | return false; 188 | } 189 | if (cl == '') { 190 | cl = 1; 191 | } 192 | if (!(cl > 0 && cl < 5)) { 193 | return false; 194 | } 195 | ch -= 1; 196 | cl -= 1; 197 | var r = (ch << 2) + cl; 198 | ret += r.toString(16).toUpperCase(); 199 | i += 2; 200 | } else i += 1; 201 | } 202 | return ret; 203 | } 204 | 205 | // hex2elv() by Uwe Langhammer 206 | function hex2elv(val) { 207 | var i = 0; 208 | var ret = ''; 209 | while (i < val.length) { 210 | var h = val.substr(i, 1); 211 | var d = parseInt(h, 16); 212 | if (d >= 0 && d <= 15) { 213 | var cl = d & 3; 214 | var ch = d >> 2; 215 | cl++; 216 | ch++; 217 | if (i && (i % 2 == 0)) ret += ' '; 218 | ret += ch.toString() + cl.toString(); 219 | } else { 220 | return false; 221 | } 222 | i++; 223 | } 224 | return ret; 225 | } 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cul 2 | 3 | [![License][gpl-badge]][gpl-url] 4 | [![NPM version](https://badge.fury.io/js/cul.svg)](http://badge.fury.io/js/cul) 5 | [![Dependency Status](https://img.shields.io/gemnasium/hobbyquaker/cul.svg?maxAge=2592000)](https://gemnasium.com/github.com/hobbyquaker/cul) 6 | [![Build Status](https://travis-ci.org/hobbyquaker/cul.svg?branch=master)](https://travis-ci.org/hobbyquaker/cul) 7 | 8 | This is a [Node.js](http://nodejs.org) module that can be used to interact with a [Busware CUL (USB)](http://busware.de/tiki-index.php?page=CUL), 9 | [COC (RaspberryPi)](http://busware.de/tiki-index.php?page=COC) or [SCC (RaspberryPi)](http://busware.de/tiki-index.php?page=SCC) running [culfw](http://culfw.de). With CUL/COC/SCC and culfw 10 | many RF devices can be controlled, like [FS20](http://www.elv.de/fs20-funkschaltsystem.html), 11 | [MAX!](http://www.elv.de/max-imale-kontrolle.html), temperature sensors, weather stations and more. 12 | [Click here for a full list of supported Devices](http://culfw.de/culfw.html#Features) 13 | 14 | #### Purpose 15 | 16 | This module provides a thin abstraction for the serial port communication with CUL/COC/SCC and lightweight parse and command 17 | wrappers. It's intended to be used in different Node.js based Home Automation software. 18 | 19 | #### Credits 20 | 21 | based on the work of Rudolf Koenig, Author of [culfw](http://culfw.de) and [fhem](http://fhem.de) (both licensed under GPLv2) 22 | 23 | 24 | ## Usage 25 | 26 | ```npm install cul``` 27 | 28 | ```javascript 29 | var Cul = require('cul'); 30 | var cul = new Cul(); 31 | 32 | // ready event is emitted after serial connection is established and culfw acknowledged data reporting 33 | cul.on('ready', function () { 34 | // send arbitrary commands to culfw 35 | cul.write('V'); 36 | }); 37 | 38 | cul.on('data', function (raw) { 39 | // show raw incoming messages 40 | console.log(raw); 41 | }); 42 | 43 | ``` 44 | 45 | ## Options 46 | 47 | * **serialport** (default: ```"/dev/ttyAMA0"```) 48 | * **baudrate** (default: ```9600```) 49 | * **mode** (default: ```"SlowRF"```) 50 | possible values: 51 | * ```SlowRF``` (FS20, HMS, FHT, EM, ...) 52 | * ```MORITZ``` (MAX! devices) 53 | * ```AskSin``` (HomeMatic devices) 54 | * **parse** (default: ```true```) 55 | try to parse received messages 56 | * **init** (default: ```true```) 57 | auto send "enable datareporting" command when connection is established (depends on chosen mode) 58 | * **coc** (default: ```false```) 59 | has to be enabled for usage with [COC](http://busware.de/tiki-index.php?page=COC)), changes default baudrate to 38400 and default serialport to /dev/ttyACM0 60 | * **scc** (default: ```false```) 61 | has to be enabled for usage with [SCC](http://busware.de/tiki-index.php?page=SCC)), changes default baudrate to 38400 and default serialport to /dev/ttyAMA0 62 | * **rssi** (default: ```true```) 63 | receive rssi (signal strength) value with every message (works only if init and parse are both true) 64 | 65 | pass options when creating a new cul object: 66 | ```javascript 67 | var Cul = require('cul'); 68 | var fs20 = new Cul({ 69 | serialport: '/dev/ttyACM0', 70 | mode: 'SlowRF' 71 | }); 72 | var max = new Cul({ 73 | serialport: '/dev/ttyACM1', 74 | mode: 'MORITZ' 75 | }); 76 | ``` 77 | 78 | ## Methods 79 | 80 | 81 | * **close( )** 82 | close the serialport connection 83 | * **write(raw, callback)** 84 | send message to cul. writes directly to the serialport 85 | optional callback is passed through to serialport module and is called with params (err, res) 86 | * **cmd(protocol, arg1, arg2, ..., callback)** 87 | generate a command and send it to cul (see chapter "predefined commands" below) 88 | optional callback is passed through to serialport module and is called with params (err, res) 89 | 90 | 91 | ## Events 92 | 93 | * **ready** 94 | called when serialport connection is established and (if init is true) datareporting is enabled 95 | * **close** 96 | called when serialport connection is closed 97 | * **data(raw, obj)** 98 | called for every received message 99 | * **raw** string, contains the raw message received from cul 100 | * **obj** object, contains parsed message data (see "data parsing" below) 101 | 102 | ## Sending commands 103 | 104 | ### Raw commands 105 | 106 | Example 107 | ```javascript 108 | cul.write('F6C480111'); // Raw command 109 | ``` 110 | ### Predefined commands 111 | 112 | (until now only FS20 is implemented) 113 | 114 | #### FS20 115 | 116 | Take a look at the file lib/fs20.js - it exports a function cmd(housecode, address, command, time, bidi, res) 117 | 118 | example 119 | ```javascript 120 | cul.cmd('FS20', '2341 2131', '1112', 'on'); // house code in ELV-Notation, address in ELV-Notation, command as text 121 | cul.cmd('FS20', '6C48', '01', '11'); // house code as hex string, address as hex string, command as hex string 122 | ``` 123 | (these examples result in the same message as the raw command example above.) 124 | 125 | 126 | ## Data parsing 127 | 128 | The 2nd param ```obj``` of the data event contains a object representation of the parsed data. 129 | 130 | Each object has the following attributes: 131 | 132 | * **protocol** 133 | FS20, EM, HMS, WS, MORITZ, ... 134 | * **address** 135 | a unique address in this protocol 136 | * **device** 137 | device type name 138 | * **rssi** 139 | radio signal strength value (only present if option rssi is true) 140 | * **data** 141 | a object with the parsed data 142 | 143 | 144 | ### Examples 145 | 146 | Sample output of 147 | ```javascript 148 | cul.on('data', function (raw, obj) { 149 | console.log(raw, obj); 150 | }); 151 | ``` 152 | 153 | #### FS20 154 | ``` 155 | F6C480011E5, { 156 | protocol: 'FS20', 157 | address: '6C4800', 158 | device: 'FS20', 159 | rssi: -87.5, 160 | data: { 161 | addressCode: '6C48', 162 | addressCodeElv: '2341 2131', 163 | addressDevice: '00', 164 | addressDeviceElv: '1111', 165 | extended: false, 166 | time: null, 167 | bidirectional: false, 168 | response: false, 169 | cmdRaw: '11', 170 | cmd: 'on' 171 | 172 | } 173 | } 174 | ``` 175 | 176 | #### EM1000 177 | ``` 178 | E020563037A01000200EC, { 179 | protocol: 'EM', 180 | address: '0205', 181 | device: 'EM1000-EM', 182 | rssi: -84, 183 | data: { seq: 99, total: 31235, current: 1, peak: 2 } 184 | } 185 | ``` 186 | 187 | #### S300TH 188 | ``` 189 | K1145525828, { 190 | protocol: 'WS', 191 | address: 1, 192 | device: 'S300TH', 193 | rssi: -28, 194 | data: { temperature: 24.5, humidity: 58.5 }, 195 | } 196 | ``` 197 | 198 | #### Moritz (MAX!) 199 | ``` 200 | V 1.66 CSM868 { data: { culfw: { version: '1.66', hardware: 'CSM868' } }, 201 | protocol: 'MORITZ', 202 | rssi: -22 } 203 | Z0C000442113AD30C4F0D001CB41D { data: 204 | { len: 12, 205 | msgcnt: 0, 206 | msgFlag: '04', 207 | msgTypeRaw: '42', 208 | msgType: 'WallThermostatControl', 209 | src: '113ad3', 210 | dst: '0c4f0d', 211 | groupid: 0, 212 | payload: '1CB41D', 213 | desiredTemperature: 14, 214 | measuredTemperature: 18 }, 215 | protocol: 'MORITZ', 216 | address: '113ad3', 217 | device: 'WallMountedThermostat', 218 | rssi: -59.5 } 219 | Z0E0002020C4F0D113AD3000119001C1E { data: 220 | { len: 14, 221 | msgcnt: 0, 222 | msgFlag: '02', 223 | msgTypeRaw: '02', 224 | msgType: 'Ack', 225 | src: '0c4f0d', 226 | dst: '113ad3', 227 | groupid: 0, 228 | payload: '0119001C1E', 229 | dstDevice: 'WallMountedThermostat' }, 230 | protocol: 'MORITZ', 231 | address: '0c4f0d', 232 | rssi: -59 } 233 | Z0B4F06300E3F3C1234560012F7 { data: 234 | { len: 11, 235 | msgcnt: 79, 236 | msgFlag: '06', 237 | msgTypeRaw: '30', 238 | msgType: 'ShutterContactState', 239 | src: '0e3f3c', 240 | dst: '123456', 241 | groupid: 0, 242 | payload: '12F7', 243 | isopen: 1, 244 | unkbits: 4, 245 | rferror: 0, 246 | batterlow: 0, 247 | battery: 'ok' }, 248 | protocol: 'MORITZ', 249 | address: '0e3f3c', 250 | device: 'ShutterContact', 251 | rssi: -78.5 } 252 | ``` 253 | 254 | 255 | Until now only for a few selected devices data parsing is implemented. 256 | 257 | | protocol | device | should work | tested | 258 | |:--------- |:---------------------- |:-----------: |:------: | 259 | | FS20 | all Devices | :white_check_mark: | :white_check_mark: | 260 | | HMS | HMS100T | :white_check_mark: | :white_check_mark: | 261 | | HMS | HMS100TF | :white_check_mark: | | 262 | | EM | EM1000(-EM, -GZ, -WZ) | :white_check_mark: | :white_check_mark: | 263 | | WS | S300TH | :white_check_mark: | :white_check_mark: | 264 | | MORITZ | HeatingThermostat | :white_check_mark: | | 265 | | MORITZ | WallMountedThermostat | :white_check_mark: | | 266 | | MORITZ | ShutterContact | :white_check_mark: | :white_check_mark: | 267 | | MORITZ | PushButton | :white_check_mark: | | 268 | 269 | More can be added easily: take a look at the files in the directory lib/ and find your inspiration on 270 | http://sourceforge.net/p/fhem/code/HEAD/tree/trunk/fhem/FHEM/ 271 | 272 | Pull requests welcome! 273 | 274 | ## further reading 275 | 276 | * [culfw command reference](http://culfw.de/commandref.html) 277 | 278 | 279 | 280 | ## Todo 281 | 282 | * configurable serialport auto reconnect 283 | * more data parser modules 284 | * MORITZ (MAX!) (inprogress) 285 | * ESA 286 | * FHT 287 | * HMS: HMS100WD, RM100-2, HMS100TFK, HMS100MG, HMS100CO, HMS100FIT 288 | * ... 289 | * more command modules 290 | * MORITZ (inprogress) 291 | * FHT 292 | * ... 293 | * more tests 294 | * [CUNO](http://busware.de/tiki-index.php?page=CUNO) support 295 | 296 | Pull requests welcome! :smile: 297 | 298 | ## Credits 299 | 300 | * http://culfw.de 301 | * http://fhem.de 302 | * https://github.com/voodootikigod/node-serialport 303 | * https://github.com/netAction/CUL_FS20 304 | * https://github.com/katanapod/COC_FS20 305 | 306 | ## License 307 | 308 | [Licensed under GPLv2](LICENSE) 309 | 310 | Copyright (c) 2014-2016 hobbyquaker 311 | 312 | [gpl-badge]: https://img.shields.io/badge/License-GPL-blue.svg?style=flat 313 | [gpl-url]: LICENSE -------------------------------------------------------------------------------- /lib/moritz.js: -------------------------------------------------------------------------------- 1 | var device_types = { 2 | 0 : "Cube", 3 | 1 : "HeatingThermostat", 4 | 2 : "HeatingThermostatPlus", 5 | 3 : "WallMountedThermostat", 6 | 4 : "ShutterContact", 7 | 5 : "PushButton" 8 | }; 9 | 10 | var msgId2Cmd = { 11 | "00" : "PairPing", 12 | "01" : "PairPong", 13 | "02" : "Ack", 14 | "03" : "TimeInformation", 15 | 16 | "10" : "ConfigWeekProfile", 17 | "11" : "ConfigTemperatures", //like eco/comfort etc 18 | "12" : "ConfigValve", 19 | 20 | "20" : "AddLinkPartner", 21 | "21" : "RemoveLinkPartner", 22 | "22" : "SetGroupId", 23 | "23" : "RemoveGroupId", 24 | 25 | "30" : "ShutterContactState", 26 | 27 | "40" : "SetTemperature", //to thermostat 28 | "42" : "WallThermostatControl", //by WallMountedThermostat 29 | //Sending this without payload to thermostat sets desiredTempeerature to the comfort/eco temperature 30 | //We don't use it, we just do SetTemperature 31 | "43" : "SetComfortTemperature", 32 | "44" : "SetEcoTemperature", 33 | 34 | "50" : "PushButtonState", 35 | 36 | "60" : "ThermostatState", //by HeatingThermostat 37 | 38 | "70" : "WallThermostatState", 39 | 40 | "82" : "SetDisplayActualTemperature", 41 | 42 | "F1" : "WakeUp", 43 | "F0" : "Reset", 44 | }; 45 | 46 | var ctrl_modes = { 47 | '0' : "auto", 48 | '1' : "manual", 49 | '2' : "temporary", 50 | '3' : "boost", 51 | }; 52 | 53 | var seen_devices = {}; 54 | 55 | function addDeviceByMsgType(addr, msgType) { 56 | switch (msgType) { 57 | case "ShutterContactState": 58 | seen_devices[addr] = device_types[4]; 59 | break; 60 | case "WallThermostatConfig": 61 | case "WallThermostatState": 62 | case "WallThermostatControl": 63 | case "SetTemperature": 64 | seen_devices[addr] = device_types[3]; 65 | break; 66 | case "HeatingThermostatConfig": 67 | case "ThermostatState": 68 | seen_devices[addr] = device_types[1]; 69 | break; 70 | } 71 | } 72 | 73 | function hex2byte (hexStr) { 74 | var result = null; 75 | try { 76 | result = ("0x"+hexStr)*1; 77 | } 78 | catch(err) { 79 | result = null; 80 | } 81 | return result; 82 | } 83 | 84 | function getBits(value, offset, len) { 85 | var mask = 0; 86 | while (len > 0) { 87 | mask = mask << 1; 88 | mask++; 89 | len--; 90 | } 91 | return ((value >> offset) & mask); 92 | } 93 | 94 | function MAX_DateTime2Internal(dateObj) { 95 | var result = ((dateObj.month & 0x0E) << 20); 96 | result |= (dateObj.day << 16); 97 | result |= ((dateObj.month & 0x01) << 15); 98 | result |= ((dateObj.year - 2000) << 8); 99 | result |= (dateObj.hour * 2 + (dateObj.min/30)); 100 | 101 | return result; 102 | } 103 | 104 | function MAX_ParseDateTime(byte1, byte2, byte3) { 105 | var result = {}; 106 | result.day = byte1 & 0x1F; 107 | result.month = ((byte1 & 0xE0) >> 4) | (byte2 >> 7); 108 | result.year = byte2 & 0x3F; 109 | var time = byte3 & 0x3F; 110 | if ((time % 2) > 0) 111 | result.time = (time / 2) + ":30"; 112 | else 113 | result.time = (time / 2) + ":00"; 114 | 115 | result.str = result.day+"."+result.month+"."+result.year+" "+result.time; 116 | return result; 117 | } 118 | 119 | function MAX_SerializeTemperature(temperature) { 120 | if ((temperature == "on") || (temperature == "off")) 121 | return temperature; 122 | else if (temperature == 4.5) 123 | return "off"; 124 | else if (temperature == 30.5) 125 | return "on"; 126 | else 127 | return temperature+" C"; 128 | } 129 | 130 | module.exports.parse = function (raw) { 131 | var message = { data: {} }; 132 | var data = message.data; 133 | message.protocol = 'MORITZ'; 134 | 135 | switch (raw.charAt(0)) { 136 | case "Z": 137 | // Check if we have a Z character and at least two following characters which specify the length 138 | data.len = hex2byte(raw.substr(1,2)); 139 | if ((2*data.len+3+2) != raw.length) { //+3 = +1 for 'Z' and +2 for len field in hex and +2 looks to be some checksum 140 | data.error = 'len mismatch'; 141 | data.strlen = raw.length; 142 | data.expectedlen = (2*data.len+3); 143 | } 144 | else { 145 | data.msgcnt = hex2byte(raw.substr(3,2)); 146 | data.msgFlag = raw.substr(5,2); 147 | data.msgTypeRaw = raw.substr(7,2); 148 | data.msgType = msgId2Cmd[data.msgTypeRaw] ? msgId2Cmd[data.msgTypeRaw] : data.msgTypeRaw; 149 | data.src = raw.substr(9,6).toLowerCase(); 150 | message.address = data.src; 151 | data.dst = raw.substr(15,6).toLowerCase(); 152 | data.groupid = hex2byte(raw.substr(21,2)); 153 | data.payload = raw.substr(23, (2*data.len - 2)); 154 | 155 | addDeviceByMsgType(data.src, data.msgType); 156 | if (seen_devices[data.src]) message.device = seen_devices[data.src]; 157 | if (seen_devices[data.dst]) data.dstDevice = seen_devices[data.dst]; 158 | 159 | var desiredTemperatureRaw = null; 160 | var measuredTemperature = null; 161 | 162 | switch (data.msgType) { 163 | case "TimeInformation": 164 | if (data.payload.length == 12) { 165 | data.year = 2000+hex2byte(data.payload.substr(0,2)); 166 | data.day = hex2byte(data.payload.substr(2,2)); 167 | data.hour = hex2byte(data.payload.substr(4,2)) & 0x1F; 168 | data.min = hex2byte(data.payload.substr(6,2)) & 0x3F; 169 | data.sec = hex2byte(data.payload.substr(8,2)) & 0x3F; 170 | data.month = ((hex2byte(data.payload.substr(6,2)) >> 6) << 2) | (hex2byte(data.payload.substr(8,2)) >> 6); // this is just quessed according to FHEM 171 | data.unk1 = hex2byte(data.payload.substr(4,2)) >> 5; 172 | data.unk2 = hex2byte(data.payload.substr(6,2)) >> 6; 173 | data.unk3 = hex2byte(data.payload.substr(8,2)) >> 6; 174 | } 175 | break; 176 | case "PairPing": 177 | data.firmware = hex2byte(data.payload.substr(0,2)); 178 | data.msgType = hex2byte(data.payload.substr(2,2)); 179 | data.testresult = hex2byte(data.payload.substr(4,2)); 180 | data.serial = data.payload.substr(6); 181 | message.device = device_types[message.pairPing.type]; 182 | seen_devices[message.address] = message.device; 183 | break; 184 | case "SetTemperature": 185 | var bits = hex2byte(data.payload.substr(0,2)); 186 | data.mode = bits >> 6; 187 | data.modeStr = ctrl_modes[data.mode] ? ctrl_modes[data.mode] : data.mode; 188 | data.desiredTemperature = (bits & 0x3F) / 2.0; // Convert to degree celcius. 189 | break; 190 | case "WallThermostatControl": 191 | desiredTemperatureRaw = hex2byte(data.payload.substr(0,2)); 192 | measuredTemperature = hex2byte(data.payload.substr(2,2)); 193 | break; 194 | case "WallThermostatState": 195 | var bits2 = hex2byte(data.payload.substr(0,2)); 196 | data.displayActualTemperature = hex2byte(data.payload.substr(2,2)); 197 | desiredTemperatureRaw = hex2byte(data.payload.substr(4,2)); 198 | var null1 = hex2byte(data.payload.substr(6,2)); 199 | var heaterTemperature = hex2byte(data.payload.substr(8,2)); 200 | var null2 = hex2byte(data.payload.substr(10,2)); 201 | measuredTemperature = hex2byte(data.payload.substr(12,2)); 202 | 203 | data.mode = getBits(bits2, 0, 2); 204 | data.modeStr = ctrl_modes[data.mode] ? ctrl_modes[data.mode] : data.mode; 205 | data.dstsetting = getBits(bits2, 3, 1); //is automatically switching to DST activated 206 | data.langateway = getBits(bits2, 4, 1); //?? 207 | data.panel = getBits(bits2, 5, 1); //1 if the heating thermostat is locked for manually setting the temperature at the device 208 | data.rferror = getBits(bits2, 6, 1); //communication with link partner (what does that mean?) 209 | data.batterlow = getBits(bits2, 7, 1); //1 if battery is low 210 | data.battery = data.batterlow ? "low" : "ok"; 211 | 212 | if ((null2) && (null1 > 0) || (null2 > 0)) { 213 | data.untilStr = MAX_ParseDateTime(null1,heaterTemperature,null2); 214 | heaterTemperature = null; 215 | } 216 | if (heaterTemperature !== null) data.heaterTemperature = heaterTemperature; 217 | 218 | break; 219 | case "ThermostatState": 220 | var bits2 = hex2byte(data.payload.substr(0,2)); 221 | data.valveposition = hex2byte(data.payload.substr(2,2)); 222 | var desiredTemperatureRaw = hex2byte(data.payload.substr(4,2)); 223 | var until1 = hex2byte(data.payload.substr(6,2)); 224 | var until2 = hex2byte(data.payload.substr(8,2)); 225 | var until3 = hex2byte(data.payload.substr(10,2)); 226 | 227 | data.mode = getBits(bits2, 0, 2); 228 | data.modeStr = ctrl_modes[data.mode] ? ctrl_modes[data.mode] : data.mode; 229 | data.dstsetting = getBits(bits2, 3, 1); //is automatically switching to DST activated 230 | data.langateway = getBits(bits2, 4, 1); //?? 231 | data.panel = getBits(bits2, 5, 1); //1 if the heating thermostat is locked for manually setting the temperature at the device 232 | data.rferror = getBits(bits2, 6, 1); //communication with link partner (what does that mean?) 233 | data.batterlow = getBits(bits2, 7, 1); //1 if battery is low 234 | data.battery = data.batterlow ? "low" : "ok"; 235 | 236 | if (until3 > 0) data.untilStr = MAX_ParseDateTime(until1, until2, until3); 237 | var measuredTemperature = (until2 > 0) ? (((until1 & 0x01)<<8) + until2)/10 : 0; 238 | //If the control mode is not "temporary", the cube sends the current (measured) temperature 239 | if ((data.mode == 2) || (measuredTemperature == 0)) measuredTemperature = null; 240 | if (data.mode != 2) delete data.untilStr; 241 | 242 | if (measuredTemperature) data.measuredTemperature = measuredTemperature; 243 | 244 | break; 245 | case "ShutterContactState": 246 | var bits = hex2byte(data.payload.substr(0,2)); 247 | data.isopen = (getBits(bits, 0, 2) === 0) ? 0 : 1; 248 | data.unkbits = getBits(bits, 2, 4); 249 | data.rferror = getBits(bits, 6, 1); 250 | data.batterlow = getBits(bits, 7, 1); 251 | data.battery = data.batterlow ? "low" : "ok"; 252 | break; 253 | case "PushButtonState": 254 | var bits2 = hex2byte(data.payload.substr(0,2)); 255 | data.onoff = data.payload.substr(2,2); 256 | 257 | //The meaning of $bits2 is completly guessed based on similarity to other devices, TODO: confirm 258 | data.gateway = getBits(bits2, 4, 1); // Paired to a CUBE? 259 | data.rferror = getBits(bits2, 6, 1); 260 | data.batterlow = getBits(bits2, 7, 1); 261 | data.battery = data.batterlow ? "low" : "ok"; 262 | break; 263 | /* 264 | if($device_types{$type} =~ /HeatingThermostat./) { 265 | Dispatch($shash, "MAX,$isToMe,HeatingThermostatConfig,$src,17,21,30.5,4.5,$defaultWeekProfile,80,5,0,12,15,100,0,0,12", {}); 266 | } elsif($device_types{$type} eq "WallMountedThermostat") { 267 | Dispatch($shash, "MAX,$isToMe,WallThermostatConfig,$src,17,21,30.5,4.5,$defaultWeekProfile,80,5,0,12", {}); 268 | } 269 | 270 | */ 271 | case "HeatingThermostatConfig": 272 | case "WallThermostatConfig": 273 | break; 274 | } 275 | if (desiredTemperatureRaw !== null) data.desiredTemperature = (desiredTemperatureRaw & 0x7F) / 2.0; 276 | if (measuredTemperature !== null) data.measuredTemperature = (((desiredTemperatureRaw & 0x80)<<1) + measuredTemperature) / 10 ; 277 | } 278 | break; 279 | case "V": 280 | // Should be followed by version of culfw. 281 | var version=raw.split(' '); 282 | data.culfw = {}; 283 | data.culfw.version = version[1]; 284 | data.culfw.hardware = version[2] 285 | break; 286 | } 287 | 288 | return message; 289 | }; 290 | 291 | module.exports.cmd = function () { 292 | 293 | return false; 294 | }; 295 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------