Adax WiFi Thermostat

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” :smiley: (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.

2 Likes

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

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.

Great that someone has a clue about this, I’ve never been able to write code for something like this. Do you think it works on the latest generation thermostat? Gen 3, I see there are some small differences on the board. I have 2 heaters with Gen 2 that are similar to the one in the picture above and 2 with gen 3. Gen 3 is Esp32 S3.

So, one of my adax heaters isn’t playing nice with the local, official integration, and I stumble upon this, and figure I’d give it a go… Except I can’t get it to compile…
Tried to copy as close as possible without success.

The esphome:

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_vavstuga"
  friendly_name: "Adax vävstuga"
  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
  ssid: !secret wifi_ssid
  password: !secret wifi_password

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); 

The result, normal stuff followed by:

src/esphome/components/7segment_gpio/7segment_gpio.cpp:189:51: warning: ignoring attribute 'section (".iram1.3")' because it conflicts with previous 'section (".iram1.1")' [-Wattributes]
  189 | void IRAM_ATTR HOT LcdDigitsData::timer_interrupt() {
      |                                                   ^
In file included from src/esphome/components/7segment_gpio/7segment_gpio.cpp:1:
src/esphome/components/7segment_gpio/7segment_gpio.h:70:22: note: previous declaration here
   70 |   void IRAM_ATTR HOT timer_interrupt();
      |                      ^~~~~~~~~~~~~~~
src/esphome/components/7segment_gpio/7segment_gpio.cpp: In member function 'void esphome::lcd_digits::LcdDigitsComponent::set_mode(Mode)':
src/esphome/components/7segment_gpio/7segment_gpio.cpp:355:5: error: 'timerAlarmEnable' was not declared in this scope; did you mean 'timerAlarm'?
  355 |     timerAlarmEnable(timer);
      |     ^~~~~~~~~~~~~~~~
      |     timerAlarm
src/esphome/components/7segment_gpio/7segment_gpio.cpp:358:5: error: 'timerAlarmDisable' was not declared in this scope
  358 |     timerAlarmDisable(timer);
      |     ^~~~~~~~~~~~~~~~~
src/esphome/components/7segment_gpio/7segment_gpio.cpp: In member function 'virtual void esphome::lcd_digits::LcdDigitsComponent::setup()':
src/esphome/components/7segment_gpio/7segment_gpio.cpp:436:21: error: too many arguments to function 'hw_timer_t* timerBegin(uint32_t)'
  436 |   timer = timerBegin(0, 80, true);
      |           ~~~~~~~~~~^~~~~~~~~~~~~
In file included from /data/cache/platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal.h:98,
                 from /data/cache/platformio/packages/framework-arduinoespressif32/cores/esp32/Arduino.h:44,
                 from src/esphome/core/macros.h:7,
                 from src/esphome.h:2,
                 from src/esphome/components/7segment_gpio/7segment_gpio.h:15:
/data/cache/platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-timer.h:35:13: note: declared here
   35 | hw_timer_t *timerBegin(uint32_t frequency);
      |             ^~~~~~~~~~
src/esphome/components/7segment_gpio/7segment_gpio.cpp:438:25: error: too many arguments to function 'void timerAttachInterrupt(hw_timer_t*, void (*)())'
  438 |     timerAttachInterrupt(timer, &s_timer_intr, true);
      |     ~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~
/data/cache/platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-timer.h:50:6: note: declared here
   50 | void timerAttachInterrupt(hw_timer_t *timer, void (*userFunc)(void));
      |      ^~~~~~~~~~~~~~~~~~~~
src/esphome/components/7segment_gpio/7segment_gpio.cpp:442:5: error: 'timerAlarmWrite' was not declared in this scope; did you mean 'timerWrite'?
  442 |     timerAlarmWrite(timer, 50, true);
      |     ^~~~~~~~~~~~~~~
      |     timerWrite
src/esphome/components/7segment_gpio/7segment_gpio.cpp:443:5: error: 'timerAlarmEnable' was not declared in this scope; did you mean 'timerAlarm'?
  443 |     timerAlarmEnable(timer);
      |     ^~~~~~~~~~~~~~~~
      |     timerAlarm
Compiling .pioenvs/adax_vavstuga/src/esphome/components/api/list_entities.cpp.o
Compiling .pioenvs/adax_vavstuga/src/esphome/components/api/proto.cpp.o
*** [.pioenvs/adax_vavstuga/src/esphome/components/7segment_gpio/7segment_gpio.cpp.o] Error 1
========================= [FAILED] Took 18.37 seconds =========================

I’ve been able to troubleshoot most esphome stuff I’ve run into, but external components are somewhat beyond me… All I can find is some stuff about timerAlarmEnable being removed from arduino esp32 3.0 api, which whilst probably interesting on some level, seems irrelevant since when I tried to set arduino to a pre-3.0 version I got an error saying it only supports 3.0 and higher or something similar.
Or is that the problem?

Edit: nevermind, seems that was indeed the problem… found a fork of 7segment_gpio that solved it.
4 more to go, then figure out how to do the same on my 2 esp8266 adax heaters.

Many thanks to McTB!

Only way to make it work is to figure out the connections on the esp. After that it should be pretty straight forward to modify the code. I “reverse engineered” mine with a multimeter.

Yes, I encountered the same problem with the display library. It is not compatible with the updated Arduino framework. I attempted to familiarize myself with the component system but eventually just implemented my own code to control the display using ESPHome GPIO.

It is nice to know that someone found this helpful :+1: Good luck with the ESP8266 :slight_smile:

I’ve replaced half of my thermostats now (the ones that were most unreliable), but I’m having a bit of a snag, since I’m not the one having to look at them most in the family…
I’ve made some changes to the code, and I don’t know if that’s causing it, but the display is incapable of showing 2 numbers at the same time. I realize it can’t actually do that, multiplex etc, but I’m under the impression that it should be able to switch between left and right so fast that it’s pretty invisible (and on the original firmware it is), but I’ve tried update_interval ranging from 1ms to 1s without and visible change, it just keeps switching, at around half a second intervals. Left-right-left-right…
Let’s just say I’m glad I’m not prone to seizure.
Does your Adax do this as well or does it look right?
I’ll take a disco thermostat that is reliable over
broken Adax original any day of the week, but if it doesn’t have to be stuck in the 90’s, even better. :smiley:

I do not have that problem with the display. It may be related to the forked library. I will post my current configuration, which does not use the library, later today.

Truly appreciate it!

Here is the latest configuration I am using. It is a quick hack to get the display working, no warranty for the code :wink:

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: esp-idf

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

#esp32_ble_tracker:
#  scan_parameters: 
#    active: True

#bluetooth_proxy:
#  active: True

time:
  - platform: homeassistant
    id: hassTime

globals:
  - id: interactTimeout
    type: int
    initial_value: "0"
  - id: displayText
    type: std::string
    initial_value: '"--"'
  - id: displayDigit
    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
    state_class: measurement
    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
    state_class: measurement
    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);
            }
          }

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);
  - interval: 5ms
    then:
      - lambda: |-
          unsigned long _Millis = millis();
          id(driveDisplay)->execute(_Millis);

