Tiemme 4Heat Stove WiFi Controller

Hello, I purchased a Tiemme wifi stove controller for my Milkuz Pellet Stove. It connects to the wifi router and then to https://wifi4heat.azurewebsites.net/ [IP: 51.140.152.154]

There is only Mobile App 4Heat to send commands to the controller.
Is there anyone working on this? Is anyone interested?
The bummer is that I can’t seem to find any contact with the company. They say they support the controller for integration with google home and integration with Alexa. I tried to add it to Google home, it is not working.

This is the user manual

I got some traffic with wireshark. It uses TLSv1.2 to connect to azure. This is the wireshark ,pcapng file

So I need help to get the certificate from the app to decode the TLS.

Thanks to all reading this. Merry Christmas and Happy New Year :slight_smile:

[Update]

I installed PCAP Remote on my phone and got some more traffic.

This is the Wireshark traffic decrypted tiemme-4heat.pcapng.

So I need help on what to do next to make an integration with the HA. Any help will do, this is my first time doing this.

Thank you.

1 Like

Hi.
I’ve same requremtens as you, but not purchased this module yet.
I want to try connect directly to my mainbord (NG21), using rs232 cable, maybe you can take foto of rj11 jack, both side, please?

This module supports local (direct) connection to 4heat app. In documentation this is described. Maybe you can scan traffic without Azure connection? I’ll try check your dump file later.

I’m very glad, that I’m not alone.

1 Like

The RJ11 cable is a simple straight cable, but I don’t know the pinology, what is Rx what is TX, coz that cable is going to my wifi controller. At first, I contacted Tiemme for help with the cable, there is still no response :frowning:

The previous tiemme-4heat.pcapng file has the local communication in it. This is what I find out.

  1. The app first contact the server
  2. then sends the commands locally to the controller IP port 80. The command list I found out is below:
  3. there are some other commands like schedule, graph, and thermostat, but I didn’t search for them coz that is what the Home Assistant would work :slight_smile:
    it would be nice to make one integration for TCP and RS232 communication for this one.
    if anyone knows a way how to make this easy as integration, please give me notes (or examples even)

Commands list:

IMPORTANT !!! If you use copy/paste of the code from here, the first (") (double quote) is upper (ASCII 226 128 156), the second is lower (ASCII 226 128 157), That won’t work if you paste it to the CLI. The editor in here makes it so. They should be the same as the double quote on the keyboard (ASCII 34)

[“SEL”,“0”] - get the status

[“SEC”,“3”,“I30001000000000000”,“I30002000000000000”,“I30017000000000000”] - get short status (used when stove is in block mode)

and this is the string with the result:
[“SEL”,“0”][“SEL”,“11”,“J30001000000000000”,“J30002000000000000”,“J30005000000000027”,“J30006000000000000”,“J30011000000000110”,“J30012000000000000”,“J30017000000000029”,“J30020000000000000”,“B20180000000000070”,“B20005000000000030”,“B20006000000000080”]

where:
J30001 - state of the stove:
J30001000000000000 - the stove is off
J30001000000000030 - the stove is in ignition mode
J30001000000000031 - Check Up
J30001000000000032 - unknown to me (occurs after 32, short time, on the mobile it says Ignition)
J30001000000000003 - stove is in stabilization mode
J30001000000000005 - stove is in Run Mode
J30001000000000006 - stove is in Modulation mode
J30001000000000007 - stove is Extinguishing
J30001000000000008 - Safety
J30001000000000009 - the stove is in Block mode (error exist)
J30001000000000010 - Recovery mode
J30001000000000011 - Standby

J30002 - error code
J30002000000000000 - error code number (no error)
J30002000000000012 - error code number (error code12)

J30005 is exhaust temperature
J30005000000000027 - temp of exhaust is 27°C

J30006 is Room temperature
J30006000000000000 - in my case there is no room temperature sensor

J30011 is the speed of the pellet dropping
J30011000000000001 - Speed 1
J30011000000000002 - Speed 2 etc
J30011000000000110 - stove is turning off
J30011000000000255 - stove is in ignition mode

J30012 is Buffer temperature
J30012000000000000 - in my case there is not sensor

J30017 is temperature of the water in the boiler
J30017000000000029 - temp of water is 29°C

J30020 is water pressure in mbar
J30020000000000000 in my case there is no sensor

B20180 is the stove internal boiler target temperature
B20180000000000070 - temp is set to 70°C

B20005 is Minimum Range of Boiler Thermostat
B20005000000000030 - temp is set to 30°C

B20006 is Maximum Range of Boiler Thermostat
B20006000000000080- temp is set to 80°C

[“SEC”,“1”,“B20180000000000080”]
command to set internal boiler target temperature
response:
[“SEC”,“1”,“A20180000000000080”]

command to turn on the stove
Tx: [“SEC”,“1”,“0”] - legacy command
Rx: [“SEC”,“0”]
[“SEC”,“1”,“J30253000000000001”] - new type command

command to turn off the stove
Tx: [“SEC”,“1”,“1”] - legacy command
Rx: [“SEC”,“0”]
[“SEC”,“1”,“J30254000000000001”] - new type command

Error messages:
[“ERR”,“1”,“5”] - Error exists]

