@1st-setup/cul
Version:
Module to interact with Busware CUL / culfw
414 lines (371 loc) • 15.3 kB
JavaScript
'use strict';
var device_types = {
0: "Cube",
1: "HeatingThermostat",
2: "HeatingThermostatPlus",
3: "WallMountedThermostat",
4: "ShutterContact",
5: "PushButton"
};
var msgId2Cmd = {
"00": "PairPing",
"01": "PairPong",
"02": "Ack",
"03": "TimeInformation",
"10": "ConfigWeekProfile",
"11": "ConfigTemperatures", //like eco/comfort etc
"12": "ConfigValve",
"20": "AddLinkPartner",
"21": "RemoveLinkPartner",
"22": "SetGroupId",
"23": "RemoveGroupId",
"30": "ShutterContactState",
"40": "SetTemperature", //to thermostat
"42": "WallThermostatControl", //by WallMountedThermostat
//Sending this without payload to thermostat sets desiredTempeerature to the comfort/eco temperature
//We don't use it, we just do SetTemperature
"43": "SetComfortTemperature",
"44": "SetEcoTemperature",
"50": "PushButtonState",
"60": "ThermostatState", //by HeatingThermostat
"70": "WallThermostatState",
"82": "SetDisplayActualTemperature",
"F1": "WakeUp",
"F0": "Reset",
};
var ctrl_modes = {
'0': "auto",
'1': "manual",
'2': "temporary",
'3': "boost",
};
var day2str = {
0: 'Sat',
1: 'Sun',
2: 'Mon',
3: 'Tue',
4: 'Wed',
5: 'Thu',
6: 'Fri'
};
var seen_devices = {};
function addDeviceByMsgType(addr, msgType) {
switch (msgType) {
case "ShutterContactState":
seen_devices[addr] = device_types[4];
break;
case "WallThermostatConfig":
case "WallThermostatState":
case "WallThermostatControl":
case "SetTemperature":
seen_devices[addr] = device_types[3];
break;
case "HeatingThermostatConfig":
case "ThermostatState":
seen_devices[addr] = device_types[1];
break;
case "PushButtonState":
seen_devices[addr] = device_types[5];
break;
}
}
function hex2byte(hexStr) {
var result = null;
try {
result = ("0x" + hexStr) * 1;
}
catch (err) {
result = null;
}
if (isNaN(result)) {
result = null;
}
return result;
}
function getBits(value, offset, len) {
var mask = 0;
while (len > 0) {
mask = mask << 1;
mask++;
len--;
}
return ((value >> offset) & mask);
}
function MAX_DateTime2Internal(dateObj) {
var result = ((dateObj.month & 0x0E) << 20);
result |= (dateObj.day << 16);
result |= ((dateObj.month & 0x01) << 15);
result |= ((dateObj.year - 2000) << 8);
result |= (dateObj.hour * 2 + (dateObj.min / 30));
return result;
}
function MAX_ParseDateTime(byte1, byte2, byte3) {
//$until = sprintf("%06x",(($month&0xE) << 20) | ($day << 16) | (($month&1) << 15) | (($year-2000) << 8) | ($hour*2 + int($min/30)));
// value = 1111 1111 1111 1111 1111 1111
// byte1 byte2 byte3
// month = 4 bits
// 1110 0000 1000 0000 0000 0000
// day = 5 bits = 1-31 decimal
// 0001 1111 0000 0000 0000 0000
// year = years since 2000 7 bits max of 127 years (2127)
// 0000 0000 0111 1111 0000 0000
// time = 8 bits resolution is 30 minutes.
let value = (byte1 << 16) | (byte2 << 8) | byte3;
var result = {};
result.month = ((value & 0xE00000) >> 20) | ((value & 0x8000) >> 15);
result.day = ((value & 0x1F0000) >> 16);
result.year = ((value & 0x7F0000) > 8) + 2000;
let minutes = (value & 0xFF) * 30; // This is in minutes now.
result.hour = Math.floor(minutes / 60);
result.minute = (minutes - (result.hour * 60));
result.str = result.day + "-" + result.month + "-" + result.year + " " + result.hour + ":" + result.minute;
return result;
}
function MAX_SerializeTemperature(temperature) {
if ((temperature == "on") || (temperature == "off"))
return temperature;
else if (temperature == 4.5)
return "off";
else if (temperature == 30.5)
return "on";
else
return temperature + " C";
}
module.exports.parse = function (raw) {
var message = { data: {} };
var data = message.data;
message.protocol = 'MORITZ';
switch (raw.charAt(0)) {
case "Z":
// Check if we have a Z character and at least two following characters which specify the length
data.len = hex2byte(raw.substr(1, 2));
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
data.error = 'len mismatch';
data.strlen = raw.length;
data.expectedlen = (2 * data.len + 3);
}
else {
data.msgcnt = hex2byte(raw.substr(3, 2));
data.msgFlag = raw.substr(5, 2);
data.msgTypeRaw = raw.substr(7, 2);
data.msgType = msgId2Cmd[data.msgTypeRaw] ? msgId2Cmd[data.msgTypeRaw] : data.msgTypeRaw;
data.src = raw.substr(9, 6).toLowerCase();
message.address = data.src;
data.dst = raw.substr(15, 6).toLowerCase();
data.groupid = hex2byte(raw.substr(21, 2));
data.payload = raw.substr(23, (2 * data.len - 2));
addDeviceByMsgType(data.src, data.msgType);
if (seen_devices[data.src]) message.device = seen_devices[data.src];
if (seen_devices[data.dst]) data.dstDevice = seen_devices[data.dst];
var desiredTemperatureRaw = null;
var measuredTemperature = null;
switch (data.msgType) {
case "Ack":
data.ackResponse = hex2byte(data.payload.substr(0, 2));
if (data.payload.length -4 > 0) {
data.ackPayload = data.payload.substr(2, data.payload.length -4);
}
break;
case "RemoveLinkPartner":
case "AddLinkPartner":
data.linkPartner = {
address: data.payload.substr(0, 6).toLowerCase(),
device: device_types[hex2byte(data.payload.substr(6, 2))]
}
break;
case "PairPing":
// my ($firmware,$type,$testresult,$serial) = unpack("CCCa*",pack("H*",$payload));
data.firmware = hex2byte(data.payload.substr(0, 2));
data.type = hex2byte(data.payload.substr(2, 2));
data.testresult = hex2byte(data.payload.substr(4, 2));
data.serial = data.payload.substr(6);
break;
case "TimeInformation":
if (data.payload.length == 12) {
data.year = 2000 + hex2byte(data.payload.substr(0, 2));
data.day = hex2byte(data.payload.substr(2, 2));
data.hour = hex2byte(data.payload.substr(4, 2)) & 0x1F;
data.min = hex2byte(data.payload.substr(6, 2)) & 0x3F;
data.sec = hex2byte(data.payload.substr(8, 2)) & 0x3F;
data.month = ((hex2byte(data.payload.substr(6, 2)) >> 6) << 2) | (hex2byte(data.payload.substr(8, 2)) >> 6); // this is just quessed according to FHEM
data.unk1 = hex2byte(data.payload.substr(4, 2)) >> 5;
data.unk2 = hex2byte(data.payload.substr(6, 2)) >> 6;
data.unk3 = hex2byte(data.payload.substr(8, 2)) >> 6;
}
break;
case "PairPing":
data.firmware = hex2byte(data.payload.substr(0, 2));
data.msgType = hex2byte(data.payload.substr(2, 2));
data.testresult = hex2byte(data.payload.substr(4, 2));
data.serial = data.payload.substr(6);
if (message.pairPing) {
message.device = device_types[message.pairPing.type];
seen_devices[message.address] = message.device;
}
break;
case "SetTemperature":
var bits = hex2byte(data.payload.substr(0, 2));
data.mode = bits >> 6;
data.modeStr = ctrl_modes[data.mode] ? ctrl_modes[data.mode] : data.mode;
data.desiredTemperature = (bits & 0x3F) / 2.0; // Convert to degree celcius.
if (data.desiredTemperature == 0) {
delete data.desiredTemperature;
}
break;
case "ConfigTemperatures":
// my $comfort = int($h{comfortTemperature}*2);
// my $eco = int($h{ecoTemperature}*2);
// my $max = int($h{maximumTemperature}*2);
// my $min = int($h{minimumTemperature}*2);
// my $offset = int(($h{measurementOffset} + 3.5)*2);
// my $windowOpenTemp = int($h{windowOpenTemperature}*2);
// my $windowOpenTime = int($h{windowOpenDuration}/5);
// my $groupid = MAX_ReadingsVal($hash,"groupid");
// my $payload = sprintf("%02x%02x%02x%02x%02x%02x%02x",$comfort,$eco,$max,$min,$offset,$windowOpenTemp,$windowOpenTime);
data.comfortTemperature = hex2byte(data.payload.substr(0, 2)) / 2;
data.ecoTemperature = hex2byte(data.payload.substr(2, 2)) / 2;
data.maximumTemperature = hex2byte(data.payload.substr(4, 2)) / 2;
data.minimumTemperature = hex2byte(data.payload.substr(6, 2)) / 2;
data.offset = (hex2byte(data.payload.substr(8, 2)) / 2) - 3.5;
data.windowOpenTemperature = hex2byte(data.payload.substr(10, 2)) / 2;
data.windowOpenTime = hex2byte(data.payload.substr(12, 2)) * 5;
break;
case "WallThermostatControl":
desiredTemperatureRaw = hex2byte(data.payload.substr(0, 2));
measuredTemperature = hex2byte(data.payload.substr(2, 2));
break;
case "WallThermostatState":
var bits2 = hex2byte(data.payload.substr(0, 2));
data.displayActualTemperature = hex2byte(data.payload.substr(2, 2));
desiredTemperatureRaw = hex2byte(data.payload.substr(4, 2));
var null1 = hex2byte(data.payload.substr(6, 2));
var heaterTemperature = hex2byte(data.payload.substr(8, 2));
var null2 = hex2byte(data.payload.substr(10, 2));
measuredTemperature = hex2byte(data.payload.substr(12, 2));
data.mode = getBits(bits2, 0, 2);
data.modeStr = ctrl_modes[data.mode] ? ctrl_modes[data.mode] : data.mode;
data.dstsetting = getBits(bits2, 3, 1); //is automatically switching to DST activated
data.langateway = getBits(bits2, 4, 1); //??
data.panel = getBits(bits2, 5, 1); //1 if the heating thermostat is locked for manually setting the temperature at the device
data.rferror = getBits(bits2, 6, 1); //communication with link partner (what does that mean?)
data.batterylow = getBits(bits2, 7, 1); //1 if battery is low
data.battery = data.batterylow ? "low" : "ok";
if ((null2 !== null) && (null1 > 0) || (null2 > 0)) {
data.untilStr = MAX_ParseDateTime(null1, heaterTemperature, null2);
heaterTemperature = null;
}
if (heaterTemperature !== null) data.heaterTemperature = heaterTemperature;
break;
case "ThermostatState":
var bits2 = hex2byte(data.payload.substr(0, 2));
data.valveposition = hex2byte(data.payload.substr(2, 2));
var desiredTemperatureRaw = hex2byte(data.payload.substr(4, 2));
var until1 = hex2byte(data.payload.substr(6, 2));
var until2 = hex2byte(data.payload.substr(8, 2));
var until3 = hex2byte(data.payload.substr(10, 2));
data.mode = getBits(bits2, 0, 2);
data.modeStr = ctrl_modes[data.mode] ? ctrl_modes[data.mode] : data.mode;
data.dstsetting = getBits(bits2, 3, 1); //is automatically switching to DST activated
data.langateway = getBits(bits2, 4, 1); //??
data.panel = getBits(bits2, 5, 1); //1 if the heating thermostat is locked for manually setting the temperature at the device
data.rferror = getBits(bits2, 6, 1); //communication with link partner (what does that mean?)
data.batterylow = getBits(bits2, 7, 1); //1 if battery is low
data.battery = data.batterylow ? "low" : "ok";
if (until3 > 0) data.untilStr = MAX_ParseDateTime(until1, until2, until3);
// var measuredTemperature = (until2 > 0) ? (((until1 & 0x01)<<8) + until2)/10 : 0;
var measuredTemperature = (until2 > 0) ? (((until1 & 0x01) << 8) + until2) : 0;
//If the control mode is not "temporary", the cube sends the current (measured) temperature
if ((data.mode == 2) || (measuredTemperature == 0)) measuredTemperature = null;
if (data.mode != 2) delete data.untilStr;
if (measuredTemperature) data.measuredTemperature = measuredTemperature;
break;
case "ShutterContactState":
var bits = hex2byte(data.payload.substr(0, 2));
data.isopen = (getBits(bits, 0, 2) === 0) ? 0 : 1;
data.unkbits = getBits(bits, 2, 4);
data.rferror = getBits(bits, 6, 1);
data.batterylow = getBits(bits, 7, 1);
data.battery = data.batterylow ? "low" : "ok";
break;
case "PushButtonState":
var bits2 = hex2byte(data.payload.substr(0, 2));
data.onoff = data.payload.substr(2, 2);
//The meaning of $bits2 is completly guessed based on similarity to other devices, TODO: confirm
data.gateway = getBits(bits2, 4, 1); // Paired to a CUBE?
data.rferror = getBits(bits2, 6, 1);
data.batterylow = getBits(bits2, 7, 1);
data.battery = data.batterylow ? "low" : "ok";
break;
/*
if($device_types{$type} =~ /HeatingThermostat./) {
Dispatch($shash, "MAX,$isToMe,HeatingThermostatConfig,$src,17,21,30.5,4.5,$defaultWeekProfile,80,5,0,12,15,100,0,0,12", {});
} elsif($device_types{$type} eq "WallMountedThermostat") {
Dispatch($shash, "MAX,$isToMe,WallThermostatConfig,$src,17,21,30.5,4.5,$defaultWeekProfile,80,5,0,12", {});
}
*/
case "ConfigWeekProfile":
// example payload: 0 2 3C48 4860 48AE 4908 3D20 4520 4520 0 8
// $newWeekprofilePart .= sprintf("%04x", (int($temperature*2) << 9) | int(($hour * 60 + $min)/5));
// First 7 control points sprintf("0%1d%s", $day, substr($newWeekprofilePart,0,2*2*7))
// Remaining 6 control points sprintf("1%1d%s", $day, substr($newWeekprofilePart,2*2*7,2*2*6))
// First digit specifies which set of control points:
// 0 : first 7
// 1 : remaining 6
// Second digit is weekday
// (0 => 'Sat', 1 => 'Sun', 2 => 'Mon', 3 => 'Tue', 4 => 'Wed', 5 => 'Thu', 6 => 'Fri');
//
// Next 7 or 6 2 byte hex values are controlpoints
// Last two digits are unknown for now ??
data.setId = data.payload.substr(0, 1);
let controlpointCount = (data.setId == 0) ? 7 : 6;
if (data.payload.length >= (2 + controlpointCount * 4)) {
data.weekday = data.payload.substr(1, 1);
data.weekdayStr = day2str[data.weekday];
data.controlpoints = [];
let previousHour = 0;
let previousMinute = 0;
for (var i = 0; i < controlpointCount; i++) {
let controlpoint = hex2byte(data.payload.substr(2 + (i * 4), 4));
let temperature = ((controlpoint >> 9) & 0x3F) / 2;
let time = (controlpoint & 0x1FF) * 5;
let hour = Math.floor((time / 60) % 24);
let minute = time % 60;
data.controlpoints.push({
temperature: temperature,
hour: previousHour,
minute: previousMinute
});
if (hour === 0 && minute === 0) {
// Last point.
break;
}
previousHour = hour;
previousMinute = minute;
}
}
else {
data.error = 'Not enough data for week profile';
data.expectedlen = (2 + controlpointCount * 4);
}
break;
case "HeatingThermostatConfig":
case "WallThermostatConfig":
break;
}
if (desiredTemperatureRaw !== null) data.desiredTemperature = (desiredTemperatureRaw & 0x7F) / 2.0;
if (measuredTemperature !== null) data.measuredTemperature = (((desiredTemperatureRaw & 0x80) << 1) + measuredTemperature) / 10;
}
break;
case "V":
// Should be followed by version of culfw.
var version = raw.split(' ');
data.culfw = {};
data.culfw.version = version[1];
data.culfw.hardware = version[2]
break;
}
return message;
};
module.exports.cmd = function () {
return false;
};