output:
  - platform: gpio
    id: "displaySegmentA"
    pin:
      number: 23
      inverted: True
  - platform: gpio
    id: "displaySegmentB"
    pin:
      number: 21
      inverted: True
  - platform: gpio
    id: "displaySegmentC"
    pin:
      number: 25
      inverted: True
  - platform: gpio
    id: "displaySegmentD"
    pin:
      number: 26
      inverted: True
  - platform: gpio
    id: "displaySegmentE"
    pin:
      number: 33
      inverted: True
  - platform: gpio
    id: "displaySegmentF"
    pin:
      number: 32
      inverted: True
  - platform: gpio
    id: "displaySegmentG"
    pin:
      number: 4
      inverted: True
  - platform: gpio
    id: "displayDigit1"
    pin:
      number: 14
      inverted: True
  - platform: gpio
    id: "displayDigit2"
    pin:
      number: 13
      inverted: True

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: driveDisplay
    parameters:
      millis: int
    then:
      - lambda: |-
          auto get_segments = [](char ch) -> uint8_t {
            switch(ch) {
              case '0': return 0b01111110;
              case '1': return 0b00110000;
              case '2': return 0b01101101;
              case '3': return 0b01111001;
              case '4': return 0b00110011;
              case '5': return 0b01011011;
              case '6': return 0b01011111;
              case '7': return 0b01110000;
              case '8': return 0b01111111;
              case '9': return 0b01111011;
              case '-': return 0b00000001;
              case '_': return 0b00001000;
              case 'F': return 0b01000111;
              case 'N': return 0b01110110;
              case 'O': return 0b01111110;
              default:  return 0b00000000;
            }
          };
          auto set_digit = [&](int digit, uint8_t ch) {
            id(displayDigit1).set_state(0);
            id(displayDigit2).set_state(0);
            id(displaySegmentA).set_state(0);
            id(displaySegmentB).set_state(0);
            id(displaySegmentC).set_state(0);
            id(displaySegmentD).set_state(0);
            id(displaySegmentE).set_state(0);
            id(displaySegmentF).set_state(0);
            id(displaySegmentG).set_state(0);
            id(displayDigit1).set_state(digit == 0);
            id(displayDigit2).set_state(digit == 1);
            uint8_t segments = get_segments(ch);
            id(displaySegmentA).set_state(segments & 0b01000000);
            id(displaySegmentB).set_state(segments & 0b00100000);
            id(displaySegmentC).set_state(segments & 0b00010000);
            id(displaySegmentD).set_state(segments & 0b00001000);
            id(displaySegmentE).set_state(segments & 0b00000100);
            id(displaySegmentF).set_state(segments & 0b00000010);
            id(displaySegmentG).set_state(segments & 0b00000001);
          };
          std::string text = id(displayText);
          while (text.length() < 2) {
            text = " " + text;
          }
          set_digit(id(displayDigit), text[id(displayDigit)]);
          id(displayDigit)++;
          if (id(displayDigit) > 1) {
            id(displayDigit) = 0;
          }
  - 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) {
                id(displayText) = "-0";
              } else {
                char buf[4];
                if ((((millis - id(interactTimeout)) / 1000) % 2) == id(displayFractional)) {
                  sprintf(buf, "%02d", static_cast<int>(val));
                } else {
                  sprintf(buf, "_%d", static_cast<int>(round(val * 10.0f)) % 10);
                }
                id(displayText) = buf;
              }
              break;
            case ${DISPLAY_STATE_TURN_ON}:
              id(displayText) = "ON";
              break;
            case ${DISPLAY_STATE_TURN_OFF}:
              id(displayText) = "OF";
              break;
            default:
              id(displayText) = "  ";
          }
  - 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);

