maxcube2
Version:
eq-3 Max! Cube interface for homebridge-platform-maxcube
402 lines (342 loc) • 15.4 kB
JavaScript
// Device types
var EQ3MAX_DEV_TYPE_CUBE = 0;
var EQ3MAX_DEV_TYPE_THERMOSTAT = 1;
var EQ3MAX_DEV_TYPE_THERMOSTAT_PLUS = 2;
var EQ3MAX_DEV_TYPE_WALLTHERMOSTAT = 3;
var EQ3MAX_DEV_TYPE_SHUTTER_CONTACT = 4;
var EQ3MAX_DEV_TYPE_PUSH_BUTTON = 5;
var EQ3MAX_DEV_TYPE_WINDOW_SWITCH = 6;
var EQ3MAX_DEV_TYPE_UNKNOWN = 99;
const StringDecoder = require('string_decoder').StringDecoder;
const stringDecoder = new StringDecoder('utf8');
function parse (commandType, payload) {
switch (commandType) {
case 'H':
return parseCommandHello.call(this, payload);
break;
case 'M':
return parseCommandMetadata.call(this, payload);
break;
case 'C':
return parseCommandConfiguration.call(this, payload);
break;
case 'L':
return parseCommandDeviceList.call(this, payload);
break;
case 'S':
return parseCommandSendDevice.call(this, payload);
break;
case 'A':
return parseCommandAcknowledge.call(this, payload);
break;
default:
console.error('Unknown command type: ' + commandType);
}
}
var decodeStringPayload = function (charArray) {
return stringDecoder.write(Buffer.from(charArray));
};
function parseCommandHello (payload) {
var payloadArr = payload.split(",");
var dataObj = {
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],
};
return dataObj;
}
function parseCommandMetadata (payload) {
var payloadArr = payload.split(",");
var decodedPayload = Buffer.from(payloadArr[2], 'base64');
var room_count = decodedPayload[2];
var currentIndex = 3;
var rooms = {};
var devices = {};
// parse rooms
for (var i = 0; i < room_count; i++) {
var room_id = decodedPayload[currentIndex];
var room_name_length = decodedPayload[currentIndex + 1];
var room_name = decodeStringPayload(decodedPayload.slice(currentIndex + 2, currentIndex + 2 + room_name_length));
var group_rf_address = decodedPayload.slice(currentIndex + 2 + room_name_length, currentIndex + room_name_length + 5).toString('hex');
var roomData = {
room_id: room_id,
room_name: room_name,
group_rf_address: group_rf_address
};
rooms[room_id] = roomData;
currentIndex = currentIndex + room_name_length + 5;
};
// parse devices
if (currentIndex < decodedPayload.length) {
var device_count = decodedPayload[currentIndex];
for (var i = 0; i < device_count; i++) {
var device_type = decodedPayload[currentIndex + 1];
var rf_address = decodedPayload.slice(currentIndex + 2, currentIndex + 5).toString('hex');
var serialnumber = decodedPayload.slice(currentIndex + 5, currentIndex + 15).toString();
var device_name_length = decodedPayload[currentIndex + 15];
var device_name = decodeStringPayload(decodedPayload.slice(currentIndex + 16, currentIndex + 16 + device_name_length));
var room_id = decodedPayload[currentIndex + 16 + device_name_length];
var deviceData = {
device_type: device_type,
rf_address: rf_address,
serialnumber: serialnumber,
device_name: device_name,
room_id: room_id,
};
devices[rf_address] = deviceData;
currentIndex = currentIndex + 16 + device_name_length;
}
}
return { rooms: rooms, devices: devices };
}
function parseCommandConfiguration (payload) {
/*
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 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');
var decodedPayload = Buffer.from(payloadArr[1], 'base64');
var length = decodedPayload[0];
var dataObj = {
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)
};
try {
if(dataObj.device_type == 1 || dataObj.device_type == 2){
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;
}
function parseCommandDeviceList (payload) {
var dataObj = [];
var decodedPayload = Buffer.from(payload, 'base64');
while (decodedPayload.length > 0) {
if (decodedPayload.length >= decodedPayload[0]) {
var rf_address = decodedPayload.slice(1, 4).toString('hex');
var deviceStatus = decodeDevice.call(this, decodedPayload);
dataObj.push(deviceStatus);
decodedPayload = decodedPayload.slice(decodedPayload[0] + 1);
}
};
return dataObj;
}
function parseCommandSendDevice (payload) {
var payloadArr = payload.split(",");
var dataObj = {
accepted: payloadArr[1] === '0',
duty_cycle: parseInt(payloadArr[0], 16),
free_memory_slots: parseInt(payloadArr[2], 16)
};
return dataObj;
}
function parseCommandAcknowledge () {
return true;
}
function decodeDevice (payload) {
var deviceStatus = {};
var deviceType = undefined;
switch (payload[0]) {
case 6: deviceType = EQ3MAX_DEV_TYPE_WINDOW_SWITCH; deviceStatus = decodeDeviceWindowSwitch (payload); break;
case 8: deviceType = EQ3MAX_DEV_TYPE_PUSH_BUTTON; break;
case 11: deviceType = EQ3MAX_DEV_TYPE_THERMOSTAT; deviceStatus = decodeDeviceThermostat (payload); break;
case 12: deviceType = EQ3MAX_DEV_TYPE_WALLTHERMOSTAT; deviceStatus = decodeDeviceWallThermostat (payload); break;
default: deviceType = EQ3MAX_DEV_TYPE_UNKNOWN; break;
}
deviceStatus.rf_address = payload.slice(1, 4).toString('hex');
return deviceStatus;
}
function decodeDeviceWindowSwitch (payload) {
/*
According to https://github.com/Bouni/max-cube-protocol/blob/master/L-Message.md the information about
the window status is mapped in the lowest two bits in the flag word.
*/
var open = false;
if ((payload[6] & (1 << 1)) > 0) {
open = true;
}
var deviceStatus = {
rf_address: payload.slice(1, 4).toString('hex'),
open: open,
};
return deviceStatus;
}
function decodeDeviceThermostat (payload) {
/*
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)
*/
var 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';
}
var 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] / 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);
var 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;
}
return deviceStatus;
}
function decodeDeviceWallThermostat (payload) {
//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
*/
//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;
}
exports.parse = parse;