Unblock
[“SEC”,“1”,“J30255000000000001”]

Network Info:
[“CF7”,“0”] - DO NOT USE THIS, IT WILL DUMB YOUR DEVICE
response:
[“CF7”,“4”,“Stanojoski Home Network 1”,“192.168.0.14”,“1”,"-65"]

1 Like

any update…?

I think the RS232 cable is a standard one. Look at the picture. You can buy these cables in store for a view euro’s.
Maybe the commands @11125 is listing are also the RS232 control commands? I have a KEPO 20KW pellet boiler with a Tiemme NG21 control board and i’ll give it a try next week.

Didn’t have time for more exploring, but altho the female connector to the controller side is RJ12 (6 pins), the cable is RJ11 (4 pins). So there are only 16 combinations to the connection. The one @MicDre provided could be the one. I will test more this weekend to find out some of the error codes and I will update the list. Also, i should start developing integration to HA afterward… my first one.

I just have to edit this
I just updated the codes with the Error message 03

[“ERR”,“1”,“5”] - Alt Er03 [Extinguishing for Exhausting Temperature lowering] (probably coz of out of pellets)

[Edit]
It seems that [“ERR”,“1”,“5”] is msg that the error exists, the error number is shown at J30002 line. when there is an error this is the TCP flow of traffic.

Note at the example the error code is 12 (unsuccessful ignition)

[“SEC”,“3”,“I30001000000000000”,“I30002000000000000”,“I30017000000000000”]
[“SEC”,“3”,“J30001000000000009”,“J30002000000000012”,“J30017000000000038”]

[“SEL”,“0”]
[“SEL”,“11”,“J30001000000000009”,“J30002000000000012”,“J30005000000000038”,“J30006000000000000”,“J30011000000000110”,“J30012000000000000”,“J30017000000000038”,“J30020000000000000”,“B20180000000000070”,“B20005000000000030”,“B20006000000000080”]

1 Like

Hello, I’m also interested in this. I did a little bit of digging and reverse engineering and I managed to find how the wifi4heat.azurewebsites.net API works. I can successfully authenticate, get a token, and do some basic queries like to get the status of the boiler, water temperature, and exhaust temperature. Is this something that you are interested? Or you are looking only at local control?

1 Like

@azos All help is welcomed. The more info we gather, the better app/integration we will make
Can you verify some of the codes we have, or add some new ones.

@11125 I’m not sure if I can get any command IDs from the API, I’m guessing that the API itself has some sort of User/Group Restrictions and the data you can get from the cloud is sort of limited.

So to start you have to get you device_id and device_pin, which are visible during the first 4heat configuration when you are connecting it to your wifi. Then you need to get a access token like this:


curl --location --request POST 'https://wifi4heat.azurewebsites.net/Token' \

--header 'Content-Type: application/x-www-form-urlencoded' \

--data-urlencode 'grant_type=password' \