You are a hero!
It might not be the cleanest way (not that I’d know tbh), but it works as well as one could expect from a thermostat and that’s really the most important thing.
Now I can flash the rest of them without worrying about funny looks :rofl:

I figured I’d share this in case it’s of use to anyone else…
I’ve changed a few parts, but most is the same.
It works, but I’m not entirely sure about the board part, I can’t get OTA to work, complains about wrong flash size, from what I can find it seems to have an 8mb flash chip, but cba atm to figure that part out, just satisfied it works.

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-heater"
  friendly_name: "Adax heater"


esp8266:
  board: esp01_1m
 #   type: arduino
 #   type: esp-idf

logger:
  level: INFO

api:
  id: api_id

ota:
  - platform: esphome


wifi:
  id: wifi_id
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "Adax-heater"
    password: "password"

i2c:
  sda: GPIO2
  scl: GPIO14
  scan: True    

pcf8574:
  - id: 'pcf8574_hub'
    address: 0x20
    pcf8575: False

captive_portal:

web_server:
  port: 80
  version: 3

time:
  - platform: homeassistant
    id: hassTime

globals:
  - id: interactTimeout
    type: int
    initial_value: "0"

  - id: displayText
    type: std::string
    initial_value: '"--"'
  - id: displayDigit
    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_ON}
  - 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: GPIO1
      mode: output
      inverted: True

