Viessmann Vitodens 100-W B1HG - Local OpenTherm control via ESP32/ESPHome (bypassing Viessmann cloud paywall)
Status: Implemented and running. Document reflects the working configuration as of April 2026, including Viessmann-specific quirks discovered during deployment.
The Viessmann ViCare cloud API returns PACKAGE_NOT_PAID_FOR (HTTP 402) for all
/features endpoints. Authentication and equipment endpoints work fine - the
paywall is specifically on feature data (temperatures, status, controls).
Test results:
- OAuth2 + PKCE authentication: OK
GET /equipment/installations: OKGET /equipment/installations/.../gateways: OK (TCU10x)GET /equipment/installations/.../devices: OK (E3_Vitodens_100_BHC_0122_1)GET /features/installations/.../devices/0/features: 402 PACKAGE_NOT_PAID_FOR
The device roles include capability:monetization;AdvancedReport, confirming the
paywall is a server-side flag on the device/account, not a client configuration issue.
Conclusion: New Viessmann accounts (registered after the monetization change) are paywalled. Older accounts appear to be grandfathered with existing access. Viessmann's paid API tiers are discontinued but no longer purchasable. The free "Basic" tier only provides equipment metadata, not feature data.
| Option | Verdict | Reason |
|---|---|---|
| HA ViCare integration | Blocked | Same cloud API, same paywall |
| viessmann2mqtt | Blocked | Same cloud API, confirmed 402 |
| open3e (CAN bus) | Not practical | Vitodens 100 has no accessible external CAN port |
| Finder OPTA + Modbus-to-OT | Overkill | Industrial PLC approach, ~5x the cost |
| OpenTherm + ESP32 | Chosen | B1HG has OpenTherm as standard on terminal D; fully local, no cloud, ~EUR 30-40 |
System context: The boiler drives an underfloor heating (UFH) manifold, NOT radiators. The manifold has a stainless heat exchanger for primary/secondary separation, a thermostatic mixing valve (TMV) as UFH-side safety cap, and a high-efficiency circulator on the secondary side. Flow temperature and safety clamps in the config are calibrated for this topology.
+------------------+
| Home Assistant |
| (optional) |
+--------+---------+
|
WiFi/API
|
+------------------+ dry +-----------+-----------+ OpenTherm +------------------+
| Zone controller |---contact--| ESP32 + OpenTherm |---2 wire-------| Vitodens 100-W |
| (e.g. Computherm | (2wire) | Shield (ESPHome) | (term D/3) | B1HG boiler |
| Q4Z) | +-----------------------+ +------------------+
+------------------+ |
term E/4 (NTC,
unchanged)
- Existing zone controller manages zone valves and thermostat demands as before
- Its boiler relay output (dry contact) feeds an ESP32 GPIO pin
- When any zone calls for heat, the ESP sees the signal
- ESP calculates optimal flow temperature (weather compensation from outdoor temp)
- ESP sends flow setpoint to boiler via OpenTherm
- Boiler modulates burner accordingly (10-100% proportional modulation, not on/off)
- If HA goes down: ESP continues operating autonomously - all logic runs locally on the ESP
- Modulating control instead of on/off (more efficient, quieter, longer boiler life)
- Deep condensing mode - low flow temps keep return water cold enough for the boiler to condense flue vapor, recovering ~10% extra energy
- No cloud dependency - everything is local
- Failsafe - ESP operates standalone without HA
- Proper UFH control - weather-compensated continuous low-temperature flow matches UFH's thermal inertia profile
| Item | Purpose | Est. Price |
|---|---|---|
| DIYLESS Master OpenTherm Shield (ESP32) | OpenTherm interface board | ~EUR 15-20 |
| WeMos D1 Mini ESP32 | Microcontroller (often bundled with shield) | ~EUR 5-8 |
| 2-core cable, 1.5mm², ~2m | OpenTherm connection to boiler | ~EUR 2 |
| USB-C power supply 5V/1A | Power for ESP | ~EUR 5 |
| Small enclosure | Protect ESP + shield assembly | ~EUR 3-5 |
| Total | ~EUR 30-40 |
Alternative: Ihor Melnyk's OpenTherm Adapter (~EUR 10). Functionally equivalent.
Terminal strip (low voltage side) on the HBMU:
9 8 7 6 5 4 3 2 1
| | |
| | +-- 1: PlusBus (accessories)
| |
| +----- 2: Cylinder temp sensor (system boiler)
|
+-------- 3: D - OpenTherm device <-- CONNECT ESP HERE
4: E - Outside temp sensor <-- KEEP EXISTING NTC
- Connect 2-core cable from shield's boiler terminals to terminal 3 (D)
- Polarity does not matter - OpenTherm is polarity-independent
- Route through the ELV diaphragm grommets into the boiler
- Outdoor NTC sensor on terminal 4 (E) stays as-is
- Enter the commissioning assistant (installer mode)
- Navigate to C.4 (Operating mode), set to 14 (OpenTherm)
- Optional: 2483.0 = 1 (boiler follows DHW demands via OpenTherm)
- Note and record the original C.4 value for rollback
Error code if OpenTherm device not connected: F.96
Two non-obvious requirements on Vitodens 100-W. The boiler will acknowledge CH demand but refuse to fire the burner without these.
-
ch_enable: trueANDdhw_enable: trueMUST be set in the hub block, even if you also define switches for them. Per ESPHome docs, the status bits are sent only if ALL of: hub flag = true, switch = on, setpoint ≠ 0. Missing the hub flag → switch state is irrelevant → CH enable is never actually sent. -
max_rel_mod_level,t_room, andt_room_setinputs are required. Viessmann firmware interprets the absence of these as "no real heat demand" and holds burner modulation at 0% despite reportingch_active=on. Symptoms: pump runs, water temp drops (cold UFH return), flame never lights. DHW works fine (separate code path). This took significant debugging to track down.The fix: stream a constant 100% max_rel_mod_level, and synthesize a plausible room-temperature and room-setpoint pair that flips with the zone controller's demand signal. The boiler doesn't act on these values directly (it's in OpenTherm-slave mode from C.4=14), it just wants to see them to confirm a real room is asking for heat.
esphome:
name: vitodens-opentherm
friendly_name: Vitodens OpenTherm Controller
esp32:
board: esp32dev
framework:
type: arduino
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
reboot_timeout: 0s
ap:
ssid: "Vitodens-OT-Fallback"
password: !secret fallback_password
api:
reboot_timeout: 0s
ota:
platform: esphome
password: !secret ota_password
logger:
level: INFO
# --- OpenTherm Hub ---
opentherm:
in_pin: 21
out_pin: 22
ch_enable: true
dhw_enable: true
# Viessmann-specific: boiler won't fire for CH without these
max_rel_mod_level: "max_mod_level_input"
t_room: "room_temp_input"
t_room_set: "room_setpoint_input"
# --- Zone controller heat demand (dry contact on GPIO 25) ---
binary_sensor:
- platform: gpio
pin:
number: 25
mode: INPUT_PULLUP
inverted: true
name: "Zone Heat Demand"
id: zone_demand
device_class: heat
filters:
- delayed_on: 2s
# 3 min hold bridges the boiler's internal anti-short-cycle min-run window
- delayed_off: 180s
- platform: opentherm
flame_on:
name: "Flame Active"
id: flame_active
ch_active:
name: "CH Active"
id: ch_active_binary
dhw_active:
name: "DHW Active"
id: dhw_active_binary
fault_indication:
name: "Boiler Fault"
# diagnostic_indication (OT Data-ID 5 LB bit 6) intentionally omitted:
# the B1HG does not toggle it in sync with its own diagnostic state
# (e.g. stays OFF during an F.540 condensate lockout). Watch
# oem_diagnostic_code != 0 in HA instead.
# Attribute the flame to either DHW or CH based on boiler mode flags.
# DHW has priority - if dhw_active is on, flame is 100% for hot water.
- platform: template
name: "Flame for DHW"
id: flame_for_dhw
device_class: heat
lambda: |-
return id(flame_active).state && id(dhw_active_binary).state;
- platform: template
name: "Flame for CH"
id: flame_for_ch
device_class: heat
lambda: |-
return id(flame_active).state
&& !id(dhw_active_binary).state
&& id(ch_active_binary).state;
# --- Sensors ---
sensor:
- platform: opentherm
t_boiler:
name: "Boiler Water Temperature"
# t_ret is NOT supported on Vitodens 100-W B1HG (always reads 0) - commented
# t_ret:
# name: "Return Water Temperature"
t_outside:
name: "Outside Temperature (NTC)"
id: outdoor_temp_ntc
t_dhw:
name: "DHW Temperature"
rel_mod_level:
name: "Burner Modulation"
id: burner_modulation
ch_pressure:
name: "CH Water Pressure"
# OT burner/pump counters (Data-IDs 116/117/118/119/120/121/123) are NOT
# implemented on the B1HG - they sit at 0 forever despite real burn
# activity. Omitted to keep the HA device card clean and save ~8 OT
# requests per poll cycle. If long-term wear tracking is wanted, count
# flame_for_ch / flame_for_dhw transitions HA-side, or read the
# counters from the HMI service menu periodically.
max_capacity:
name: "Boiler Max Capacity"
id: boiler_max_capacity
entity_category: diagnostic
min_mod_level:
name: "Boiler Min Modulation"
entity_category: diagnostic
oem_fault_code:
name: "OEM Fault Code"
entity_category: diagnostic
oem_diagnostic_code:
name: "OEM Diagnostic Code"
entity_category: diagnostic
# External outdoor temp from HA (if available) - preferred over boiler NTC
- platform: homeassistant
entity_id: sensor.your_weather_station_outdoor_temperature # adjust to your entity
id: external_outdoor_temp
# Re-publish external temp to HA so you can verify the ESP is receiving it
- platform: template
name: "Outside Temperature (External, as seen by ESP)"
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
accuracy_decimals: 1
update_interval: 30s
lambda: "return id(external_outdoor_temp).state;"
# --- Synthetic inputs required by Viessmann ---
- platform: template
id: max_mod_level_input
name: "OT Input Max Modulation"
unit_of_measurement: "%"
accuracy_decimals: 0
entity_category: diagnostic
lambda: "return 100.0;"
update_interval: 60s
- platform: template
id: room_temp_input
name: "OT Input Room Temp"
unit_of_measurement: "°C"
accuracy_decimals: 1
entity_category: diagnostic
lambda: "return id(zone_demand).state ? 19.0 : 22.0;"
update_interval: 10s
- platform: template
id: room_setpoint_input
name: "OT Input Room Setpoint"
unit_of_measurement: "°C"
accuracy_decimals: 1
entity_category: diagnostic
lambda: "return id(zone_demand).state ? 21.0 : 20.0;"
update_interval: 10s
# --- Split gas consumption: CH vs DHW ---
- platform: template
name: "Gas Power CH"
id: gas_power_ch_kw
unit_of_measurement: "kW"
device_class: power
state_class: measurement
accuracy_decimals: 2
update_interval: 10s
lambda: |-
if (!id(flame_for_ch).state) return 0.0;
float mod = id(burner_modulation).state;
if (isnan(mod)) return 0.0;
float max_kw = id(boiler_max_capacity).state;
if (isnan(max_kw) || max_kw <= 0.0) max_kw = 25.0;
return max_kw * (mod / 100.0);
- platform: template
name: "Gas Power DHW"
id: gas_power_dhw_kw
unit_of_measurement: "kW"
device_class: power
state_class: measurement
accuracy_decimals: 2
update_interval: 10s
lambda: |-
if (!id(flame_for_dhw).state) return 0.0;
float mod = id(burner_modulation).state;
if (isnan(mod)) return 0.0;
float max_kw = id(boiler_max_capacity).state;
if (isnan(max_kw) || max_kw <= 0.0) max_kw = 25.0;
return max_kw * (mod / 100.0);
- platform: template
name: "Gas Power Total"
unit_of_measurement: "kW"
device_class: power
state_class: measurement
accuracy_decimals: 2
update_interval: 10s
lambda: |-
if (!id(flame_active).state) return 0.0;
float mod = id(burner_modulation).state;
if (isnan(mod)) return 0.0;
float max_kw = id(boiler_max_capacity).state;
if (isnan(max_kw) || max_kw <= 0.0) max_kw = 25.0;
return max_kw * (mod / 100.0);
- platform: uptime
name: "Controller Uptime"
entity_category: diagnostic # meta-info about the ESP, not a heating reading
# Mirrors the internal t_set number into a proper HA sensor so the active/
# commanded CH flow setpoint shows up in the Sensors section alongside the
# other boiler readings. A `number` entity cannot live in the Sensors
# section - hence this mirror and `internal: true` on the t_set number.
- platform: template
name: "CH Flow Setpoint (Active)"
id: active_flow_setpoint_sensor
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
accuracy_decimals: 1
update_interval: 10s
lambda: "return id(ot_flow_temp_setpoint).state;"
# --- Flow temperature setpoint ---
# Architecture:
# - OT t_set number is `internal: true` (hidden from HA). Lambda writes to
# it every 10s; a template sensor above mirrors the value for HA display.
# - "CH Flow Setpoint (Manual)" is a template number: the user's persistent
# manual override, used by the lambda only in manual mode with demand.
# Whole-degree slider UI to match the DHW setpoint.
number:
- platform: opentherm
t_set:
name: "CH Flow Setpoint (internal OT t_set)"
id: ot_flow_temp_setpoint
min_value: 20
max_value: 60 # hard ceiling; UFH protection comes from HEX + TMV downstream
internal: true # hidden from HA; template sensor above exposes the value
t_dhw_set:
name: "DHW Setpoint"
initial_value: 50
min_value: 40
max_value: 57
step: 1
mode: slider
- platform: template
name: "CH Flow Setpoint (Manual)"
id: manual_flow_setpoint
min_value: 25
max_value: 60
step: 1
initial_value: 40
optimistic: true
restore_value: true
mode: slider
unit_of_measurement: "°C"
# --- Switches ---
switch:
- platform: opentherm
ch_enable:
name: "Central Heating Enable"
id: ch_enable_switch
restore_mode: RESTORE_DEFAULT_ON
dhw_enable:
name: "DHW Enable"
restore_mode: RESTORE_DEFAULT_ON
- platform: template
name: "Flow Temp Auto Mode"
id: flow_auto_mode
restore_mode: RESTORE_DEFAULT_ON
optimistic: true
# --- Control loop ---
# Single set_value call at the end; weather compensation in the middle; mode/demand
# branches at the top. Hard safety clamp applied uniformly on every code path.
interval:
- interval: 10s
then:
- lambda: |-
float flow_temp;
const char* source_label = "idle";
bool demand = id(zone_demand).state;
bool auto_mode = id(flow_auto_mode).state;
if (!demand) {
// No heat demand: idle at 20C. Boiler won't fire below its min
// useful flow temp, whatever mode we're in.
flow_temp = 20.0;
} else if (!auto_mode) {
// Manual mode with demand: use the user's saved manual setpoint.
flow_temp = id(manual_flow_setpoint).state;
if (isnan(flow_temp)) flow_temp = 35.0;
source_label = "manual";
} else {
// Auto mode with demand: weather compensation.
// Source priority: external (HA) -> boiler NTC -> fixed default.
float outdoor = NAN;
if (!isnan(id(external_outdoor_temp).state)) {
outdoor = id(external_outdoor_temp).state;
source_label = "external";
} else if (!isnan(id(outdoor_temp_ntc).state)) {
outdoor = id(outdoor_temp_ntc).state;
source_label = "NTC";
}
if (isnan(outdoor)) {
flow_temp = 40.0;
source_label = "default";
} else {
// Curve: outdoor -10C -> 55C, outdoor +20C -> 25C
flow_temp = 55.0 - (outdoor - (-10.0)) * (55.0 - 25.0) / (20.0 - (-10.0));
if (flow_temp < 25.0) flow_temp = 25.0;
if (flow_temp > 55.0) flow_temp = 55.0; // auto mode cap
}
}
// Hard safety clamp on every path
if (flow_temp > 60.0) flow_temp = 60.0;
if (flow_temp < 20.0) flow_temp = 20.0;
auto call = id(ot_flow_temp_setpoint).make_call();
call.set_value(flow_temp);
call.perform();
ESP_LOGI("heating", "mode=%s demand=%s flow=%.1fC",
source_label, demand ? "YES" : "NO", flow_temp);Split gas consumption into separate CH and DHW streams. Useful for seeing seasonal patterns (winter = mostly CH, summer = mostly DHW).
You have two options: create the helpers via the HA UI (cleaner, no YAML risk)
or add them to configuration.yaml.
Settings → Devices & Services → Helpers → +
Create 3 integration helpers (Riemann sum):
- Name:
Gas Energy CH| Source:sensor.vitodens_opentherm_controller_gas_power_ch| Method:Trapezoidal rule| Unit time:h| Precision: 3 | Leave "Metric prefix" empty (see gotcha below) - Name:
Gas Energy DHW| Source:...gas_power_dhw| (same settings) - Name:
Gas Energy Total| Source:...gas_power_total| (same settings)
Then create 6 utility meters (daily + monthly per stream), each sourcing the corresponding integration helper above.
sensor:
- platform: integration
source: sensor.vitodens_opentherm_controller_gas_power_ch
name: gas_energy_kwh_ch
round: 3
method: trapezoidal
# NOTE: no unit_prefix. Source is kW -> result is natively kWh. Adding
# unit_prefix=k produces "kkWh" which is broken. See gotcha below.
- platform: integration
source: sensor.vitodens_opentherm_controller_gas_power_dhw
name: gas_energy_kwh_dhw
round: 3
method: trapezoidal
- platform: integration
source: sensor.vitodens_opentherm_controller_gas_power_total
name: gas_energy_kwh_total
round: 3
method: trapezoidal
utility_meter:
gas_ch_daily:
source: sensor.gas_energy_kwh_ch
cycle: daily
gas_ch_monthly:
source: sensor.gas_energy_kwh_ch
cycle: monthly
gas_dhw_daily:
source: sensor.gas_energy_kwh_dhw
cycle: daily
gas_dhw_monthly:
source: sensor.gas_energy_kwh_dhw
cycle: monthly
gas_total_daily:
source: sensor.gas_energy_kwh_total
cycle: daily
gas_total_monthly:
source: sensor.gas_energy_kwh_total
cycle: monthlySettings → Dashboards → Energy → Add gas source → Use an energy value and
pick sensor.vitodens_opentherm_controller_gas_energy_total (UI option) or
sensor.gas_energy_kwh_total (YAML option). The dashboard accepts gas in kWh
directly.
If you've seen other OpenTherm tutorials use unit_prefix: k, note: that
setting prepends the prefix to the source unit. With a source in kW, you
get "kkWh" (= MWh). Use unit_prefix: k only when your source is in W and
you want kWh output. For kW sources, omit entirely.
This is a modeled estimate (burner modulation × max capacity), not meter-grade
accurate. Typical error ±5-10% vs actual gas bill. Accurate enough for trend
analysis and seeing the CH/DHW split. CH vs DHW attribution depends on the
boiler correctly setting ch_active and dhw_active status bits - cross-check
against the Total stream if you see oddities.
Flow temperatures shown are primary side (what the ESP sends via OT). If your manifold has a heat exchanger between primary and secondary, the UFH loops see ~5°C lower due to the HEX's approach delta.
For a typical Continental European climate (design min -10°C, max +20°C):
| Outdoor | Primary Flow | After HEX (UFH loops) | Use case |
|---|---|---|---|
| -10°C | 55°C | ~50°C | Cold snap (auto cap) |
| -5°C | 50°C | ~45°C | Cold winter day |
| 0°C | 45°C | ~40°C | Typical winter |
| +5°C | 40°C | ~35°C | Cool |
| +10°C | 35°C | ~30°C | Mild autumn/spring |
| +15°C | 30°C | ~25°C | Warm |
| +20°C+ | 25°C | ~20°C | Off |
Slope: 1.0°C flow per 1°C outdoor. Adjust cold-end anchor if the house struggles on extreme cold days (raise) or overshoots in mild weather (lower).
UFH is sensitive to high flow temperatures (>50°C accelerates pipe aging; >60°C risks pipe damage and floor finish warping). The right approach is layered protection, not a single tight clamp:
| Layer | Mechanism | What it does |
|---|---|---|
| Weather comp curve | Lambda math | Normal operation never exceeds the curve max (55°C here) |
| ESP hard clamp | Lambda | Absolute ceiling regardless of manual input (60°C) |
| Heat exchanger | Mechanical | Always drops primary→secondary by 3-8°C - no electronics can bypass |
| Mixing valve (TMV) | Mechanical (wax thermostat) | Caps UFH loop temp at the dial setting, regardless of primary |
| Boiler internal max | Firmware parameter | Ultimate hardware backstop |
The HEX + TMV together mean you can run primary up to 60°C without UFH risk, because the UFH loops physically cannot see more than (TMV setting) regardless of what the ESP or boiler do. Set the TMV to 45-50°C as a real safety backstop (many factory-installed TMVs are left at the max setting of their dial, which effectively disables them).
| Scenario | Behavior |
|---|---|
| HA goes down | ESP continues operating autonomously - weather comp falls back to boiler NTC |
| WiFi goes down | ESP continues operating - OpenTherm is wired, WiFi not needed |
| ESP loses power | Boiler enters standby (no OT master) - safe state |
| External temp sensor fails | ESP falls back to boiler NTC (reads through OT) |
| Both temp sources fail | ESP sends a safe default (e.g. 40°C) so house doesn't go cold |
| Boiler fault | Boiler's safety systems handle it, ESP reads fault via OT |
| Zone controller fails | No heat demand signal - ESP idles setpoint to 20°C |
reboot_timeout: 0s on both wifi and api ensures the ESP never reboots
due to lost connectivity - it just keeps running the heating logic locally.
-
Viessmann requires
max_rel_mod_level,t_room,t_room_setinputs or it holds burner modulation at 0 for CH (DHW still works). Symptoms:ch_active=on, pump runs, water cools down, flame never lights. Most generic OpenTherm examples don't include these - the minimal "just sett_set" configs leave Vitodens unable to fire CH. -
ch_enable: trueis required in the hub block even if you have ach_enableswitch entity. The ESPHome OpenTherm schema docs state that CH enable is only sent if ALL of (hub flag, switch, setpoint) are truthy. -
Use
number(notoutput) fort_set. Theoutputplatform'sset_level()expects a 0.0-1.0 normalized value. Passing actual temperatures clamps to 1.0 = max_value. Use thenumberplatform andmake_call().set_value(temp_c). -
Refactor to a single
set_value()call at the bottom of the lambda. Early versions had it scattered across multiple branches; safety clamps weren't applied consistently. One variable → one clamp → one write is safer and readable. -
Separate "Active" and "Manual" setpoint entities if you want a manual override mode. A single entity that the lambda writes to every 10s appears read-only to users (their manual value gets overwritten within seconds). Two-entity design: the lambda writes an internal
t_setnumber (hidden from HA viainternal: true) and a templatesensormirrors it into the Sensors section for read-only visibility; a separate templatenumberstores the user's persistent override (used in manual mode, untouched otherwise). The mirror-sensor approach puts the active setpoint in the right place on the HA device card -numberentities can only live in the Controls or Diagnostic sections, never in Sensors. -
Min modulation on Vitodens B1HG is 12%. The boiler won't light below this (reported via OT MSG ID 15/17 pair). If your calculated gas-power is showing 1-3% during ramp-down, that's the OT sensor catching transient states; actual flame is off.
-
Vitodens 100-W doesn't support MSG ID 28 (Tret). Return temperature always reads 0. Comment out the
t_retsensor to avoid a misleading 0 in HA. -
Boiler's outdoor NTC can go bad silently. Check it against a known-good outdoor temp source (weather station, Ecowitt, etc.). Faulty NTC reading too high can trigger the boiler's internal summer economy shutoff, preventing CH firing even when OT demands it.
-
Zone controller's
delayed_offshould exceed the boiler's anti-short-cycle min-run time. Set to at least 2-3 minutes. Otherwise the ESP drops the setpoint to 20°C while the boiler is still in its mandatory minimum burn window, wasting gas on heat you don't need. -
B1HG doesn't implement OT counter Data-IDs (116/117/119/120/121/123 - burner/pump starts and hours). All six read 0 forever. Don't expose them as sensors - they're just dead entities cluttering the device card. The HMI service menu has the real counters if you need them.
-
diagnostic_indicationbinary is unreliable on B1HG. It stays OFF even whenoem_diagnostic_codereports real conditions (e.g. code 540 = condensate backup, boiler locked out). Omit the binary and alert onoem_diagnostic_code != 0instead.
One multi-condition automation handles all boiler-related alerts, with
each condition as a separate trigger id: routed through a choose:
action. Easy to extend - add a trigger + a matching branch.
Currently covered conditions:
(1) Boiler fault / diagnostic lockout. The OT fault channel and the OEM diagnostic channel are independent. Watch both:
binary_sensor.boiler_fault→ classic fault state (burner locked out).sensor.oem_fault_code→ specific Viessmann fault ID when a classic fault exists. Value255= "no specific OEM fault" (idle normal), treat as healthy.sensor.oem_diagnostic_code→ latching lockouts that bypass the fault channel (e.g. 540 = condensate backup in heat cell, which shows here but NOT inoem_fault_code). Value0= healthy.
Include in the notification payload: both codes, boiler water temp, CH pressure, flame state, DHW/CH demand flags - so the cause is often visible from the notification without opening HA.
(2) Low CH water pressure. Early warning before the boiler auto-locks out. Vitodens B1HG manual minimum is 1.0 bar; the boiler typically auto-shuts off around 0.8 bar with F.26 / F.11.
- Alert:
sensor.ch_water_pressure< 1.0 bar for 5 min. - Cleared: > 1.2 bar for 2 min (0.2 bar hysteresis to avoid flapping while topping up).
numeric_statetriggers naturally ignore unknown/unavailable states, so ESP reboots and OT comm loss do not cause false pressure alarms.
alias: "Vitodens Boiler Alerts"
mode: parallel # so fault and pressure don't block each other
max: 5
trigger:
- platform: state
entity_id: binary_sensor.vitodens_opentherm_controller_boiler_fault
from: "off"
to: "on"
for: {seconds: 30}
id: fault_on
- platform: state
entity_id: binary_sensor.vitodens_opentherm_controller_boiler_fault
from: "on"
to: "off"
id: fault_off
- platform: numeric_state
entity_id: sensor.vitodens_opentherm_controller_ch_water_pressure
below: 1.0
for: {minutes: 5}
id: pressure_low
- platform: numeric_state
entity_id: sensor.vitodens_opentherm_controller_ch_water_pressure
above: 1.2
for: {minutes: 2}
id: pressure_ok
action:
- choose:
- conditions: [{condition: trigger, id: fault_on}]
sequence:
- action: notify.your_channel
data:
title: "🔥 Boiler fault"
message: >-
Fault code: {{ states('sensor.vitodens_opentherm_controller_oem_fault_code') }}
Diag code: {{ states('sensor.vitodens_opentherm_controller_oem_diagnostic_code') }}
Boiler temp: {{ states('sensor.vitodens_opentherm_controller_boiler_water_temperature') }}°C
CH pressure: {{ states('sensor.vitodens_opentherm_controller_ch_water_pressure') }} bar
- conditions: [{condition: trigger, id: fault_off}]
sequence:
- action: notify.your_channel
data: {title: "✅ Boiler OK", message: "Fault cleared"}
- conditions: [{condition: trigger, id: pressure_low}]
sequence:
- action: notify.your_channel
data:
title: "💧 Low pressure"
message: >-
CH pressure below 1.0 bar:
{{ states('sensor.vitodens_opentherm_controller_ch_water_pressure') }} bar.
Top up via the fill valve to ~1.5 bar.
- conditions: [{condition: trigger, id: pressure_ok}]
sequence:
- action: notify.your_channel
data: {title: "✅ Pressure OK", message: "CH pressure restored"}Reset after a fault lockout is always done at the HMI with the two-button 4-second hold. The ESP has read-only access to the fault state over OT; it cannot clear the lockout.
Worth knowing in advance because it is a latching lockout that can surface on a healthy, well-maintained boiler after a long heating season.
Meaning: the boiler's safety logic has detected that condensate cannot drain out of the heat exchanger. Burner is locked out until manually reset.
Reported via OpenTherm as:
oem_diagnostic_code = 540oem_fault_code = 255(no classic OEM fault - F.540 rides the diagnostic channel, not the fault channel)binary_sensor.boiler_fault = on
First-line fix:
- Power off the boiler.
- Remove the condensate siphon/trap at the bottom of the heat exchanger.
- Rinse clean - typical culprit is sludge/debris accumulated in the U-bend. Even a partially-blocked trap can latch the safety.
- Refill the trap with clean water before reinstalling. Without the water seal, flue gases leak into the boiler cabinet.
- Inspect the full drain run downstream of the trap - any kink, sag, or blockage there will re-fault once water volume picks up.
- Power the boiler back on.
- Reset the burner manually from the HMI. F.540 is a latching
lockout - power-cycling alone does NOT clear it:
- Tap any key to surface the fault display.
- Press the two reset keys simultaneously for ~4 seconds until a rotating bar appears.
- If the condition has cleared, the home screen returns. If F.540 reappears immediately, the drain path is still blocked.
- Reset only works once the burner has cooled.
Risk factors (deep-condensing UFH setups are more exposed):
- Low flow temps produce more condensate per kWh than radiator-style high-temp operation. The drain path must move more water.
- A trap that dries out between heating seasons (DHW-only summer use) can crust up and partially block on first autumn startup.
- Installation with insufficient fall on the drain pipe, or without a proper air break at the household drain.
If it recurs more than once in a short window: the drain line itself is partially blocked downstream of the trap, or (worst case) the heat cell has damage. That's a service call. The manual warns: detach the fan unit before removing the burner, to prevent water damage.
- Vitodens 100-W B1HG Installation Manual (Viessmann doc 6227873) - available at viessmanndirect.co.uk
- ESPHome OpenTherm component: https://esphome.io/components/opentherm/
- DIYLESS OpenTherm shield: https://diyless.com/
- HA community thread on Vitodens OpenTherm: https://community.home-assistant.io/t/controlling-viessmann-vitodens-gas-heater-boilers-locally-new-models-100-w-200-w-after-2018/644447
- OpenTherm protocol spec: https://ihormelnyk.com/Content/Pages/opentherm_library/Opentherm%20Protocol%20v2-2.pdf