Vicki
Vicki is a smart thermostatic radiator valve with many features. Those include a temperature & humidity sensor, automatic temperature control algorithm open window detection and much more.
Last updated
Vicki is a smart thermostatic radiator valve with many features. Those include a temperature & humidity sensor, automatic temperature control algorithm open window detection and much more.
Last updated
in manual | dataKey | dataType | dataFormat | sampleValue |
---|---|---|---|---|
SENSOR_TEMPERATURE | temperature | Float | telemetry | 23,06 |
TARGET_TEMPERATURE | targetTemperature | Float | telemetry | 21 |
RELATIVE_HUMIDITY | humidity | Float | telemetry | 45,7 |
MOTOR_RANGE | motorRange | Float | telemetry | 432 |
MOTOR_POSITION | motorPosition | Float | telemetry | 432 |
BATTERY_VOLTAGE | battery | Float | telemetry | 3,4 |
OPEN_WINDOW | openWindow | Boolean | telemetry | False |
CHILD_LOCK | childLock | Boolean | telemetry | False |
HIGH_MOTOR_CONSUMPTION | hightMotorConsumption | Boolean | telemetry | False |
LOW_MOTOR_CONSUMPTION | lowMotorConsumption | Boolean | telemetry | False |
BROKEN_SENSOR | brokenSensor | Boolean | telemetry | False |
LORA_RSSI | rssi | Integer | telemetry | -110 |
LORA_SNR | snr | Integer | telemetry | -15 |
LORA_DATARATE | dr | String | telemetry | |
REASON | reason | Integer | telemetry | 81 |
var decodedMsg = msg;
if (msg && msg.data) {
decodedMsg = decodeUplink(msg.data)
}
if (msg && msg.rssi) {
decodedMsg.rssi = msg.rssi
}
if (msg && msg.snr) {
decodedMsg.snr = msg.snr
}
if (msg && msg.dr) {
decodedMsg.dr = msg.dr
}
return {
msg: decodedMsg,
metadata: metadata,
msgType: msgType
};
// console.log(decodedMsg)
function decodeUplink(input) {
function format(value) {
var bytes = value.replaceAll(' ', '').toLowerCase();
var byteArr = []
for (var i = 0; i < bytes.length; i+=2) {
byteArr.push(bytes[i] + "" + bytes[i + 1])
}
bytes = byteArr.map(function(res, i){
return Number('0x' +res);
});
return bytes;
}
var bytes = format(input);
var data = {};
var resultToPass = {};
toBool = function (value) { return value == '1' };
function merge_obj(obj1, obj2) {
var obj3 = {};
for (var attrname in obj1) { obj3[attrname] = obj1[attrname]; }
for (var attrname2 in obj2) { obj3[attrname2] = obj2[attrname2]; }
return obj3;
}
function handleKeepalive(bytes, data){
tmp = ("0" + bytes[6].toString(16)).substr(-2);
motorRange1 = tmp[1];
motorRange2 = ("0" + bytes[5].toString(16)).substr(-2);
motorRange = parseInt("0x" + motorRange1 + motorRange2, 16);
motorPos2 = ("0" + bytes[4].toString(16)).substr(-2);
motorPos1 = tmp[0];
motorPosition = parseInt("0x" + motorPos1 + motorPos2, 16);
batteryTmp = ("0" + bytes[7].toString(16)).substr(-2)[0];
batteryVoltageCalculated = 2 + parseInt("0x" + batteryTmp, 16) * 0.1;
function decbin(number) {
if (number < 0) {
number = 0xFFFFFFFF + number + 1
}
number = number.toString(2);
return "00000000".substr(number.length) + number;
}
byte7Bin = decbin(bytes[8]);
openWindow = byte7Bin[4];
highMotorConsumption = byte7Bin[5];
lowMotorConsumption = byte7Bin[6];
brokenSensor = byte7Bin[7];
byte8Bin = decbin(bytes[8]);
childLock = byte8Bin[0];
calibrationFailed = byte8Bin[1];
attachedBackplate = byte8Bin[2];
perceiveAsOnline = byte8Bin[3];
var sensorTemp = 0;
if (Number(bytes[0].toString(16)) == 1) {
sensorTemp = (bytes[2] * 165) / 256 - 40;
}
if (Number(bytes[0].toString(16)) == 81) {
sensorTemp = (bytes[2] - 28.33333) / 5.66666;
}
data.reason = Number(bytes[0].toString(16));
data.targetTemperature = Number(bytes[1]);
data.temperature = Number(sensorTemp.toFixed(2));
data.humidity = Number(((bytes[3] * 100) / 256).toFixed(2));
data.motorRange = motorRange;
data.motorPosition = motorPosition;
data.battery = Number(batteryVoltageCalculated.toFixed(2));
data.openWindow = toBool(openWindow);
data.hightMotorConsumption = toBool(highMotorConsumption);
data.lowMotorConsumption = toBool(lowMotorConsumption);
data.brokenSensor = toBool(brokenSensor);
data.childLock = toBool(childLock);
data.calibrationFailed = toBool(calibrationFailed);
data.attachedBackplate = toBool(attachedBackplate);
data.perceiveAsOnline = toBool(perceiveAsOnline);
return data;
}
function handleResponse(bytes, data){
var commands = bytes.map(function(byte, i){
return ("0" + byte.toString(16)).substr(-2);
});
commands = commands.slice(0,-9);
var command_len = 0;
commands.map(function (command, i) {
switch (command) {
case '04':
{
command_len = 2;
var hardwareVersion = commands[i + 1];
var softwareVersion = commands[i + 2];
var dataK = { deviceVersions: { hardware: Number(hardwareVersion), software: Number(softwareVersion) } };
resultToPass = merge_obj(resultToPass, dataK);
}
break;
case '12':
{
command_len = 1;
var dataC = { keepAliveTime: parseInt(commands[i + 1], 16) };
resultToPass = merge_obj(resultToPass, dataC);
}
break;
case '13':
{
command_len = 4;
var enabled = toBool(parseInt(commands[i + 1], 16));
var duration = parseInt(commands[i + 2], 16) * 5;
var tmp = ("0" + commands[i + 4].toString(16)).substr(-2);
var motorPos2 = ("0" + commands[i + 3].toString(16)).substr(-2);
var motorPos1 = tmp[0];
var motorPosition = parseInt('0x' + motorPos1 + motorPos2, 16);
var delta = Number(tmp[1]);
var dataD = { openWindowParams: { enabled: enabled, duration: duration, motorPosition: motorPosition, delta: delta } };
resultToPass = merge_obj(resultToPass, dataD);
}
break;
case '14':
{
command_len = 1;
var dataB = { childLock: toBool(parseInt(commands[i + 1], 16)) };
resultToPass = merge_obj(resultToPass, dataB);
}
break;
case '15':
{
command_len = 2;
var dataA = { temperatureRangeSettings: { min: parseInt(commands[i + 1], 16), max: parseInt(commands[i + 2], 16) } };
resultToPass = merge_obj(resultToPass, dataA);
}
break;
case '16':
{
command_len = 2;
var data = { internalAlgoParams: { period: parseInt(commands[i + 1], 16), pFirstLast: parseInt(commands[i + 2], 16), pNext: parseInt(commands[i + 3], 16) } };
resultToPass = merge_obj(resultToPass, data);
}
break;
case '17':
{
command_len = 2;
var dataF = { internalAlgoTdiffParams: { warm: parseInt(commands[i + 1], 16), cold: parseInt(commands[i + 2], 16) } };
resultToPass = merge_obj(resultToPass, dataF);
}
break;
case '18':
{
command_len = 1;
var dataE = { operationalMode: parseInt(commands[i + 1], 16) };
resultToPass = merge_obj(resultToPass, dataE);
}
break;
case '19':
{
command_len = 1;
var commandResponse = parseInt(commands[i + 1], 16);
var periodInMinutes = commandResponse * 5 / 60;
var dataH = { joinRetryPeriod: periodInMinutes };
resultToPass = merge_obj(resultToPass, dataH);
}
break;
case '1b':
{
command_len = 1;
var dataG = { uplinkType: parseInt(commands[i + 1], 16) };
resultToPass = merge_obj(resultToPass, dataG);
}
break;
case '1d':
{
// get default keepalive if it is not available in data
command_len = 2;
var deviceKeepAlive = 5;
var wdpC = commands[i + 1] == '00' ? false : commands[i + 1] * deviceKeepAlive + 7;
var wdpUc = commands[i + 2] == '00' ? false : parseInt(commands[i + 2], 16);
var dataJ = { watchDogParams: { wdpC: wdpC, wdpUc: wdpUc } };
resultToPass = merge_obj(resultToPass, dataJ);
}
break;
case '1f':
{
command_len = 1;
var data = { primaryOperationalMode: commands[i + 1] };
resultToPass = merge_obj(resultToPass, data);
}
break;
case '21':
{
command_len = 6;
var data = {batteryRangesBoundaries:{
Boundary1: parseInt(commands[i + 1] + commands[i + 2], 16),
Boundary2: parseInt(commands[i + 3] + commands[i + 4], 16),
Boundary3: parseInt(commands[i + 5] + commands[i + 6], 16),
}};
resultToPass = merge_obj(resultToPass, data);
}
break;
case '23':
{
command_len = 4;
var data = {batteryRangesOverVoltage:{
Range1: parseInt(commands[i + 2], 16),
Range2: parseInt(commands[i + 3], 16),
Range3: parseInt(commands[i + 4], 16),
}};
resultToPass = merge_obj(resultToPass, data);
}
break;
case '27':
{
command_len = 1;
var data = {OVAC: parseInt(commands[i + 1], 16)};
resultToPass = merge_obj(resultToPass, data);
}
break;
case '28':
{
command_len = 1;
var data = { manualTargetTemperatureUpdate: parseInt(commands[i + 1], 16) };
resultToPass = merge_obj(resultToPass, data);
}
break;
case '29':
{
command_len = 2;
var data = { proportionalAlgoParams: { coefficient: parseInt(commands[i + 1], 16), period: parseInt(commands[i + 2], 16) } };
resultToPass = merge_obj(resultToPass, data);
}
break;
case '2b':
{
command_len = 1;
var data = { algoType: commands[i + 1] };
resultToPass = merge_obj(resultToPass, data);
}
break;
default:
break;
}
commands.splice(i,command_len);
});
return resultToPass;
}
if (bytes[0].toString(16) == 1 || bytes[0].toString(16) == 129) {
data = merge_obj(data, handleKeepalive(bytes, data));
}else{
data = merge_obj(data, handleResponse(bytes, data));
bytes = bytes.slice(-9);
data = merge_obj(data, handleKeepalive(bytes, data));
}
return data;
}