In the past months I’ve invested a lot of hours to reverse engineer the remote control interface of two heaters from the manufacturer JollyMec and I would like to share my solution to control those heaters. I found two ways to control the heaters. Please be aware that the second method requires wiring to the heater and can damage the elecronics of your heater permanently. I will not take any responsibility for damages to your heater!
I’ve uploaded the code for both methods to GitHub: https://github.com/TheNetStriker/JollyMecApi
Please read the readme on Github on how to use the Python script and for information about how to wire the heaters and setup an Teensy Arduino to control the heaters directly. Here I will only share the rules and items required in Openhab to use those interfaces.
Python script:
This method should be save, but only works for newer heaters. It will also require the official wifi module from JollyMec and an active internet connection. Also sometimes the JollyMec servers are down. Please read the GitHub readme about the requirements. Here are the rules and items that I use to control the heater using this script:
Items:
Switch SW_JollyMec_Cellar_OnOff "Odette on-off [%s]" (G_Heating)
Number NUM_JollyMec_Cellar_Power "Odette power [%d]" (G_Heating)
Rules:
var String commandLineBase = "/usr/bin/python /etc/openhab2/scripts/jollymec.py"
rule "JollyMec cellar on/off"
when
Item SW_JollyMec_Cellar_OnOff changed
then
var commandLine = commandLineBase + " set_heater_on_off"
if (SW_JollyMec_Cellar_OnOff.state == ON) {
commandLine = commandLine + " on"
} else {
commandLine = commandLine + " off"
}
var String strOutput = executeCommandLine(commandLine, 20000)
//logError("JollyMec cellar on/off", strOutput)
if (strOutput != "OK") {
logError("JollyMec cellar on/off", strOutput)
sendMail("test@test.com", "Error JollyMec cellar on/off", strOutput)
}
end
rule "JollyMec cellar power"
when
Item NUM_JollyMec_Cellar_Power changed
then
var commandLine = commandLineBase + " set_power"
var Number power = (NUM_JollyMec_Cellar_Power.state as DecimalType).intValue
if (power > -1 && power < 5)
{
commandLine = commandLine + " " + power.toString()
var String strOutput = executeCommandLine(commandLine, 20000)
if (strOutput != "OK") {
logError("JollyMec cellar power", strOutput)
}
}
end
rule "JollyMec get_state"
when
Time cron "0 0/5 * * * ?"
then
var commandLine = commandLineBase + " get_state"
var String strOutput = executeCommandLine(commandLine, 5000)
val String airTemperature = transform("JSONPATH", "$.airTemperature", strOutput)
//do something with the value
end
Serial interface:
This method requires wiring, soldering and a Teensy 3.2 and can damage your heater if you connect the wrong pins! There is some serious voltage on some of the pins that can damage the electronics. I myself damaged two electronic boards trying to reverse engineer this. Please read the readme on GitHub on how to setup the Arduino and about the wiring to the heater.
This method also requires a modified version of the serial addon. I’ve uploaded my changes here and I will create a pull request to merge this into the main version for this soon.
Here are the rules I’am using in Openhab to control the two heaters using the Arduino:
Items:
String ST_JollyMec_Arduino "JollyMec Arduino" (G_System) { serial="/dev/ttyArduino@9600,BASE64(),CHARSET(ISO-8859-1)" }
Switch SW_JollyMec_Living_OnOff "Foghet on-off [%s]" (G_Heating)
Number NUM_JollyMec_Living_State "Foghet state [%d]" (G_Heating)
Number NUM_JollyMec_Living_Power "Foghet power [%d]" (G_Heating)
Number NUM_JollyMec_Living_SmokeTemp "Foghet smoke temp. [%d °C]" (G_Heating)
Number NUM_JollyMec_Living_AmbientTemp "Foghet ambient temp. [%d °C]" (G_Heating)
Number NUM_JollyMec_Living_VentilationLevel "Foghet circulating fan level [%d]" (G_Heating)
Switch SW_JollyMec_Living_Standby "Foghet standby [%s]" (G_Heating)
Number NUM_JollyMec_Living_Mode "Foghet mode [MAP(jollymec_mode.map):%s]" (G_Heating)
Number NUM_JollyMec_Living_PelletLevel "Foghet pellet level [%.0f %%]" (G_Heating)
Switch SW_JollyMec_Cellar_OnOff "Odette on-off [%s]" (G_Heating)
Number NUM_JollyMec_Cellar_State "Odette state [%d]" (G_Heating)
Number NUM_JollyMec_Cellar_Power "Odette power [%d]" (G_Heating)
Number NUM_JollyMec_Cellar_SmokeTemp "Odette smoke temp. [%d °C]" (G_Heating)
Switch SW_JollyMec_Cellar_Standby "Odette standby [%s]" (G_Heating)
Number NUM_JollyMec_Cellar_PelletLevel "Odette pellet level [%.0f %%]" (G_Heating)
Switch SW_JollyMec_ForceStateUpdate "Odette force state update" (G_Heating)
jollymec_mode.map:
0=Wood
1=Pellet
NULL=Not initialized
-=Not initialized
Rules:
import java.util.List
import java.util.Arrays;
import javax.xml.bind.DatatypeConverter;
var boolean debug = false
val Functions.Function2<List<Byte>, Integer, Integer> calculateModulo256 = [
List<Byte> bytes, Integer count |
var int checksum;
for (var i=0; i < count; i++) {
checksum += bytes.get(i) % 256
}
return checksum;
]
rule "JollyMec living on/off"
when
Item SW_JollyMec_Living_OnOff received command
then
if (SW_JollyMec_Living_OnOff.state == ON) {
ST_JollyMec_Arduino.sendCommand("\u0001\u0000\u0001\u0003");
} else {
ST_JollyMec_Arduino.sendCommand("\u0001\u0000\u0000\u0002");
}
end
rule "JollyMec living power"
when
Item NUM_JollyMec_Living_Power received command
then
var byte power = (NUM_JollyMec_Living_Power.state as DecimalType).intValue.byteValue
if (power > 0 && power < 6)
{
val List<Byte> command = "\u0001\u0001\u0000\u0000".getBytes()
command.set(2, power)
command.set(3, calculateModulo256.apply(command, 3).byteValue)
if (debug)
{
val StringBuilder sb1 = new StringBuilder
command.forEach[b | sb1.append(String::format("%02X", b) + ":")]
logError("JollyMec living power", sb1.toString)
}
ST_JollyMec_Arduino.sendCommand(new String(command));
}
end
rule "JollyMec cellar on/off"
when
Item SW_JollyMec_Cellar_OnOff received command
then
if (SW_JollyMec_Cellar_OnOff.state == ON) {
ST_JollyMec_Arduino.sendCommand("\u0002\u0000\u0001\u0003");
} else {
ST_JollyMec_Arduino.sendCommand("\u0002\u0000\u0000\u0002");
}
end
rule "JollyMec cellar power"
when
Item NUM_JollyMec_Cellar_Power received command
then
var byte power = (NUM_JollyMec_Cellar_Power.state as DecimalType).intValue.byteValue
if (power > -1 && power < 6)
{
val List<Byte> command = "\u0002\u0001\u0000\u0000".getBytes()
command.set(2, power)
command.set(3, calculateModulo256.apply(command, 3).byteValue)
if (debug)
{
val StringBuilder sb1 = new StringBuilder
command.forEach[b | sb1.append(String::format("%02X", b) + ":")]
logError("JollyMec cellar power", sb1.toString)
}
ST_JollyMec_Arduino.sendCommand(new String(command));
}
end
rule "JollyMec force status update"
when
Item SW_JollyMec_ForceStateUpdate changed to ON or
Time cron "0 0/5 * * * ?"
then
ST_JollyMec_Arduino.sendCommand("\u0000\u0001\u0001\u0002");
SW_JollyMec_ForceStateUpdate.postUpdate(OFF)
end
rule "JollyMec status"
when
Item ST_JollyMec_Arduino changed
then
var String status = ST_JollyMec_Arduino.state.toString
var byte[] statusBytes = DatatypeConverter::parseBase64Binary(status)
if (debug)
{
val StringBuilder sb1 = new StringBuilder
statusBytes.forEach[b | sb1.append(String::format("%02X", b) + ":")]
//logError("JollyMec status incoming string", ST_JollyMec_Arduino.state.toString)
logError("JollyMec status incoming bytes", sb1.toString)
}
for (var i=0; i < statusBytes.length / 4; i++) {
var int offset = i * 4
var byte [] subStatusBytes = Arrays.copyOfRange(statusBytes, offset, offset + 4);
var int deviceId = Byte.toUnsignedInt(subStatusBytes.get(0))
var int type = Byte.toUnsignedInt(subStatusBytes.get(1))
var int value = Byte.toUnsignedInt(subStatusBytes.get(2))
var int checksum = Byte.toUnsignedInt(subStatusBytes.get(3))
var int checksumValidation = Byte.toUnsignedInt(calculateModulo256.apply(subStatusBytes, 3).byteValue)
if (checksumValidation != checksum)
{
val StringBuilder sb2 = new StringBuilder
subStatusBytes.forEach[b | sb2.append(String::format("%02X", b) + ":")]
var String debugString = String::format("Checksum missmatch, checksum: %02X calculated checksum: %02X, bytes: %s", checksum, checksumValidation, sb2.toString)
logError("JollyMec status incoming string", ST_JollyMec_Arduino.state.toString)
logError("JollyMec status", debugString)
}
else
{
if (deviceId == 1)
{
//Heater living
if (type == 0)
{
//State
NUM_JollyMec_Living_State.postUpdate(value)
//On/off update
if (value >= 1)
{
SW_JollyMec_Living_OnOff.postUpdate(ON)
}
else
{
SW_JollyMec_Living_OnOff.postUpdate(OFF)
}
if (debug)
{
logError("JollyMec status", "Heater living state: {}", value)
}
}
else if (type == 1)
{
//Power update
NUM_JollyMec_Living_Power.postUpdate(value)
if (debug)
{
logError("JollyMec status", "Heater living power update: {}", value)
}
}
else if (type == 2)
{
//Smoke temperature
NUM_JollyMec_Living_SmokeTemp.postUpdate(value)
if (debug)
{
logError("JollyMec status", "Heater living smoke temperature: {}", value)
}
}
else if (type == 3)
{
//Ambient temperature
NUM_JollyMec_Living_AmbientTemp.postUpdate(value)
if (debug)
{
logError("JollyMec status", "Heater living ambient temperature: {}", value)
}
}
else if (type == 4)
{
//Ventilation level
NUM_JollyMec_Living_VentilationLevel.postUpdate(value)
if (debug)
{
logError("JollyMec status", "Heater living ventilation level: {}", value)
}
}
else if (type == 5)
{
//Standby
if (value == 0)
{
SW_JollyMec_Living_Standby.postUpdate(OFF)
}
else
{
SW_JollyMec_Living_Standby.postUpdate(ON)
}
if (debug)
{
logError("JollyMec status", "Heater living standby: {}", value)
}
}
else if (type == 6)
{
//Mode wood or pellet
NUM_JollyMec_Living_Mode.postUpdate(value)
if (debug)
{
logError("JollyMec status", "Heater living mode: {}", value)
}
}
else if (type == 7)
{
//Pellet level
NUM_JollyMec_Living_PelletLevel.postUpdate((55-value)*1.8)
if (debug)
{
logError("JollyMec status", "Heater living pellet level: {}", value)
}
if (NUM_JollyMec_Living_PelletLevel.value < 10) {
sendMail("test@test.com", "Pelletlevel Parterre < 10%", "Pelletlevel Parterre < 10%" )
}
}
else if (type == 253)
{
//Acknowlage command message
if (debug)
{
logError("JollyMec status", "Heater living acknowlage command: {}", value)
}
}
else if (type == 254)
{
logError("JollyMec status", "Heater living Arduino not acknowlage command: {}", value)
}
else if (type == 255)
{
logError("JollyMec status", "Heater living Heater not acknowlage command: {}", value)
}
}
else if (deviceId == 2)
{
//Heater cellar
if (type == 0)
{
//State
NUM_JollyMec_Cellar_State.postUpdate(value)
//On/off update
if (value >= 1)
{
SW_JollyMec_Cellar_OnOff.postUpdate(ON)
}
else
{
SW_JollyMec_Cellar_OnOff.postUpdate(OFF)
}
if (debug)
{
logError("JollyMec status", "Heater cellar state: {}", value)
}
}
else if (type == 1)
{
//Power update
NUM_JollyMec_Cellar_Power.postUpdate(value)
if (debug)
{
logError("JollyMec status", "Heater cellar power update: {}", value)
}
}
else if (type == 2)
{
//Smoke temperature
NUM_JollyMec_Cellar_SmokeTemp.postUpdate(value)
if (debug)
{
logError("JollyMec status", "Heater cellar smoke temperature: {}", value)
}
}
else if (type == 5)
{
//Standby
if (value == 0)
{
SW_JollyMec_Cellar_Standby.postUpdate(OFF)
}
else
{
SW_JollyMec_Cellar_Standby.postUpdate(ON)
}
if (debug)
{
logError("JollyMec status", "Heater cellar standby: {}", value)
}
}
else if (type == 7)
{
//Pellet level
NUM_JollyMec_Cellar_PelletLevel.postUpdate((49-value)*2.04)
if (debug)
{
logError("JollyMec status", "Heater cellar pellet level: {}", value)
}
if (NUM_JollyMec_Cellar_PelletLevel.value < 10) {
sendMail("test@test.com", "Pelletlevel Keller < 10%", "Pelletlevel Keller < 10%" )
}
}
else if (type == 253)
{
//Acknowlage command message
if (debug)
{
logError("JollyMec status", "Heater cellar acknowlage command: {}", value)
}
}
else if (type == 254)
{
logError("JollyMec status", "Heater cellar Arduino not acknowlage command: {}", value)
}
else if (type == 255)
{
logError("JollyMec status", "Heater cellar Heater not acknowlage command: {}", value)
}
}
}
}
end