###
  - platform: gpio
    id: heatingLed
    pin:
      pcf8574: pcf8574_hub
      number: 7
#      number: GPIO19
      mode: output
      inverted: True
  - platform: gpio
    id: heatingControl
    pin:
      number: GPIO15
      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: GPIO0
      mode: input
      inverted: True
    id: minusButton
    on_multi_click:
      - timing:
          - ON for at most 1s
        then:
          - lambda: |-
              id(modTargetTemp)->execute(false, false);
  - platform: gpio
    pin:
      number: GPIO3
      mode: input
      inverted: True
    id: plusButton
    on_multi_click:
      - timing:
          - ON for at most 1s
        then:
          - lambda: |-
              id(modTargetTemp)->execute(true, false);
  - platform: gpio
    pin:
      number: GPIO4
      mode: input
      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
    state_class: measurement
    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
    state_class: measurement
    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: 1
    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);
            }
          };


interval:
  - interval: 60s
    then:
      - lambda: |-
          id(currentPower).publish_state(id(currentPower).state);


  - interval: 200ms
    then:
      - lambda: |-
          unsigned long _Millis = millis();
          id(setLedStates)->execute(_Millis);
          id(updateDisplay)->execute(_Millis);



  - interval: 10ms
    then:
      - lambda: |-
          unsigned long _Millis = millis();
          id(driveDisplay)->execute(_Millis);

output:
  - platform: gpio
    id: "displaySegmentA"
    pin:
      pcf8574: pcf8574_hub
#      number: 2
      number: 0
      inverted: True
  - platform: gpio
    id: "displaySegmentB"
    pin:
      pcf8574: pcf8574_hub
      number: 1
      inverted: True
  - platform: gpio
    id: "displaySegmentC"
    pin:
      pcf8574: pcf8574_hub
      number: 2
#      number: 0
      inverted: True
  - platform: gpio
    id: "displaySegmentD"
    pin:
      pcf8574: pcf8574_hub
#      number: 4
      number: 3
      inverted: True
  - platform: gpio
    id: "displaySegmentE"
    pin:
      pcf8574: pcf8574_hub
#      number: 3
      number: 4
      inverted: True
  - platform: gpio
    id: "displaySegmentF"
    pin:
      pcf8574: pcf8574_hub
      number: 5
      inverted: True
  - platform: gpio
    id: "displaySegmentG"
    pin:
      pcf8574: pcf8574_hub
      number: 6
      inverted: True
  - platform: gpio
    id: "displayDigit1"
    pin:
      number: 13
      inverted: True
  - platform: gpio
    id: "displayDigit2"
    pin:
      number: 12
      inverted: True

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(setConnectionLedState)->execute(id(wifi_id).is_connected() && (id(api_id).is_connected() || _LedOn));
          } else {
            id(setHeatingLedState)->execute(false);
            id(setConnectionLedState)->execute(false);
          };



  - id: driveDisplay
    parameters:
      millis: int
    then:
      - lambda: |-
          auto get_segments = [](char ch) -> uint8_t {
            switch(ch) {
              case '0': return 0b01111110;
              case '1': return 0b00110000;
              case '2': return 0b01101101;
              case '3': return 0b01111001;
              case '4': return 0b00110011;
              case '5': return 0b01011011;
              case '6': return 0b01011111;
              case '7': return 0b01110000;
              case '8': return 0b01111111;
              case '9': return 0b01111011;
              case '-': return 0b00000001;
              case '_': return 0b00001000;
              case 'F': return 0b01000111;
              case 'N': return 0b01110110;
              case 'O': return 0b01111110;
              default:  return 0b00000000;
            }
          };
          auto set_digit = [&](int digit, uint8_t ch) {
            id(displayDigit1).set_state(0);
            id(displayDigit2).set_state(0);
            id(displaySegmentA).set_state(0);
            id(displaySegmentB).set_state(0);
            id(displaySegmentC).set_state(0);
            id(displaySegmentD).set_state(0);
            id(displaySegmentE).set_state(0);
            id(displaySegmentF).set_state(0);
            id(displaySegmentG).set_state(0);
            id(displayDigit1).set_state(digit == 0);
            id(displayDigit2).set_state(digit == 1);
            uint8_t segments = get_segments(ch);
            id(displaySegmentA).set_state(segments & 0b01000000);
            id(displaySegmentB).set_state(segments & 0b00100000);
            id(displaySegmentC).set_state(segments & 0b00010000);
            id(displaySegmentD).set_state(segments & 0b00001000);
            id(displaySegmentE).set_state(segments & 0b00000100);
            id(displaySegmentF).set_state(segments & 0b00000010);
            id(displaySegmentG).set_state(segments & 0b00000001);
          };
          std::string text = id(displayText);
          while (text.length() < 2) {
            text = " " + text;
          }
          set_digit(id(displayDigit), text[id(displayDigit)]);
          id(displayDigit)++;
          if (id(displayDigit) > 1) {
            id(displayDigit) = 0;
          }
  - id: updateDisplay
    parameters:
      millis: int
    then: 
      - lambda: |-
         // Always-on display logic, no decimals
         if (id(displayMode).active_index() == 0) {
           // Display OFF
           id(displayText) = "  ";
           return;
         }

         // Heating OFF → show "--"
         if (id(climateControl).mode != CLIMATE_MODE_HEAT) {
           id(displayText) = "--";
           return;
         }

         // Heating ON → show current temperature (integer)
         float temp = id(climateControl).target_temperature;
         int val = static_cast<int>(round(temp));

         // Clamp just in case
         val = std::max(-9, std::min(99, val));

         char buf[3];
         snprintf(buf, sizeof(buf), "%02d", val);
         id(displayText) = buf;



  - 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);