--data-urlencode 'username={{[email protected]}}' \

--data-urlencode 'password={{YOUR_PASSWORD}}'

this will return you json like this:


{"access_token":"xxxxxx","token_type":"bearer","expires_in":9599,"refresh_token":"xxxxxx","userName":"[email protected]",".issued":"DayOfWeek, dd mm yyyy hh:mm:ss GMT",".expires":"DayOfWeek, dd mm yyyy hh:mm:ss GMT"}

Or you can even directly get assign the access_token to a env var:


export access_token=`curl --location --request POST 'https://wifi4heat.azurewebsites.net/Token' \

--header 'Content-Type: application/x-www-form-urlencoded' \

--data-urlencode 'grant_type=password' \

--data-urlencode 'username={{[email protected]}}' \

--data-urlencode 'password={{YOUR_PASSWORD}}' | jq -r '.access_token'`

also set your deviceID to env var:


export device_id=xxxxxx
export device_pin=xxxxxx

Then you can get your boiler historical data like this for example, although it has around 10min delay until you get the most recent entry. Also the the density is only 5 minutes:


curl --location --request GET 'https://wifi4heat.azurewebsites.net/api/Devices/History?inputModel%5BDeviceId%5D={{$device_id}}&inputModel%5BFrom%5D=2022-01-31&inputModel%5BPeriod%5D=default&inputModel%5BTags%5D%5B0%5D%5BName%5D=209_Temperatura_Esterna&inputModel%5BTags%5D%5B0%5D%5BAggregation%5D=minOfTheDay&inputModel%5BTags%5D%5B1%5D%5BName%5D=209_Temperatura_Esterna&inputModel%5BTags%5D%5B1%5D%5BAggregation%5D=default&inputModel%5BTags%5D%5B2%5D%5BName%5D=209_Temperatura_Esterna&inputModel%5BTags%5D%5B2%5D%5BAggregation%5D=maxOfTheDay' \

--header 'Content-Type: application/json' \

--header 'Authorization: Bearer {{$access_token}}'

Get boiler cron:


curl --location --request GET 'https: //wifi4heat.azurewebsites.net/api/devices/cron?deviceId={{$device_id}}&' \

--header 'Content-Type: application/json' \

--header 'Authorization: Bearer {{$access_token}}'

This is probably interesting because I can see some command IDs in the response (here you will need also your pin that you had set during the initial setup):


curl --location --request GET 'https: //wifi4heat.azurewebsites.net/api/Devices/FileMap?pin={{device_pin}}&id={{device_ID}}' \

--header 'Content-Type: application/x-www-form-urlencoded' \

--header 'Authorization: Bearer {{$access_token}}'

You can get the list of errors/notifications with this one:


curl --location --request GET 'https: //wifi4heat.azurewebsites.net/api/Devices/NotificationErrors/{{$device_id}}?max=50' \

--header 'Content-Type: application/x-www-form-urlencoded'

Get devices summary for your account:


curl --location --request GET 'https: //wifi4heat.azurewebsites.net/api/devices/summary?ids={{device_id}}' \

--header 'Content-Type: application/json'

Get specific device details:


curl --location --request GET 'https: //wifi4heat.azurewebsites.net/api/devices/Details?id={{$device_id}}' \

--header 'Authorization: Bearer {{$access_token}}' \

--header 'Content-Type: application/json'

This is what I’m getting as a current status locally:

["SEL","0"]
["SEL","18","J30001000000000003","J30002000000000000","J30005000000000086","B20813000000000006","B20803000000000001","J30012000000000000","J30015000000000000","J30017000000000056","J30020000000000000","J30026000000000000","J30033000000000022","J30040000000000000","J30044000000000000","B20180000000000065","B20211000000000005","B20211000000000005","B20005000000000050","B20006000000000080"]

Those I can recognize from the app
J30017000000000056 - current water temp (56 C)
B20180000000000065 - target water temp (65 C)
J30033000000000022 - I think this is exhaust depresion (22 Pa)
J30001000000000007 - Extinguishing at the moment

1 Like

Hello,

