Optimizing Itho HRU 350 control via MQTT and Home Assistant
(Updated: )
I have an Itho HRU 350 in my home, which is CO₂ controlled, but unfortunately it’s a very crude & noisy control algorithm. Here I document my approach to improve this. I also include humidity and bypass state checks to ensure the house is cooled & dehumidified appropriately.
Current situation & goal ¶
Out of the box, the Itho HRU 350 (and other ventilation boxes) run at low speed (~700 rpm in my case) until the CO₂ level gets too high (>1200 ppm in my case), when it spins up to a rather noisy ~3300 rpm. When CO₂ is below 1000 ppm it slows down again. This is a rather blunt scheme that we can easily improve.
Goal: get more gradual control on the ventilation box, specifically:
- CO₂ <500ppm: run at auto setting (~700 rpm)
- CO₂ 500-800ppm: run at medium settinfg (~1400 rpm)
- CO₂ 800-1100ppm: run at autonight setting (~1800 rpm)
- CO₂ >1100ppm: run at high setting (~2900 rpm)
Update: additionally I want the ventilation to not go below a certain level in case of cooling or dehumidifying the house:
- If Itho bypass valve is open (house is cooling down): don’t go below autonight setting
- If max humidity is > 60 (house is moist): don’t go below autonight setting
Hardware ¶
I got a pre-built ESP8266 board from Arjen Hiemstra at NRG.watch (nrgwatch.nl). I tried to solder on the CC1101 myself and failed, so would recommend to save the frustration and spend 5 EUR to have it pre-soldered.
Connecting the board to the Itho HRU was done with a 15cm straight 8p UTP cable.
Ventilator calibration ¶
My ventilator box runs at the following speeds & power consumption levels before calibration:
- Auto: 750 rpm @ 5W
- Autonight: 2000rpm [= (max-min)*Night min vent optima 1 multi-floor 3+ persons (%) +min) = (3300-750)*48%+750] @ 40W
- Low: 750 rpm @ 5W
- Medium: 1500 rpm @ 20W
- High: 3300 rpm @ 167W
Because the power goes with the cube of speed (engineeringtoolbox.com), it can be worthwhile to see what maximum speed you need. In my case I reduced the highest setting a bit using the potentio meter on the box.
After calibration:
- Auto: 750 rpm @ 5W
- Autonight: 1750rpm [= (max-min)*Night min vent optima 1 multi-floor 3+ persons (%) +min) = (2850-750)*48%+750] @ 31W
- Low: 750 rpm @ 5W
- Medium: 1390 rpm @ 16W [=30% of min-max range]
- High: 2850 rpm @ 115W
Understanding Itho bypass control ¶
The Itho HRU has a bypass valve which opens to disable the heat recovery. This can be useful in summer with cooler nights to cool down the house. The control algorithm is a bit opaque, and reverse-engineered here (tweakers.net), here (tweakers.net), and [here](https://gathering.tweakers.net/forum/list_message/71246692#71246692 (tweakers.net).
The Itho HRU has two temperature sensors:
- Supply temp (C) is the temperature of air supplied to the house, and is slightly lower than the indoor temperature (due to efficiency losses)
- Exhaust temp (C) is the temperature of air exhausted from the house, ans is slightly higher than the outdoor temperature (due to efficiency losses)
Bypass opens when:
- Summerday (K_min) == 1
- par 105. BypassTimer == 0
- [Supply temp (C) - Exhaust temp (C) ] > par.14, de “Offset bypass regulation (0.1*K)”.
Summerday is set to 1 when:
- par.104 SummerCounter (K_min) > par.12: “Summer day time (K*hr) (default “*60 = 5*60 = 300
- par.104 SummerCounter (K_min) is increased when Exhaust temperature > par.10: “Summer temp (C)” [default 19C], and is evaluated every minute
Bypass closes when:
- Supply temp (C) < par.13 Wanted temp bypass regulation (°C) [default 17C] OR
- Supply temp (C) > Exhaust temp (C) (i.e. when it’s too warm outside to cool)
It’s unclear how these parameters work:
- par.105 BypassTimer (min)
- par.15 Max bypass open time (hrs)
Software ¶
I got the software from Arjen Hiemstra’s IthoWifi (github.com) and combining it with Home Assistant as documented on the Non-CVE units (github.com), MQTT integration (github.com), and Home Assistant (github.com) wiki pages.
Configure module ¶
On the module’s config page:
- Set ‘Home Assistant MQTT Discovery’ to off
- Set ‘Normalize keys’ to on
- Set up virtual remote. It’s important to set this up in order for the module to send the right commands. See details on remote types here (github.com).
Configure Itho ¶
On the ‘Itho settings’ page you can query the >100 settings and try to optimize the unit yourself. I haven’t found any documentation, and setting names are quite cryptic, but I did find:
- Increase ‘autonight’ speed - this is useful because autonight also triggers higher speed on high CO₂
- Set ‘80 - Number of floors (floor)’ to max of 2.
- Set ‘81 - Inhabitants (inhab)’ to max of 3.
- Tweak high/low CO₂ threshold
- Set ‘78 - PoorCo2Quality (ppm)’ to lowest value 1501
- Set ‘79 - GoodCo2Quality (ppm)’ to mid value 1000
Configure Home Assistant ¶
Add config to Home Assistant. Ensure you merge this config with existing mqtt/sensor configuration (e.g. you should only have one ‘mqtt’ key, Home Assistant will give an error if you don’t.)
mqtt:
sensor:
- name: "itho_hru_co2"
unique_id: "itho_hru_co2"
state_topic: "itho/ithostatus"
value_template: "{{ value_json['highest-received-co2-value_ppm'] }}"
unit_of_measurement: "ppm"
device_class: carbon_dioxide
state_class: 'measurement'
device: { identifiers: ["mqtt", "nrg-itho-28ac"] }
- name: "itho_hru_exhaust_fan_actual"
unique_id: "itho_hru_exhaust_fan_actual"
state_topic: "itho/ithostatus"
unit_of_measurement: "rpm"
value_template: "{{ value_json['exhaust-fan-actual_rpm'] }}"
device: { identifiers: ["mqtt", "nrg-itho-28ac"] }
- name: "itho_hru_supply_fan_actual"
unique_id: "itho_hru_supply_fan_actual"
state_topic: "itho/ithostatus"
unit_of_measurement: "rpm"
value_template: "{{ value_json['supply-fan-actual_rpm'] }}"
device: { identifiers: ["mqtt", "nrg-itho-28ac"] }
- name: "itho_hru_exhaust_temp"
unique_id: "itho_hru_exhaust_temp"
state_topic: "itho/ithostatus"
unit_of_measurement: "°C"
device_class: temperature
value_template: "{{ value_json['exhaust-temp_c'] }}"
device: { identifiers: ["mqtt", "nrg-itho-28ac"] }
- name: "itho_hru_supply_temp"
unique_id: "itho_hru_supply_temp"
state_topic: "itho/ithostatus"
unit_of_measurement: "°C"
device_class: temperature
value_template: "{{ value_json['supply-temp_c'] }}"
device: { identifiers: ["mqtt", "nrg-itho-28ac"] }
binary_sensor:
- name: "itho_hru_bypass"
unique_id: "itho_hru_bypass"
state_topic: "itho/ithostatus"
value_template: "{{ value_json['bypass-position'] }}"
payload_off: 0
payload_on: 1
device_class: opening
device: { identifiers: ["mqtt", "nrg-itho-28ac"] }
fan:
- name: "itho_hru_fan"
device:
identifiers: ["mqtt", "nrg-itho-28ac"]
model: ITHO Wifi Add-on
name: itho_hru_fan_device
#availability_topic: itho/state
unique_id: itho_hru_fan
state_topic: itho/lwt
payload_on: "online"
payload_off: "offline"
#state_value_template: '{% if value == "online" %}ON{% else %}OFF{% endif %}'
# json_attributes_topic: itho/ithostatus
command_topic: "itho/cmd"
preset_mode_state_topic: "itho/ithostatus"
preset_mode_command_template: "{ vremote: '{{ value }}'}"
preset_mode_value_template: >
{% set am = value_json['actual-mode'] | int %}
{% if am == 1 %}
low
{% elif am == 2 %}
medium
{% elif am == 3 %}
high
{% elif am == 13 %}
timer
{% elif am == 24 %}
auto
{% elif am == 25 %}
autonight
{% else %}
{{ am }}
{% endif %}
preset_mode_command_topic: "itho/cmd"
preset_modes:
- "low"
- "medium"
- "high"
- "auto"
- "autonight"
- "timer1"
- "timer2"
- "timer3"
Test via Home Assistant Developer tools
-> Services
fan.set_preset_mode
‘high’ - worksfan.set_preset_mode
‘medium’ - worksfan.set_preset_mode
’low’ - worksfan.set_preset_mode
‘autonight’ - works
Automate controls ¶
Goal ¶
Now that we can control the Itho unit, it’s time to implement our improved control algorithm:
Goal:
- Ensure that time-average CO₂ stays as close to outdoor level as possible at comfortable ventilation
- Ensure that CO₂ never exceeds 1000 ppm for long
- Ensure quiet operation of ventilator –> max rpm ~2500
- Ensure ventilation never goes below autonight when humidity is above 65%
- Ensure ventilation never goes below autonight when bypass is open
Algorithm:
- CO₂ >1100ppm: run at high setting (~2900 rpm)
- CO₂ 800-1100ppm: run at autonight setting (~1800 rpm)
- Bypass open: run at autonight setting (~1800 rpm)
- High humidity: run at autonight setting (~1800 rpm)
- CO₂ 500-800ppm: run at medium settinfg (~1400 rpm)
- CO₂ <500ppm: run at auto setting (~700 rpm)
Boundary conditions:
- Don’t change speed unnecessarily –> only trigger when level changed for more than X minutes
- Don’t override timer functions used for e.g. getting rid of humidity –> use
states.fan.itho_hru_350.attributes.preset_mode
!= timer1/timer2/timer3
Home Assistant implementation ¶
There’s a few approaches that could work:
- Set up timer job, check every 5 min and set mode according to CO₂ - Pro: easy Con: no ’low pass filter’ / could trigger at spiky value
- Run automation on CO₂ level and change when it crosses a boundary (i.e.
platform: numeric_state
) - Pro: can use ‘for’ setting to prevent spurious triggers. - Use value_template to check
from_state
&to_state
. Pro: triggers only on threshold crossing. Con: does not have ’low pass filter’ / could trigger at spiky value - Use
wait_for_trigger
to return to low speed when ventilation is done. Pro: works nicely for 2 levels. Con: only for 2 levels? - Use filter integration (home-assistant.io) to filter the underlying sensor value, combine with timer automation? Pro: easy, works, robust Con: sends commands more than necessary, overrides user setting
In the end I used option 2 from above, which has now been running well for a few weeks. I split the automation into 4 levels which gives me more fine-grained control (continuous control would still be better but somehow Itho decided against this for the HRU 350). To also trigger on humidity and bypass level, I added a numeric_state
trigger for humidity, and a trigger on any change of bypass.
To actuate the Itho unit, I check the conditions one by one, stopping at the lowest ventilation speed allowed.
alias: Itho control override+
description: Control Itho manually based on CO2, humidity, and bypass
trigger:
- id: auto
platform: numeric_state
entity_id: sensor.max_co2
for:
hours: 0
minutes: 5
seconds: 0
below: 500
- id: medium
platform: numeric_state
entity_id: sensor.max_co2
for:
hours: 0
minutes: 5
seconds: 0
above: 500
below: 800
- id: autonight
platform: numeric_state
entity_id:
- sensor.max_co2
for:
hours: 0
minutes: 5
seconds: 0
above: 800
below: 1000
- id: high
platform: numeric_state
entity_id:
- sensor.max_co2
for:
hours: 0
minutes: 5
seconds: 0
above: 1000
- platform: state
entity_id:
- binary_sensor.itho_hru_bypass
- id: humidity_high
platform: numeric_state
entity_id:
- sensor.max_humidity
for:
hours: 0
minutes: 5
seconds: 0
above: 60
- id: humidity_normal
platform: numeric_state
entity_id:
- sensor.max_humidity
for:
hours: 0
minutes: 5
seconds: 0
below: 60
condition:
- condition: not
conditions:
- condition: state
entity_id: fan.itho_hru_350
attribute: preset_mode
state:
- timer1
- timer2
- timer3
action:
- service: notify.notify
metadata: {}
data:
message: |
{% if trigger.id == "high" %}
"high trigger "{{ trigger.id }}
{% elif trigger.id == "autonight" %}
"autonight trigger" {{ trigger.id }}
{% elif states("binary_sensor.itho_hru_bypass") == "on" %}
"bypass open trigger autonight" {{ trigger.id }}
{% elif (states("sensor.max_humidity")|default('55')|int > 60) %}
"humid trigger autonight" {{ trigger.id }}
{% elif trigger.id == "medium" %}
"medium trigger" {{ trigger.id }}
{% elif trigger.id == "auto" %}
"auto trigger" {{ trigger.id }}
{% else %}
"unknown trigger, setting to auto" {{ trigger.id }}
{% endif %}
mode: single