I have installed a MAICO PP45 decentralized ventilation system with heat recovery in our newly renovated house. I also have four MAICO 2-speed extraction fans as part of the whole house ventilation scheme.
Of course it had to be integrated with HA. For the systems I installed (lights, security alarm, ventilation) the requirement is that they work autark, and that they can be monitored and controlled from HA for the ‘nice to have’ features.
The MAICO Push-Pull fans work in pairs and are controlled by a RLS 45 K controller. Each controller can have up to three ventilator pairs connected. I have one controller for the ground floor with two ventilator pairs and one controller for first floor with one pair.
The RLS controllers have a MODBUS interface, so I have a LAN gateway connected to both controllers.
The extraction fans are controlled by the same PLC that controls my lights. That PLC is connected through LAN (Modbus protocol) with HA.
Everything comes together in HA:
HA also controls the speed as a function of time (f.i. low speed in bedrooms at night) and it controls the heat recovery / cross ventialtion mode (based on outside temperature).
HA controls the bathroom fan, starting a ventilation cycle when a RH spike (from the `Homematic underfloor heating control thermostat) is detected (a shower). And HA controls the Toilet fan based on the light (delayed on to low, then delayed on to high, then off). The fans can be manually controlled from the wall switches connected to the PLC (but never are …).
(the remaining 2 extraction fans are in a part of the house that is not ready yet)
YAML:
#
# *** MAICO RLS45 PACKAGE ***
#
# Connects two Maico RLS45 room air controllers to Home Assistant via
# Modbus RTU over a RS-485 -> Ethernet gateway.
#
# Native RLS45 Modbus interface: RTU only (9600, 8, Even, 1 stop bit).
# There is no native Modbus TCP on the RLS45 itself - TCP access requires
# a serial gateway, hence "rtuovertcp" below.
#
# Gateway: Waveshare RS485 TO ETH (B), MAC 04-EE-E8-1D-9A-3C, fixed via
# FRITZ!Box DHCP reservation at 192.168.178.95, TCP Server mode on port 502.
#
# INTEGRATIONS
modbus:
- name: rls45_gateway
type: rtuovertcp
host: 192.168.178.95
port: 502
timeout: 5
delay: 0
message_wait_milliseconds: 30
sensors:
# --- Unit 1 (address 10, EG) ---
# Ventilation level: 0=Off, 1..5=Ventilation step 1-5
- name: "RLS45 Unit 1 Ventilation Level"
slave: 10
address: 201
input_type: holding
unique_id: 8A1F3C0051
# Time remaining until filter change
- name: "RLS45 Unit 1 Filter Time Remaining"
slave: 10
address: 300
input_type: holding
unit_of_measurement: "d"
unique_id: 8A1F3C0021
# Raw error code (0-7, see comment in template sensor below)
- name: "RLS45 Unit 1 Error Code"
slave: 10
address: 302
input_type: holding
unique_id: 8A1F3C0023
# Operating days - ventilation off
- name: "RLS45 Unit 1 Operating Days Off"
slave: 10
address: 400
input_type: holding
unit_of_measurement: "d"
unique_id: 8A1F3C0025
# Operating hours (remainder, 0-23) - ventilation off
- name: "RLS45 Unit 1 Operating Hours Off"
slave: 10
address: 401
input_type: holding
unit_of_measurement: "h"
unique_id: 8A1F3C0024
# Operating days - total
- name: "RLS45 Unit 1 Operating Days Total"
slave: 10
address: 412
input_type: holding
unit_of_measurement: "d"
unique_id: 8A1F3C0027
# Operating hours (remainder, 0-23) - total
- name: "RLS45 Unit 1 Operating Hours Total"
slave: 10
address: 413
input_type: holding
unit_of_measurement: "h"
unique_id: 8A1F3C0026
# --- Unit 2 (address 11, OG) ---
# Ventilation level: 0=Off, 1..5=Ventilation step 1-5
- name: "RLS45 Unit 2 Ventilation Level"
slave: 11
address: 201
input_type: holding
unique_id: 8A1F3C0052
# Time remaining until filter change
- name: "RLS45 Unit 2 Filter Time Remaining"
slave: 11
address: 300
input_type: holding
unit_of_measurement: "d"
unique_id: 8A1F3C0028
# Raw error code (0-7, see comment in template sensor below)
- name: "RLS45 Unit 2 Error Code"
slave: 11
address: 302
input_type: holding
unique_id: 8A1F3C0030
# Operating days - ventilation off
- name: "RLS45 Unit 2 Operating Days Off"
slave: 11
address: 400
input_type: holding
unit_of_measurement: "d"
unique_id: 8A1F3C0032
# Operating hours (remainder, 0-23) - ventilation off
- name: "RLS45 Unit 2 Operating Hours Off"
slave: 11
address: 401
input_type: holding
unit_of_measurement: "h"
unique_id: 8A1F3C0031
# Operating days - total
- name: "RLS45 Unit 2 Operating Days Total"
slave: 11
address: 412
input_type: holding
unit_of_measurement: "d"
unique_id: 8A1F3C0034
# Operating hours (remainder, 0-23) - total
- name: "RLS45 Unit 2 Operating Hours Total"
slave: 11
address: 413
input_type: holding
unit_of_measurement: "h"
unique_id: 8A1F3C0033
switches:
# --- Unit 1 ---
# Operating mode: off=Heat recovery (continuous ventilation), on=Cross ventilation
- name: "RLS45 Unit 1 Cross Ventilation Mode"
slave: 10
address: 200
write_type: holding
command_on: 1
command_off: 0
verify:
input_type: holding
address: 200
state_on: 1
state_off: 0
scan_interval: 10
unique_id: 8A1F3C0041
# --- Unit 2 ---
# Operating mode: off=Heat recovery (continuous ventilation), on=Cross ventilation
- name: "RLS45 Unit 2 Cross Ventilation Mode"
slave: 11
address: 200
write_type: holding
command_on: 1
command_off: 0
verify:
input_type: holding
address: 200
state_on: 1
state_off: 0
scan_interval: 10
unique_id: 8A1F3C0044
# SENSORS
template:
- sensor:
# --- Unit 1 ---
# Human-readable error text for Unit 1, from raw error code (302)
- name: "RLS45 Unit 1 Error Text"
unique_id: 8A1F3C0061
state: >
{% set code = states('sensor.rls45_unit_1_error_code') | int(0) %}
{% set codes = {
0: 'No error',
1: 'Internal error in RLS',
2: 'Overtemperature/overload of mains adapter',
3: 'No communication / failure of external power units, RLS or PPB 30 K',
4: 'No communication / failure of sensors (RS485, I2C)',
5: 'Internal system error slave power unit / RLS / PPB 30 K',
6: 'No communication with EnOcean module (PP 45 EO)',
7: 'No communication with taught-in sensor / error message from PP 45 RC'
} %}
{{ codes.get(code, 'Unknown code: ' ~ code) }}
# Total operating time for Unit 1, combining days register + hour remainder register
- name: "RLS45 Unit 1 Operating Hours Combined"
unique_id: 8A1F3C0063
state: >
{% set days = states('sensor.rls45_unit_1_operating_days_total') | float(0) %}
{% set hours = states('sensor.rls45_unit_1_operating_hours_total') | float(0) %}
{{ (days * 24 + hours) | round(0) }}
unit_of_measurement: "h"
# # --- Unit 2 ---
# Human-readable error text for Unit 2, from raw error code (302)
- name: "RLS45 Unit 2 Error Text"
unique_id: 8A1F3C0062
state: >
{% set code = states('sensor.rls45_unit_2_error_code') | int(0) %}
{% set codes = {
0: 'No error',
1: 'Internal error in RLS',
2: 'Overtemperature/overload of mains adapter',
3: 'No communication / failure of external power units, RLS or PPB 30 K',
4: 'No communication / failure of sensors (RS485, I2C)',
5: 'Internal system error slave power unit / RLS / PPB 30 K',
6: 'No communication with EnOcean module (PP 45 EO)',
7: 'No communication with taught-in sensor / error message from PP 45 RC'
} %}
{{ codes.get(code, 'Unknown code: ' ~ code) }}
# Total operating time for Unit 2, combining days register + hour remainder register
- name: "RLS45 Unit 2 Operating Hours Combined"
unique_id: 8A1F3C0064
state: >
{% set days = states('sensor.rls45_unit_2_operating_days_total') | float(0) %}
{% set hours = states('sensor.rls45_unit_2_operating_hours_total') | float(0) %}
{{ (days * 24 + hours) | round(0) }}
unit_of_measurement: "h"
# FANS
# Create a fan entry in HA for each RLS45
- fan:
# --- Unit 1 ---
# Ground floor fan pairs
- name: "RLS45 Unit 1 Fan"
unique_id: eg_fans
optimistic: true
# State: On if level is 1-5, Off if 0
state: >
{{ states('sensor.rls45_unit_1_ventilation_level') | int(0) > 0 }}
# Percentage: Map 0-5 levels to 0%, 20%, 40%, 60%, 80%, 100%
percentage: >
{% set level = states('sensor.rls45_unit_1_ventilation_level') | int(0) %}
{{ [0, 20, 40, 60, 80, 100][level] if level in [0,1,2,3,4,5] else 0 }}
# Preset Mode: Read from the switch
preset_mode: >
{% if is_state('switch.rls45_unit_1_cross_ventilation_mode', 'on') %}
Cross Ventilation
{% else %}
Heat Recovery
{% endif %}
speed_count: 5
preset_modes:
- "Heat Recovery"
- "Cross Ventilation"
turn_on:
- action: modbus.write_register
data:
hub: rls45_gateway
slave: 10
address: 201
value: 1
turn_off:
- action: modbus.write_register
data:
hub: rls45_gateway
slave: 10
address: 201
value: 0
set_percentage:
- action: modbus.write_register
data:
hub: rls45_gateway
slave: 10
address: 201
value: "{{ (percentage / 20) | round(0) | int }}"
set_preset_mode:
- action: >
{% if preset_mode == 'Cross Ventilation' %} switch.turn_on {% else %} switch.turn_off {% endif %}
target:
entity_id: switch.rls45_unit_1_cross_ventilation_mode
# --- Unit 2 ---
# First floor fan pair
- name: "RLS45 Unit 2 Fan"
unique_id: og_fans
optimistic: true
# State: On if level is 1-5, Off if 0
state: >
{{ states('sensor.rls45_unit_2_ventilation_level') | int(0) > 0 }}
# Percentage: Map 0-5 levels to 0%, 20%, 40%, 60%, 80%, 100%
percentage: >
{% set level = states('sensor.rls45_unit_2_ventilation_level') | int(0) %}
{{ [0, 20, 40, 60, 80, 100][level] if level in [0,1,2,3,4,5] else 0 }}
# Preset Mode: Read from your existing verified switch
preset_mode: >
{% if is_state('switch.rls45_unit_2_cross_ventilation_mode', 'on') %}
Cross Ventilation
{% else %}
Heat Recovery
{% endif %}
speed_count: 5
preset_modes:
- "Heat Recovery"
- "Cross Ventilation"
turn_on:
- action: modbus.write_register
data:
hub: rls45_gateway
slave: 11
address: 201
value: 1
turn_off:
- action: modbus.write_register
data:
hub: rls45_gateway
slave: 11
address: 201
value: 0
set_percentage:
- action: modbus.write_register
data:
hub: rls45_gateway
slave: 11
address: 201
value: "{{ (percentage / 20) | round(0) | int }}"
set_preset_mode:
- action: >
{% if preset_mode == 'Cross Ventilation' %} switch.turn_on {% else %} switch.turn_off {% endif %}
target:
entity_id: switch.rls45_unit_2_cross_ventilation_mode
# SCRIPTS
# These write momentary "command" registers (204/205) which have no
# meaningful read-back state, so they are exposed as scripts rather than
# persistent entities.
script:
# --- Unit 1 ---
rls45_unit1_error_reset:
alias: "RLS45 Unit 1 - Reset Error"
sequence:
- action: modbus.write_register
data:
hub: rls45_gateway
slave: 10
address: 204
value: 1
rls45_unit1_filter_reset:
alias: "RLS45 Unit 1 - Reset Filter Timer"
sequence:
- action: modbus.write_register
data:
hub: rls45_gateway
slave: 10
address: 205
value: 1
# --- Unit 2 ---
rls45_unit2_error_reset:
alias: "RLS45 Unit 2 - Reset Error"
sequence:
- action: modbus.write_register
data:
hub: rls45_gateway
slave: 11
address: 204
value: 1
rls45_unit2_filter_reset:
alias: "RLS45 Unit 2 - Reset Filter Timer"
sequence:
- action: modbus.write_register
data:
hub: rls45_gateway
slave: 11
address: 205
value: 1
# END