I also own this mod. Currently I retrieve the instructions in Jeedom by a python script. I am also analyzing the possibility of using an esp286 module.

cordially

joseph

Nice going…

I had a similar problem to tackle with my Duepi-Evo based stove.
However, I used the serial line, so I would be able to bypass the cloud all together.
And side effect; the original app also still works in AP mode(provided it is configured with the local IP address)

Any success with RS232 communication and pinout. I have Kepo MC20

I did not got the 4Heat module as it way expensive for simple ESP board. I don’t know the RS232 pinout but I have the touchscreen which communicates using RS-485 which I can sniff maybe RS232 uses the same protocol.

The 4Heat module have both RS232 and RS485 but I was informed that only RS232 needs to be used.

Since module is powered via RS232 cable it can be using non standard pinout as it needs pin for VCC

Code:

# Kotel
- platform: tcp
  name: 4heat_response
  host: 192.168.0.14
  port: 80
  timeout: 5
  payload: '["SEL","0"]'
  unit_of_measurement: "°C"

- platform: template
  sensors:
    stove_temp0:
      friendly_name: "4heat Boiler Water Temperature"
      value_template: "{{ int(states('sensor.4heat_response')[states('sensor.4heat_response').find('J30017')+6:states('sensor.4heat_response').find('J30017')+18]) }}"
      unit_of_measurement: "°C"
      device_class: temperature
    stove_temp1:
      friendly_name: "4heat Boiler Thermostat"
      value_template: "{{ int(states('sensor.4heat_response')[states('sensor.4heat_response').find('B20180')+6:states('sensor.4heat_response').find('B20180')+18]) }}"
      unit_of_measurement: "°C"
      device_class: temperature
    stove_stat:
      friendly_name: "4heat State"
      value_template: >
        {% set SensorResponse = states('sensor.4heat_response') %}
        {% set SensorCode = int(SensorResponse[SensorResponse.find('J30001')+6:SensorResponse.find('J30001')+18]) %}

        {% if SensorCode == 0 %}
          Off
        {% elif SensorCode == 30 %}
          Ignition Mode 0
        {% elif SensorCode == 31 %}
          Check Up
        {% elif SensorCode == 32 %}
          Ignition Mode 2
        {% elif SensorCode == 33 %}
          Ignition Mode 3
        {% elif SensorCode == 3 %}
          Stabilization Mode
        {% elif SensorCode == 5 %}
          Run Mode
        {% elif SensorCode == 6 %}
          Modulation
        {% elif SensorCode == 7 %}
          Extinguishing
        {% elif SensorCode == 8 %}
          Safety
        {% elif SensorCode == 9 %}
          Block mode
        {% elif SensorCode == 10 %}
          Recover Ignition
        {% elif SensorCode == 11 %}
          Standby
        {% else %}
          Unknown mode : {{ SensorCode }}
        {% endif %}
    stove_err:
      friendly_name: "4heat Error"
      value_template: "{{ int(states('sensor.4heat_response')[states('sensor.4heat_response').find('J30002')+6:states('sensor.4heat_response').find('J30002')+18]) }}"
    stove_exhaust:
      friendly_name: "4heat Exhaust Temperature"
      value_template: "{{ int(states('sensor.4heat_response')[states('sensor.4heat_response').find('J30005')+6:states('sensor.4heat_response').find('J30005')+18]) }}"
      unit_of_measurement: "°C"
      device_class: temperature
    stove_speed:
      friendly_name: "4heat Combustion"
      value_template: >
        {% set SensorResponse = states('sensor.4heat_response') %}
        {% set SensorCode = int(SensorResponse[SensorResponse.find('J30011')+6:SensorResponse.find('J30011')+18]) %}

        {% if SensorCode < 110 %}
          {{ SensorCode }}
        {% elif SensorCode == 110 %}
          Off
        {% elif SensorCode == 255 %}
          Ignition Mode
        {% else %}
          Unknown mode : {{ SensorCode }}
        {% endif %}

result:

1 Like

That’s awesome thanks!, although I’m getting this error

