UNPKG

iobroker.maxcube

Version:
350 lines (305 loc) 13.7 kB
'use strict'; // Device types const EQ3MAX_DEV_TYPE_CUBE = 0; const EQ3MAX_DEV_TYPE_THERMOSTAT = 1; const EQ3MAX_DEV_TYPE_THERMOSTAT_PLUS = 2; const EQ3MAX_DEV_TYPE_WALLTHERMOSTAT = 3; const EQ3MAX_DEV_TYPE_SHUTTER_CONTACT = 4; const EQ3MAX_DEV_TYPE_PUSH_BUTTON = 5; const EQ3MAX_DEV_TYPE_WINDOW_SWITCH = 6; const EQ3MAX_DEV_TYPE_UNKNOWN = 99; const StringDecoder = require('string_decoder').StringDecoder; const stringDecoder = new StringDecoder('utf8'); function parse(commandType, payload, log) { switch (commandType) { case 'H': return parseCommandHello.call(this, payload, log); case 'M': return parseCommandMetadata.call(this, payload, log); case 'C': return parseCommandConfiguration.call(this, payload, log); case 'L': return parseCommandDeviceList.call(this, payload, log); case 'S': return parseCommandSendDevice.call(this, payload, log); case 'A': return parseCommandAcknowledge.call(this, payload, log); default: console.error('Unknown command type: ' + commandType); } } const decodeStringPayload = function (charArray) { return stringDecoder.write(Buffer.from(charArray)); }; function parseCommandHello(payload, log) { const payloadArr = payload.split(','); return { serial_number: payloadArr[0], rf_address: payloadArr[1], firmware_version: payloadArr[2], //unknown: payloadArr[3], http_connection_id: payloadArr[4], duty_cycle: parseInt(payloadArr[5], 16), free_memory_slots: parseInt(payloadArr[6], 16), cube_date: 2000 + parseInt(payloadArr[7].substr(0, 2), 16) + '-' + parseInt(payloadArr[7].substr(2, 2), 16) + '-' + parseInt(payloadArr[7].substr(4, 2), 16), cube_time: parseInt(payloadArr[8].substr(0, 2), 16) + ':' + parseInt(payloadArr[8].substr(2, 2), 16) , state_cube_time: payloadArr[9], ntp_counter: payloadArr[10] }; } function parseCommandMetadata(payload, log) { const payloadArr = payload.split(','); if (payloadArr.length < 3) { console.error('Invalid Metadata received'); return { rooms: {}, devices: {} }; } let decodedPayload = new Buffer(payloadArr[2], 'base64'); const room_count = decodedPayload[2]; let currentIndex = 3; const rooms = {}; const devices = {}; // parse rooms for (let i = 0; i < room_count; i++) { const room_id = decodedPayload[currentIndex]; const room_name_length = decodedPayload[currentIndex + 1]; const room_name = decodeStringPayload(decodedPayload.slice(currentIndex + 2, currentIndex + 2 + room_name_length)); const group_rf_address = decodedPayload.slice(currentIndex + 2 + room_name_length, currentIndex + room_name_length + 5).toString('hex'); rooms[room_id] = { room_id: room_id, room_name: room_name, group_rf_address: group_rf_address }; currentIndex = currentIndex + room_name_length + 5; } // parse devices if (currentIndex < decodedPayload.length) { const device_count = decodedPayload[currentIndex]; for (let i = 0; i < device_count; i++) { const device_type = decodedPayload[currentIndex + 1]; const rf_address = decodedPayload.slice(currentIndex + 2, currentIndex + 5).toString('hex'); const serialnumber = decodedPayload.slice(currentIndex + 5, currentIndex + 15).toString(); const device_name_length = decodedPayload[currentIndex + 15]; const device_name = decodeStringPayload(decodedPayload.slice(currentIndex + 16, currentIndex + 16 + device_name_length)); const room_id = decodedPayload[currentIndex + 16 + device_name_length]; devices[rf_address] = { device_type: device_type, rf_address: rf_address, serialnumber: serialnumber, device_name: device_name, room_id: room_id }; currentIndex = currentIndex + 16 + device_name_length; } } return { rooms: rooms, devices: devices }; } function parseCommandConfiguration(payload, log) { /* Start Length Value Description ================================================================== 00 1 D2 Length of data: D2 = 210(decimal) = 210 bytes 01 3 003508 RF address 04 1 01 Device Type 05 3 0114FF ? 08 10 IEQ0109125 Serial Number 18 1 28 Comfort Temperature 19 1 28 Eco Temperature 20 1 3D MaxSetPointTemperature 21 1 09 MinSetPointTemperature 22 1 07 Temperature Offset * 2 The default value is 3,5, which means the offset = 0 degrees. The offset is adjustable between -3,5 and +3,5 degrees, which results in a value in this response between 0 and 7 (decoded already) 23 1 28 Window Open Temperature 24 1 03 Window Open Duration 25 1 30 Boost Duration and Boost Valve Value The 3 MSB bits gives the duration, the 5 LSB bits the Valve Value%. Duration: With 3 bits, the possible values (Dec) are 0 to 7, 0 is not used. The duration in Minutes is: if Dec value = 7, then 30 minutes, else Dec value * 5 minutes Valve Value: dec value 5 LSB bits * 5 gives Valve Value in % 26 1 0C Decalcification: Day of week and Time In bits: DDDHHHHH The three most significant bits (MSB) are presenting the day, Saturday = 1, Friday = 7 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) */ const payloadArr = payload.split(','); // const rf_address = payloadArr[0].slice(0, 6).toString('hex'); const decodedPayload = new Buffer(payloadArr[1], 'base64'); // const length = decodedPayload[0]; return { rf_address: decodedPayload.slice(1, 4).toString('hex'), device_type: decodedPayload[4], serial_number: String.fromCharCode.apply(null, decodedPayload.slice(8, 18)), comfort_temp: decodedPayload[18] / 2, eco_temp: decodedPayload[19] / 2, max_setpoint_temp: decodedPayload[20] / 2, min_setpoint_temp: decodedPayload[21] / 2, temp_offset: (decodedPayload[22] / 2) - 3.5, max_valve: decodedPayload[27] * (100 / 255) }; } function parseCommandDeviceList(payload, log) { const dataObj = []; let decodedPayload = new Buffer(payload, 'base64'); while (decodedPayload.length > 0) { if (decodedPayload.length >= decodedPayload[0]) { const rf_address = decodedPayload.slice(1, 4).toString('hex'); const deviceStatus = decodeDevice.call(this, decodedPayload, log); dataObj.push(deviceStatus); decodedPayload = decodedPayload.slice(decodedPayload[0] + 1); } } return dataObj; } function parseCommandSendDevice(payload, log) { const payloadArr = payload.split(','); return { accepted: payloadArr[1] === '0', duty_cycle: parseInt(payloadArr[0], 16), free_memory_slots: parseInt(payloadArr[2], 16) }; } function parseCommandAcknowledge () { return true; } function decodeDevice(payload, log) { let deviceStatus = {}; let deviceType = undefined; switch (payload[0]) { case 6: if (payload[1] === 9) { deviceType = EQ3MAX_DEV_TYPE_PUSH_BUTTON; deviceStatus = decodeDeviceButton(payload, log); } else { deviceType = EQ3MAX_DEV_TYPE_SHUTTER_CONTACT; deviceStatus = decodeDeviceContact(payload, log); } break; case 8: deviceType = EQ3MAX_DEV_TYPE_PUSH_BUTTON; deviceStatus = decodeDeviceButton(payload, log); break; case 11: deviceType = EQ3MAX_DEV_TYPE_THERMOSTAT; deviceStatus = decodeDeviceThermostat(payload, log); break; case 12: deviceType = EQ3MAX_DEV_TYPE_WALLTHERMOSTAT; deviceStatus = decodeDeviceThermostat(payload, log); break; case 13: deviceType = EQ3MAX_DEV_TYPE_THERMOSTAT_PLUS; deviceStatus = decodeDeviceThermostat(payload, log); break; // ??? if 13 is correct default: deviceType = EQ3MAX_DEV_TYPE_UNKNOWN; break; } deviceStatus.rf_address = payload.slice(1, 4).toString('hex'); return deviceStatus; } function decodeDeviceContact(payload, log) { return { rf_address: payload.slice(1, 4).toString('hex'), initialized: !!(payload[5] & (1 << 1)), fromCmd: !!(payload[5] & (1 << 2)), error: !!(payload[5] & (1 << 3)), valid: !!(payload[5] & (1 << 4)), dst_active: !!(payload[6] & (1 << 3)), gateway_known: !!(payload[6] & (1 << 4)), link_error: !!(payload[6] & (1 << 6)), battery_low: !!(payload[6] & (1 << 7)), opened: !!(payload[6] & (1 << 1)) }; } function decodeDeviceButton(payload, log) { return { rf_address: payload.slice(1, 4).toString('hex'), initialized: !!(payload[5] & (1 << 1)), fromCmd: !!(payload[5] & (1 << 2)), error: !!(payload[5] & (1 << 3)), valid: !!(payload[5] & (1 << 4)), dst_active: !!(payload[6] & (1 << 3)), gateway_known: !!(payload[6] & (1 << 4)), link_error: !!(payload[6] & (1 << 6)), battery_low: !!(payload[6] & (1 << 7)), eco_mode: !!(payload[6] & (1 << 1)) }; } function decodeDeviceThermostat(payload, log) { /* source: http://www.domoticaforum.eu/viewtopic.php?f=66&t=6654 Start Length Value Description ================================================================== 0 1 0B Length of data: 0B = 11(decimal) = 11 bytes 1 3 003508 RF address 4 1 00 ? 5 1 12 bit 4 Valid 0=invalid;1=information provided is valid bit 3 Error 0=no; 1=Error occurred bit 2 Answer 0=an answer to a command,1=not an answer to a command bit 1 Status initialized 0=not initialized, 1=yes 12 = 00010010b = Valid, Initialized 6 1 1A bit 7 Battery 1=Low bit 6 Linkstatus 0=OK, 1=error bit 5 Panel 0=unlocked, 1=locked bit 4 Gateway 0=unknown, 1=known bit 3 DST setting 0=inactive, 1=active bit 2 Not used bit 1,0 Mode 00=auto/week schedule 01=Manual 10=Vacation 11=Boost 1A = 00011010b = Battery OK, Linkstatus OK, Panel unlocked, Gateway known, DST active, Mode Vacation. 7 1 20 Valve position in % 8 1 2C Temperature setpoint, 2Ch = 44d; 44/2=22 deg. C 9 2 858B Date until (05-09-2011) (see Encoding/Decoding date/time) B 1 2E Time until (23:00) (see Encoding/Decoding date/time) */ let mode = 'AUTO'; if ((payload[6] & 3) === 3) { mode = 'BOOST'; } else if (payload[6] & (1 << 0)) { mode = 'MANUAL'; } else if (payload[6] & (1 << 1)) { mode = 'VACATION'; } const deviceStatus = { rf_address: payload.slice(1, 4).toString('hex'), initialized: !!(payload[5] & (1 << 1)), fromCmd: !!(payload[5] & (1 << 2)), error: !!(payload[5] & (1 << 3)), valid: !!(payload[5] & (1 << 4)), mode: mode, dst_active: !!(payload[6] & (1 << 3)), gateway_known: !!(payload[6] & (1 << 4)), panel_locked: !!(payload[6] & (1 << 5)), link_error: !!(payload[6] & (1 << 6)), battery_low: !!(payload[6] & (1 << 7)), valve: payload[7], setpoint: (payload[8] & 0x7F) / 2 }; /*if (mode === 'VACATION') { // from http://sourceforge.net/p/fhem/code/HEAD/tree/trunk/fhem/FHEM/10_MAX.pm#l573 deviceStatus.date_until = 2000 + (payload[10] & 0x3F) + '-' + ('00' + (((payload[9] & 0xE0) >> 4) | (payload[10] >> 7))).substr(-2) + '-' + ('00' + (payload[9] & 0x1F)).substr(-2); const hours = (payload[11] & 0x3F) / 2; deviceStatus.time_until = ('00' + Math.floor(hours)).substr(-2) + ':' + ((hours % 1) ? '30' : '00'); } else { deviceStatus.temp = (payload[9] ? 25.5 : 0) + payload[10] / 10; }*/ log && log.debug('Raw payload: ' + payload.join(', ')); const hours = (payload[11] & 0x3F) / 2; deviceStatus.time_until = ('00' + Math.floor(hours)).substr(-2) + ':' + ((hours % 1) ? '30' : '00'); if (payload[0] === 12) { deviceStatus.date_until = 2000 + (payload[10] & 0x3F) + '-' + ('00' + (((payload[9] & 0xE0) >> 4) | (payload[10] >> 7))).substr(-2) + '-' + ('00' + (payload[9] & 0x1F)).substr(-2); deviceStatus.temp = ((payload[8] & 0x80) ? 25.6 : 0) + payload[12] / 10; } else { deviceStatus.temp = (payload[9] ? 25.6 : 0) + payload[10] / 10; } return deviceStatus; } exports.parse = parse;