Tiemme 4Heat Stove WiFi Controller

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

I noticed that yesterday and some little bugs like upper/lower cases of some letters. I will edit the code now and also add some other codes. After this, I will work on the error messages.

I unpacked the 4heat app (unpacked app) and I found some .js files named controllers.js
it seems inside there are all the codes the app recognizes.
i also cleaned a bit the file. both files you may download them in here

1 Like

Did you try changing the speed? usually, it is 9600 or 14400?

Also another code:

J30001000000000031 - Check Up
          {% elif SensorCode == 31 %}
            Check Up

I will change it now, in my 4heat 31, 32, and 33 is ignition. But checkup seems logical.

I have quickly scrolled throw the two files and can confirm tat this is the application code.
It consists of basically two parts, the local communication and the cloud communication.

Cause my 4heat interface is a branded resell of Oranier, we might get some more information if I unpack there App as well.

Keep you posted tonight …

Here the package again

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 == 10 %}
            Recover Ignition
          {% elif SensorCode == 30 %}
            Ignition Mode
          {% elif SensorCode == 31 %}
            Check Up
          {% elif SensorCode == 32 %}
            Ignition Mode
          {% elif SensorCode == 33 %}
            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 %}

With all new discovered States and two minor bugfixes

maybe we should unpack the latest app, this one i found online for download. The new versions maybe have the rest of the codes that are unknown till now.