homeassistant.exceptions.InvalidStateError: Invalid state encountered for entity ID: sensor.4heat_response. State max length is 255 characters.
2022-02-03 11:20:13 ERROR (MainThread) [homeassistant.components.sensor] Error while setting up tcp platform for sensor
Traceback (most recent call last):``` 

the response for me at least is 351 characters:

echo "["SEL","18","J30001000000000007","J30002000000000000","J30005000000000096","B20813000000000006","B20803000000000001","J30012000000000000","J30015000000000000","J30017000000000061","J30020000000000000","J30026000000000000","J30033000000000024","J30040000000000000","J30044000000000000","B20180000000000065","B20211000000000005","B20211000000000005","B20005000000000050","B20006000000000080"]" | wc
       1       1     351

I had the same problem and implemented a workaround. Mine is 436 Char long.
Here is my package conversion of @11125:

homeassistant:
  customize:
    package.node_anchors:
      customize: &customize
        package: "4heat"
sensor:
  - platform: tcp
    name: 4heat_response
    host: 192.168.0.144
    port: 80
    timeout: 5
    buffer_size: 2048
    value_template: "{{ value|truncate(254, True) }}"
    payload: '["SEL","0"]'

  - platform: template
    sensors:
      stove_temp0:
        friendly_name: "4heat Boiler Water Temperature"
        value_template: "{{ int(states('sensor.4heat_response')[states('sensor.4heat_response').find('J30017')+6:states('sensor.4heat_response').find('J30017')+18]) }}"
        unit_of_measurement: "°C"
        device_class: temperature
      stove_temp1:
        friendly_name: "4heat Boiler SET Temperature"
        value_template: "{{ int(states('sensor.4heat_response')[states('sensor.4heat_response').find('B20180')+6:states('sensor.4heat_response').find('B20180')+18]) }}"
        unit_of_measurement: "°C"
        device_class: temperature
      stove_stat:
        friendly_name: "4heat Status"
        value_template: >
          {% set SensorResponse = states('sensor.4heat_response') %}
          {% set SensorCode = int(SensorResponse[SensorResponse.find('J30001')+6:SensorResponse.find('J30001')+18]) %}

          {% if SensorCode == 0 %}
            Off
          {% elif SensorCode == 30 %}
            Ignition Mode
          {% elif SensorCode == 3 %}
            Stabilization Mode
          {% elif SensorCode == 5 %}
            Run Mode
          {% elif SensorCode == 7 %}
            Extinguishing
          {% elif SensorCode == 9 %}
            Block mode
          {% else %}
            Unknown mode : {{ sensorCode }}
          {% endif %}
      stove_err:
        friendly_name: "4heat Error"
        value_template: "{{ int(states('sensor.4heat_response')[states('sensor.4heat_response').find('J30002')+6:states('sensor.4heat_response').find('J30002')+18]) }}"
      stove_exhaust:
        friendly_name: "4heat Exhaust Temperature"
        value_template: "{{ int(states('sensor.4heat_response')[states('sensor.4heat_response').find('J30005')+6:states('sensor.4heat_response').find('J30005')+18]) }}"
        unit_of_measurement: "°C"
        device_class: temperature
      stove_speed:
        friendly_name: "4heat Speed"
        value_template: >
          {% set SensorResponse = states('sensor.4heat_response') %}
          {% set SensorCode = int(SensorResponse[SensorResponse.find('J30011')+6:SensorResponse.find('J30011')+18]) %}

          {% if SensorCode < 110 %}
            {{ SensorCode }}
          {% elif SensorCode == 110 %}
            Off
          {% elif SensorCode == 255 %}
            Ignition Mode
          {% else %}
            Unknown mode : {{ sensorCode }}
          {% endif %}

I’m about to start a integration. I already startet some Python code…

2 Likes

I’ve tried to sniff the RS485 port but I’m getting only the gibberish data. Looks like some non standard baud rate is being used

Just saw another code that we are missing:

J30001000000000010 - Recover Ignition

You can add it to the SensorCode posible statuses

          {% elif SensorCode == 10 %}
            Recover Ignition
1 Like