That board defines just 1mb flash. Try with:
board: nodemcuv2

I’ll try that tonight.
It shouldn’t bother me, as it does what it’s supposed to, but knowing that I’ll have to physically remove it, solder on wires re-flash, de-solder and then put it back in place if I want/need to change anything… It does bother me :stuck_out_tongue:

Yep, and it doesn’t make sense to configure 1mb flash if you actually have 4 (or more).

I really should stop fiddling with this kind of stuff when it’s time for bed and my brain isn’t properly awake…
I find a chip labeled “adesto1820 25SF081 SSHD”, seems to match AT25SF081, supposed to be 8Mbit, and fail to make the connection. I guess it did seem odd that they’d use something so large…
So, it should be 1MB I’m guessing, which makes the error sorta confusing…

Processing adax-undantag-gavel (board: esp01_1m; framework: arduino; platform: platformio/[email protected])
--------------------------------------------------------------------------------
HARDWARE: ESP8266 80MHz, 80KB RAM, 1MB Flash
Dependency Graph
|-- ESPAsyncTCP @ 2.0.0
|-- ESP8266WiFi @ 1.0
|-- ESPAsyncWebServer @ 3.7.10
|-- DNSServer @ 1.1.1
|-- ESP8266mDNS @ 1.2
|-- Wire @ 1.0
|-- ArduinoJson @ 7.4.2
RAM:   [====      ]  43.4% (used 35556 bytes from 81920 bytes)
Flash: [=====     ]  47.1% (used 482289 bytes from 1023984 bytes)
========================= [SUCCESS] Took 2.08 seconds =========================
INFO Successfully compiled program.
INFO Connecting to 192.168.5.178 port 8266...
INFO Connected to 192.168.5.178
INFO Uploading /data/build/adax-undantag-gavel/.pioenvs/adax-undantag-gavel/firmware.bin (486448 bytes)
INFO Compressed to 337532 bytes
ERROR Error binary size: Error: ESP has been flashed with wrong flash size. Please choose the correct 'board' option (esp01_1m always works) and then flash over USB.
WARNING Failed to upload to ['192.168.5.178']

Why say “esp01_1m always works”, when that is what I’m using?

Yes.
So what’s your board config for that error?

the board related part is simply

esp8266:
  board: esp01_1m

Nothing obvious comes to my mind.
Try to flash with serial:

esp8266:
  board: esp01_1m
  board_flash_mode: dout

And your setup is on the borderline for OTA anyway, try to clean up something…