McTB
January 3, 2025, 10:37pm
1
I have created a configuration for an Adax WiFi thermostat, board version 5.2a. It uses an ESP32-WROOM-32D module, and the board has test pads for easy programming. I have these thermostats in Adax Neo radiators, but I think they can be used in other models too.
These boards do not measure current, so I have implemented an energy consumption estimation based on the radiator’s power. Also, I don’t know what type of NTC these radiators use, so the type is currently just a “guesstimate” (any help on identifying the type would be appreciated).
Just posting here in case someone is interested. If there is interest, I can make some kind of documentation awailable.
markk71
September 12, 2025, 2:43pm
2
Hopefully not too late - would be very interested to see any configuration/documentation you might have as I’d like to bring my 2 neo heaters into esphome (they have become very slow to respond via the cloud API).
Any hints on how to open up the control box part of the heater?
Thanks and regards
McTB
October 3, 2025, 2:21pm
3
Sorry, I haven’t been checking here for a while.
The case opens by pushing the outer shell “out” with a small flathead screwdriver between the parts at the clips.
Here is the pinout for the test pads.
And here is the latest configuration I am using.
substitutions:
DEFAULT_CURRENT_IDLE: "0.033"
DEFAULT_CURRENT_HEATING: "0.033"
DEFAULT_VOLTAGE: "230"
NTC_SELF_HEATING_COMPENSATION: "-0.2"
MIN_TEMPERATURE: "5.0"
MAX_TEMPERATURE: "25.0"
DISPLAY_TIMEOUT: "10000"
DISPLAY_MODE_OFF: "Off"
DISPLAY_MODE_INTERACT: "Interact"
DISPLAY_MODE_ON: "On"
DISPLAY_STATE_NONE: "0"
DISPLAY_STATE_CURRENT_TEMP: "1"
DISPLAY_STATE_TARGET_TEMP: "2"
DISPLAY_STATE_TURN_ON: "3"
DISPLAY_STATE_TURN_OFF: "4"
esphome:
name: "adax-wifi"
friendly_name: "Adax Wifi"
name_add_mac_suffix: True
esp32:
board: esp32dev
flash_size: 8MB
framework:
type: arduino
logger:
level: INFO
api:
id: api_id
ota:
- platform: esphome
wifi:
id: wifi_id
ap:
password: "12345678"
captive_portal:
web_server:
port: 80
version: 3
time:
- platform: homeassistant
id: hassTime
external_components:
source: github://asergunov/7segment_gpio
globals:
- id: interactTimeout
type: int
initial_value: "0"
- id: displayState
type: int
initial_value: ${DISPLAY_STATE_NONE}
- id: displayFractional
type: bool
initial_value: "false"
number:
- platform: template
id: currentIdle
name: "Current Idle"
icon: mdi:flash-off-outline
mode: BOX
unit_of_measurement: "A"
disabled_by_default: True
entity_category: CONFIG
min_value: 0
max_value: 1
step: 0.001
restore_value: True
initial_value: ${DEFAULT_CURRENT_IDLE}
optimistic: True
set_action:
then:
- component.update: currentPower
- platform: template
id: currentHeating
name: "Current Heating"
icon: mdi:flash-outline
mode: BOX
unit_of_measurement: "A"
disabled_by_default: True
entity_category: CONFIG
min_value: 0
max_value: 12
step: 0.001
restore_value: True
initial_value: ${DEFAULT_CURRENT_HEATING}
optimistic: True
set_action:
then:
- component.update: currentPower
- platform: template
id: voltage
name: "Voltage"
icon: mdi:sine-wave
mode: BOX
unit_of_measurement: "V"
disabled_by_default: True
entity_category: CONFIG
min_value: 100
max_value: 300
step: 0.1
restore_value: False
initial_value: ${DEFAULT_VOLTAGE}
optimistic: True
set_action:
then:
- component.update: currentPower
- platform: template
id: temperatureCompensation
name: "Temperature Compensation"
icon: mdi:thermometer-minus
mode: BOX
unit_of_measurement: "°C"
disabled_by_default: True
entity_category: CONFIG
min_value: -10
max_value: 10
step: 0.01
restore_value: True
initial_value: 0
optimistic: True
select:
- platform: template
id: displayMode
name: "Display Mode"
icon: mdi:counter
disabled_by_default: True
entity_category: CONFIG
optimistic: True
restore_value: True
options:
- ${DISPLAY_MODE_OFF}
- ${DISPLAY_MODE_INTERACT}
- ${DISPLAY_MODE_ON}
initial_option: ${DISPLAY_MODE_INTERACT}
- platform: template
id: ledMode
name: "Led Mode"
icon: mdi:led-on
disabled_by_default: True
entity_category: CONFIG
optimistic: True
restore_value: True
options:
- ${DISPLAY_MODE_OFF}
- ${DISPLAY_MODE_INTERACT}
- ${DISPLAY_MODE_ON}
initial_option: ${DISPLAY_MODE_ON}
switch:
- platform: factory_reset
id: factoryReset
internal: True
- platform: gpio
id: connectionLed
pin:
number: GPIO18
mode: output
inverted: True
- platform: gpio
id: heatingLed
pin:
number: GPIO19
mode: output
inverted: True
- platform: gpio
id: heatingControl
pin:
number: GPIO27
mode: output
on_turn_on:
then:
- component.update: currentPower
on_turn_off:
then:
- component.update: currentPower
- platform: template
id: childLock
name: "Child Lock"
icon: mdi:lock
disabled_by_default: True
entity_category: CONFIG
optimistic: True
restore_mode: RESTORE_DEFAULT_OFF
button:
- platform: restart
name: "Restart"
disabled_by_default: True
entity_category: DIAGNOSTIC
- platform: factory_reset
name: "Factory Reset"
disabled_by_default: True
entity_category: DIAGNOSTIC
binary_sensor:
- platform: gpio
pin:
number: GPIO22
mode:
input: True
inverted: True
id: minusButton
on_multi_click:
- timing:
- ON for at most 1s
then:
- lambda: id(modTargetTemp)->execute(false, false);
- platform: gpio
pin:
number: GPIO35
mode:
input: True
inverted: True
id: plusButton
on_multi_click:
- timing:
- ON for at most 1s
then:
- lambda: id(modTargetTemp)->execute(true, false);
- platform: gpio
pin:
number: GPIO34
mode:
input: True
inverted: True
id: okButton
on_multi_click:
- timing:
- ON for at most 1s
then:
- lambda: id(setDisplayState)->execute(${DISPLAY_STATE_CURRENT_TEMP}, false);
- timing:
- ON for 3s to 10s
then:
- lambda: |-
if (!id(childLock).state) {
auto call = id(climateControl).make_call();
if (id(climateControl).mode == CLIMATE_MODE_OFF) {
call.set_mode("HEAT");
} else {
call.set_mode("OFF");
}
call.perform();
if (id(climateControl).mode == CLIMATE_MODE_OFF) {
id(setDisplayState)->execute(${DISPLAY_STATE_TURN_OFF}, false);
} else {
id(setDisplayState)->execute(${DISPLAY_STATE_TURN_ON}, false);
}
}
- timing:
- ON for at least 10s
then:
- lambda: if (!id(childLock).state) { id(factoryReset).turn_on(); }
- platform: template
name: "Heating"
icon: mdi:radiator
disabled_by_default: True
condition:
switch.is_on:
id: heatingControl
sensor:
- platform: adc
id: tempSensorVoltage
pin: A0
filters:
- sliding_window_moving_average:
window_size: 24
send_every: 12
update_interval: 5s
- platform: resistance
id: tempSensorResistance
sensor: tempSensorVoltage
configuration: DOWNSTREAM
resistor: 91kOhm
reference_voltage: 2.925
- platform: ntc
id: currentTemp
name: "Current Temperature"
icon: mdi:thermometer
unit_of_measurement: "°C"
device_class: temperature
disabled_by_default: True
accuracy_decimals: 1
sensor: tempSensorResistance
calibration:
reference_temperature: 25°C
reference_resistance: 10kOhm
b_constant: 3950
filters:
- lambda: return x + ${NTC_SELF_HEATING_COMPENSATION} + id(temperatureCompensation).state;
- platform: template
name: "Current Power"
id: currentPower
unit_of_measurement: "W"
accuracy_decimals: 1
device_class: power
disabled_by_default: True
lambda: |-
if (id(heatingControl).state) {
return id(currentHeating).state * id(voltage).state;
} else {
return id(currentIdle).state * id(voltage).state;
}
- platform: total_daily_energy
name: "Energy Consumption"
power_id: currentPower
unit_of_measurement: "kWh"
device_class: energy
disabled_by_default: True
method: left
accuracy_decimals: 3
filters:
- multiply: 0.001
- platform: wifi_signal
name: "Signal dB"
icon: mdi:wifi-strength-3
disabled_by_default: True
entity_category: DIAGNOSTIC
- platform: template
id: targetTemperature
name: "Target temperature"
icon: mdi:thermometer-lines
unit_of_measurement: "°C"
device_class: temperature
disabled_by_default: True
accuracy_decimals: 1
lambda: return id(climateControl).target_temperature;
text_sensor:
- platform: wifi_info
ip_address:
name: "IP Address"
icon: mdi:ip
disabled_by_default: True
entity_category: DIAGNOSTIC
ssid:
name: "Network SSID"
icon: mdi:wifi
disabled_by_default: True
entity_category: DIAGNOSTIC
mac_address:
name: "MAC Address"
icon: mdi:numeric
disabled_by_default: True
entity_category: DIAGNOSTIC
climate:
- platform: thermostat
id: climateControl
name: "Thermostat"
visual:
min_temperature: ${MIN_TEMPERATURE}
max_temperature: ${MAX_TEMPERATURE}
temperature_step: 0.5
sensor: currentTemp
min_heating_off_time: 10s
min_heating_run_time: 10s
min_idle_time: 10s
heat_deadband: 0.4
heat_overrun: 0.0
heat_action:
- switch.turn_on: heatingControl
idle_action:
- switch.turn_off: heatingControl
target_temperature_change_action:
then:
- lambda: |-
id(targetTemperature).publish_state(id(climateControl).target_temperature);
on_control:
- lambda: |-
if (id(climateControl).mode == CLIMATE_MODE_HEAT) {
if (x.get_mode() == CLIMATE_MODE_OFF) {
id(setDisplayState)->execute(${DISPLAY_STATE_TURN_OFF}, false);
} else if (id(climateControl).target_temperature != x.get_target_temperature()) {
id(setDisplayState)->execute(${DISPLAY_STATE_TARGET_TEMP}, false);
}
} else {
if (x.get_mode() == CLIMATE_MODE_HEAT) {
id(setDisplayState)->execute(${DISPLAY_STATE_TURN_ON}, false);
} else if (id(climateControl).target_temperature != x.get_target_temperature()) {
id(setDisplayState)->execute(${DISPLAY_STATE_TARGET_TEMP}, false);
}
}
display:
- platform: 7segment_gpio
id: lcd
digit_pins: [14, 13]
iterate_digits: True
segment_pins: [4, 32, 33, 26, 25, 21, 23]
compensate_brightness: True
intensity: 0
update_interval: 200ms
interval:
- interval: 60s
then:
- lambda: id(currentPower).publish_state(id(currentPower).state);
- interval: 200ms
then:
- lambda: |-
unsigned long _Millis = millis();
id(setLedStates)->execute(_Millis);
if (_Millis - id(interactTimeout) > ${DISPLAY_TIMEOUT}) {
id(displayState) = ${DISPLAY_STATE_NONE};
}
id(updateDisplay)->execute(_Millis);
script:
- id: setHeatingLedState
parameters:
state: bool
then:
- lambda: |-
if (state) {
if (!id(heatingLed).state) {
id(heatingLed).turn_on();
}
} else {
if (id(heatingLed).state) {
id(heatingLed).turn_off();
}
}
- id: setConnectionLedState
parameters:
state: bool
then:
- lambda: |-
if (state) {
if (!id(connectionLed).state) {
id(connectionLed).turn_on();
}
} else {
if (id(connectionLed).state) {
id(connectionLed).turn_off();
}
}
- id: setLedStates
parameters:
millis: int
then:
- lambda: |-
auto _LedMode = id(ledMode).active_index();
if ((_LedMode == 2) || ((_LedMode == 1) && ((millis - id(interactTimeout)) < ${DISPLAY_TIMEOUT}))) {
bool _LedOn = (millis / 1000) % 2 == 0;
id(setHeatingLedState)->execute(id(heatingControl).state || ((id(climateControl).mode == CLIMATE_MODE_HEAT) && _LedOn));
id(setConnectionLedState)->execute(id(wifi_id).is_connected() && (id(api_id).is_connected() || _LedOn));
} else {
id(setHeatingLedState)->execute(false);
id(setConnectionLedState)->execute(false);
}
- id: updateDisplay
parameters:
millis: int
then:
- lambda: |-
auto _displayMode = id(displayMode).active_index();
int _displayState = id(displayState);
if (_displayMode == 0) {
_displayState = ${DISPLAY_STATE_NONE};
} else {
if (millis - id(interactTimeout) > ${DISPLAY_TIMEOUT}) {
if (_displayMode == 1) {
_displayState = ${DISPLAY_STATE_NONE};
} else {
_displayState = ${DISPLAY_STATE_CURRENT_TEMP};
}
}
}
switch(_displayState) {
case ${DISPLAY_STATE_CURRENT_TEMP}:
case ${DISPLAY_STATE_TARGET_TEMP}:
float val;
if (_displayState == ${DISPLAY_STATE_CURRENT_TEMP}) {
val = id(climateControl).current_temperature;
} else {
val = id(climateControl).target_temperature;
}
val = round(val * 10.0f) / 10.0f;
if (val < 0) {
lcd->print("-0");
} else {
if ((((millis - id(interactTimeout)) / 1000) % 2) == id(displayFractional)) {
lcd->printf(0, "%02d", static_cast<int>(val));
} else {
lcd->printf(0, "_%d", static_cast<int>(round(val * 10.0f)) % 10);
}
}
break;
case ${DISPLAY_STATE_TURN_ON}:
lcd->print("ON");
break;
case ${DISPLAY_STATE_TURN_OFF}:
lcd->print("OF");
break;
default:
lcd->print(" ");
}
- id: setDisplayState
parameters:
state: int
fractional: bool
then:
- lambda: |-
unsigned long _Millis = millis();
id(displayFractional) = fractional;
id(interactTimeout) = _Millis;
id(displayState) = state;
id(updateDisplay)->execute(_Millis);
- id: modTargetTemp
parameters:
up: bool
large: bool
then:
- lambda: |-
bool _displayFractional = false;
if (id(displayState) == ${DISPLAY_STATE_TARGET_TEMP}) {
if (!id(childLock).state) {
double _current = id(climateControl).target_temperature;
double _value = _current + (up ? (large ? 1.0f : 0.5f) : (large ? -1.0f : -0.5f));
_value = round(_value * 10.0f) / 10.0f;
_value = std::min(${MAX_TEMPERATURE}, std::max(${MIN_TEMPERATURE}, _value));
_displayFractional = floor(_value) == floor(_current);
auto call = id(climateControl).make_call();
call.set_target_temperature(_value);
call.perform();
}
}
id(setDisplayState)->execute(${DISPLAY_STATE_TARGET_TEMP}, _displayFractional);
There is also a PCF8563 Real-time clock/calendar ic connected on gpio16/sda and gpio17/scl. I have not tested this as there is no use for me with it.