Project to control JollyMec heaters

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
2 Likes