This integration allows you to connect and control your Ubbink Ubiflux Vigor ventilation system (W325 or W400) from Home Assistant.
Install via HACS for one-click setup and update notifications — click the badge above. Details below.
This integration supports two ways to talk to the VMC. You pick one when adding the integration, and can switch later via Settings → Devices & Services → VMC Ubbink → Configure (switching keeps your entities and history).
Home Assistant talks to the VMC directly over your network through an Ethernet-to-RS485 gateway (e.g. Waveshare RS485 to ETH (B)). No Docker, no separate host.
Gateway setup (Waveshare, "transparent"/RTU mode):
- Baudrate 19200, data 8, parity None, stop 1 (8N1)
- Work mode: TCP Server, protocol: transparent (raw RTU over TCP)
- Note the gateway IP and TCP port (default
502) - Wire RS485 to the VMC's red Modbus port X15: A→2, B→3, GND→1
Then add the integration → choose Direct, enter the gateway IP, TCP port (502),
and the Modbus slave address (default 20).
Runs the included Python server (ubbink-server) in Docker; Home Assistant talks to it
over HTTP. See the server setup below. Existing installs keep working unchanged.
- Click the button above — it opens HACS on your Home Assistant, pre-filled with this repository.
- Click Download, then restart Home Assistant.
(Once the integration is accepted into the HACS default store you'll also be able to just search for VMC Ubbink in HACS. Until then, the button adds it as a custom repository — updates still work the same way.)
- Copy
custom_components/vmc_ubbinkfrom this repository into your Home Assistantconfig/custom_components/folder. - Restart Home Assistant.
(Manual installs don't receive update notifications — HACS is recommended.)
-
Direct (Waveshare RS485-to-ETH) — no extra software. Just configure the gateway as described under 🔀 Two connection modes above.
-
Server (HTTP) — additionally run the bundled Python server (Docker), which bridges Modbus RTU to a REST API:
git clone https://github.com/dimgen/homeassistant-vmc-ubbink.git cd homeassistant-vmc-ubbink/ubbink-server # Before first start, edit .env and replace the published username/password. docker compose up --build -d
By default the server exposes its API on port 8085 (configurable via
.env). The server code is based on pyubbink.
- Go to Settings → Devices & Services.
- Click + Add Integration → search for VMC Ubbink.
- Choose the connection mode and enter its details:
- Direct — Waveshare gateway IP, TCP port (
502), and Modbus slave id (20). - Server — server host, port (
8085), and username/password (from the server.env).
- Direct — Waveshare gateway IP, TCP port (
- Click Submit. You can switch the mode later via Configure without losing history.
Once added, Home Assistant recognizes VMC Ubiflux as a single device with the following features:
| Name | Entity ID | Unit |
|---|---|---|
| Supply Temperature | sensor.vmc_supply_temperature |
°C |
| Supply Pressure | sensor.vmc_supply_pressure |
Pa |
| Supply Airflow Actual | sensor.vmc_supply_airflow_actual |
m³/h |
| Supply Airflow Preset | sensor.vmc_supply_airflow_preset |
m³/h |
| Extract Temperature | sensor.vmc_extract_temperature |
°C |
| Extract Pressure | sensor.vmc_extract_pressure |
Pa |
| Extract Airflow Actual | sensor.vmc_extract_airflow_actual |
m³/h |
| Extract Airflow Preset | sensor.vmc_extract_airflow_preset |
m³/h |
| Airflow Mode | sensor.vmc_airflow_mode |
|
| Bypass Status | sensor.vmc_bypass_status |
|
| Filter Status | sensor.vmc_filter_status |
|
| Serial Number | sensor.vmc_serial_number |
| Name | Entity ID | Type | Options / Range |
|---|---|---|---|
| Airflow Mode | select.vmc_airflow_mode |
Select | wall_unit, holiday, low, normal, high |
| Airflow Rate | number.vmc_airflow_rate |
Number | 50-400 m³/h |
alias: Update VMC airflow speed based on CO2
description: Linearly adjusts ventilation speed (50–220) based on CO2 (to be in range 550-1000).
triggers:
- entity_id: sensor.max_co2
trigger: state
conditions: []
actions:
- variables:
co2_value: "{{ states('sensor.max_co2') | float(0) }}"
- variables:
new_speed: >-
{% set min_co2 = 550 %} {% set max_co2 = 1000 %} {% set min_speed = 50
%} {% set max_speed = 220 %}
{% if co2_value <= min_co2 %}
{{ min_speed }}
{% elif co2_value >= max_co2 %}
{{ max_speed }}
{% else %}
{% set interpolated = min_speed
+ (co2_value - min_co2)
/ (max_co2 - min_co2)
* (max_speed - min_speed) %}
{{ interpolated | round(0) }}
{% endif %}
- choose:
- conditions:
- condition: template
value_template: |
{{ states('number.airflow_rate') | float(0)
!= new_speed | float(0) }}
sequence:
- target:
entity_id: number.airflow_rate
data:
value: "{{ new_speed }}"
action: number.set_value
mode: singlealias: Update VMC airflow speed based on CO2
description: >-
Linearly adjusts ventilation speed (100–220, steps of 5) based on CO2
(600–1000).
triggers:
- entity_id: sensor.max_co2
trigger: state
conditions: []
actions:
- variables:
co2_value: "{{ states('sensor.max_co2') | float(0) }}"
- variables:
new_speed: >-
{% set min_co2 = 600 %} {% set max_co2 = 1000 %} {% set min_speed = 100
%} {% set max_speed = 220 %}
{% if co2_value <= min_co2 %}
{{ min_speed }}
{% elif co2_value >= max_co2 %}
{{ max_speed }}
{% else %}
{% set interpolated = min_speed
+ (co2_value - min_co2) / (max_co2 - min_co2)
* (max_speed - min_speed) %}
{{ ((interpolated / 5) | round(0) * 5) | int }}
{% endif %}
- choose:
- conditions:
- condition: template
value_template: |
{{ states('number.airflow_rate') | float(0)
!= new_speed | float(0) }}
sequence:
- target:
entity_id: number.airflow_rate
data:
value: "{{ new_speed }}"
action: number.set_value
mode: singlealias: Update VMC mode based on CO2
description: Sets airflow mode based on maximum CO2.
triggers:
- entity_id: sensor.max_co2
trigger: state
conditions: []
actions:
- choose:
- conditions:
- condition: numeric_state
entity_id: sensor.max_co2
above: 1000
sequence:
- choose:
- conditions:
- condition: template
value_template: "{{ states('sensor.airflow_mode') != 'high' }}"
sequence:
- target:
entity_id: select.airflow_mode
data:
option: high
action: select.select_option
- stop: CO2 above 1000 - done.
- conditions:
- condition: numeric_state
entity_id: sensor.max_co2
above: 800
sequence:
- choose:
- conditions:
- condition: template
value_template: "{{ states('sensor.airflow_mode') != 'normal' }}"
sequence:
- target:
entity_id: select.airflow_mode
data:
option: normal
action: select.select_option
- stop: CO2 above 800 - done.
- conditions:
- condition: numeric_state
entity_id: sensor.max_co2
above: 600
sequence:
- choose:
- conditions:
- condition: template
value_template: "{{ states('sensor.airflow_mode') != 'low' }}"
sequence:
- target:
entity_id: select.airflow_mode
data:
option: low
action: select.select_option
- stop: CO2 above 600 - done.
default:
- choose:
- conditions:
- condition: template
value_template: "{{ states('sensor.airflow_mode') != 'holiday' }}"
sequence:
- target:
entity_id: select.airflow_mode
data:
option: holiday
action: select.select_option
mode: singleYou can use your Ubbink Wall Controller with this integration. However, it seems that the controller always has the priority. When you change the airflow on the controller, the integration's setting is bypassed. Also, it seems that you cannot set an airflow lower than the controller's setting from the integration. The best is thus to set the controller on the minimal setting (eg: 1/4) and control the airflow via Hass. This provides a fallback via the wall unit.
- Ensure the Ubbink Server is running and accessible from Home Assistant.
- Check if the correct host/port is entered in the integration settings.
- View server logs:
docker logs ubbink-server
- If you see errors such as
Error: [Input/Output] Modbus Error: [Invalid Message] No response received, expected at least 2 bytes (0 received)in your server logs, that can be a parity issue. - The integration is using No parity, but, by default the W400 seems to be configured with parity Odd.
- You should thus change the parity configuration in your machine's menu to use No parity : ⚙ ➤ 14. Communication ➤ 4. Parity ➤ No
- Restart Home Assistant after adding the integration.
- Check the API response manually:
curl -u '<username>:<password>' http://server-ip:8085/data
This project is licensed under the MIT License.
Pull requests are welcome! If you find issues, feel free to open an issue.
