ESPhome and modbus controller select: how does the lambda work?

After my last topic I integrated my heat pump in HA via an ESP32 board. This works great but I would like to be able to select the heating/cooling mode. A text_sensor is already available to read the current setting:

text_sensor:
    # System heating mode
  - platform: modbus_controller
    modbus_controller_id: mitsubishi
    id: mitsubishi_heating_mode
    name: "System heating mode"
    icon: mdi:radiator
    address: 0x1C ## FC3: 28
    register_type: holding
    bitmask: 0
    raw_encode: HEXBYTES
    lambda: |-
      uint16_t value = modbus_controller::word_from_hex_str(x, 0);
      ESP_LOGD("main", "Value for heating mode %d", value);
      switch (value) {
        case 0: return std::string("Heating room temperature");
        case 1: return std::string("Heating flow temperature");
        case 2: return std::string("Heating curve");
        case 3: return std::string("Cooling room temperature");
        case 4: return std::string("Cooling flow temperature");
        case 5: return std::string("Heating startup procedure");
        default: return std::string("Unknown");
      }
      return x;

I have examined the docs and I think this should best be accomplished by a modbus controller select item. Unfortunately didn’t write the code I already have, and I can only follow partly what is happening in the lambda’s.

I should construct something as this:

    ## Set H/C mode
  - platform: modbus_controller
    modbus_controller_id: mitsubishi
    id: mitsubishi_set_hc_mode
    name: "Select H/C mode"
    icon: mdi:hvac
    address: 0x1C ## FC3: 28
    optionsmap: ## [str, int]
      "Heating room temperature": 0
      "Heating flow temperature": 1
      "Heating curve": 2
      "Cooling room temperature": 3
      "Cooling flow temperature": 4
    lambda: "??? "
    write_lambda: |-
      ???

But what should I put in the lambda and write_lambda? Unfortunately I don’t see a lot of examples on the internet, especially for the ESPhome modbus controller select option. If I take a lambda structure as for other registers I only get errors. Example of a working number:

number:
    ## Set tank temperature
  - platform: modbus_controller
    modbus_controller_id: mitsubishi
    id: mitsubishi_wp_set_tank_temperature
    name: "Set target tank temperature"
    icon: mdi:water-thermometer-outline
    address: 0x1F ## FC3: 31
    unit_of_measurement: "°C"
    register_type: holding
    value_type: U_WORD
    mode: box
    step: 0.5
    entity_category: config
    min_value: 30
    max_value: 59
    lambda: "return  x / 100; "
    write_lambda: |-
      uint16_t newtemp = x*100;
      ESP_LOGI("main", "Set tank temperature %d", newtemp);
      // Create a modbus command item with the flow temperature as the payload
      esphome::modbus_controller::ModbusCommandItem set_payload_command = esphome::modbus_controller::ModbusCommandItem::create_write_single_command(mitsubishi, 0x1E, newtemp);
      // Submit the command to the send queue
      mitsubishi->queue_command(set_payload_command);
      return {}; 

And if I look in the documentation the structure of the lambda’s should be different but I can figure out how. Does anyone know?

Yes. This sort of thing has been puzzling me too. Been trying to extract UART and Modbus data without success.
What are these types of lambda expressions called in C++, so I can do some research into them? Stuff like the double colon and “pushback” for example.

In the lambda is classic C code, and the lambda in this case includes a ‘switch’ statement (sometimes called a ‘case’ statement).
It returns to the surrounding component, e.g. the text_sensor, a pointer to a string which the component will then display as its current state. The string in question is chosen by the value tested-for by the switch statement’s ‘case’ selectors.
Alternatively, you can use the ‘optionsmap’ to achieve a similar result. They’re both doing the same thing: handing the component a string, based on the value of the device the component (here, a text_sensor) is working with (i.e. the modbus_controller).

I helped another user a few weeks ago with this same issue. Perhaps this is where you got some of the code you’re using.

@glyndon interesting case but I find it difficult to project on my own situation.

I’ve tried to build this on basis of the on/off switch for vacation mode which is basically writing a 0 or 1 to a similar register:

select:
  - platform: modbus_controller
    modbus_controller_id: mitsubishi
    id: mitsubishi_select_hc_mode
    name: "System H/C mode"
    icon: mdi:hvac
    address: 0x1C ## FC3: 28
#    register_type: holding
#    value_type: U_WORD
    optionsmap:
      "Heating Room Temp": 0
      "Heating Flow Temp": 1
      "Heating Heat Curve": 2
      "Cooling Room Temp": 3
      "Cooling Flow Temp": 4
      "Floor Dryup": 5
    lambda: return <std::string>
    write_lambda: |-
      uint16_t hc_mode = x;
      ESP_LOGI("main", "Set H/C mode %d", x);
      // Create a modbus command item with the h/c mode as the payload
      esphome::modbus_controller::ModbusCommandItem set_payload_command = esphome::modbus_controller::ModbusCommandItem::create_write_single_command(mitsubishi, 0x1C, x);
      // Submit the command to the send queue
      mitsubishi->queue_command(set_payload_command);
      return {};

Installing did not succeed but at least I got some logging to search for improvement :wink:

Compiling /data/esp32-warmtepomp/.pioenvs/esp32-warmtepomp/src/main.cpp.o
/config/esphome/esp-mitsubishi.yaml: In lambda function:
/config/esphome/esp-mitsubishi.yaml:877:14: error: expected primary-expression before '<' token
     lambda: return <std::string>
              ^
/config/esphome/esp-mitsubishi.yaml:877:26: error: expected primary-expression before '>' token
     lambda: return <std::string>
                          ^
/config/esphome/esp-mitsubishi.yaml:878:3: error: expected primary-expression before '}' token
     write_lambda: |-
   ^
/config/esphome/esp-mitsubishi.yaml:878:3: error: expected ';' before '}' token
/config/esphome/esp-mitsubishi.yaml: In lambda function:
/config/esphome/esp-mitsubishi.yaml:879:26: error: cannot convert 'const string {aka const std::__cxx11::basic_string<char>}' to 'uint16_t {aka short unsigned int}' in initialization
       uint16_t hc_mode = x;
                          ^
In file included from src/esphome/components/sensor/sensor.h:3:0,
                 from src/esphome/core/application.h:16,
                 from src/esphome/components/api/api_connection.h:4,
                 from src/esphome.h:3,
                 from src/main.cpp:3:
src/esphome/core/log.h:105:99: warning: format '%d' expects argument of type 'int', but argument 5 has type 'const string* {aka const std::__cxx11::basic_string<char>*}' [-Wformat=]
   esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
                                                                                                   ^
src/esphome/core/log.h:150:28: note: in expansion of macro 'esph_log_i'
 #define ESP_LOGI(tag, ...) esph_log_i(tag, __VA_ARGS__)
                            ^
/config/esphome/esp-mitsubishi.yaml:880:7: note: in expansion of macro 'ESP_LOGI'
       ESP_LOGI("main", "Set H/C mode %d", x);
       ^
/config/esphome/esp-mitsubishi.yaml:882:169: error: no matching function for call to 'esphome::modbus_controller::ModbusCommandItem::create_write_single_command(esphome::modbus_controller::ModbusController*&, int, const string&)'
       esphome::modbus_controller::ModbusCommandItem set_payload_command = esphome::modbus_controller::ModbusCommandItem::create_write_single_command(mitsubishi, 0x1C, x);
                                                                                                                                                                         ^
In file included from src/esphome.h:25:0,
                 from src/main.cpp:3:
src/esphome/components/modbus_controller/modbus_controller.h:355:28: note: candidate: static esphome::modbus_controller::ModbusCommandItem esphome::modbus_controller::ModbusCommandItem::create_write_single_command(esphome::modbus_controller::ModbusController*, uint16_t, uint16_t)
   static ModbusCommandItem create_write_single_command(ModbusController *modbusdevice, uint16_t start_address,
                            ^
src/esphome/components/modbus_controller/modbus_controller.h:355:28: note:   no known conversion for argument 3 from 'const string {aka const std::__cxx11::basic_string<char>}' to 'uint16_t {aka short unsigned int}'
*** [/data/esp32-warmtepomp/.pioenvs/esp32-warmtepomp/src/main.cpp.o] Error 1
========================= [FAILED] Took 17.90 seconds =========================

select: starts on row 861

Decided to look for another approach: an input number should theoretically work:

  - platform: modbus_controller
    modbus_controller_id: mitsubishi
    id: mitsubishi_set_hc_mode
    name: "H/C mode"
    icon: mdi:hvac
    address: 0x1C ## FC3: 28
    value_type: U_WORD
    mode: box
    step: 1.0
    entity_category: config
    min_value: 0
    max_value: 5
    lambda: "return x; "
    write_lambda: |-
      uint16_t newmode = x*100;
      ESP_LOGI("main", "Set H/C ,mode %d", newmode);
      // Create a modbus command item with the mode as the payload
      esphome::modbus_controller::ModbusCommandItem set_payload_command = esphome::modbus_controller::ModbusCommandItem::create_write_single_command(mitsubishi, 0x1C, newmode);
      // Submit the command to the send queue
      mitsubishi->queue_command(set_payload_command);
      return {}; 

But it doesn’t seem to as the manual input is not taken over :frowning:
The construction is the same as working input numbers I have.

What does work:

  - platform: modbus_controller
    modbus_controller_id: mitsubishi
    id: mitsubishi_set_hc_mode
    name: "H/C mode"
    icon: mdi:hvac
    address: 0x1C ## FC3: 28
    bitmask: 1
    value_type: U_WORD
    mode: box
    step: 1.0
    entity_category: config
    min_value: 0
    max_value: 5
    lambda: "return x; "
    write_lambda: |-
      uint16_t newmode = x;
      ESP_LOGI("main", "Set H/C ,mode %d", newmode);
      // Create a modbus command item with the mode as the payload
      esphome::modbus_controller::ModbusCommandItem set_payload_command = esphome::modbus_controller::ModbusCommandItem::create_write_single_command(mitsubishi, 0x1C, newmode);
      // Submit the command to the send queue
      mitsubishi->queue_command(set_payload_command);
      return {}; 

Hopefully I can also adapt this to a selection menu.

From another user and forum i got this solution and it works:

select:
  - platform: modbus_controller
    id: mitsubishi_set_hc_mode
    name: "H/C mode"
    address: 0x1C ## FC6: 28
    optionsmap:
      "Heating room": 0
      "Heating flow": 1
      "Heating curve": 2
      "Cooling room": 3
      "Cooling flow": 4
      "Heating startup": 5

Sometimes it’s not so hard as it seems :innocent:

3 Likes

Hi @breinonline

Could you please give all code below Select, because I’m trying to understand how to use modbus select in my case, because I have modes for different thermo pump model. I was using switches instead, but I would like to use select function. Thank you in advance

This is all the code :slight_smile:

As far as I understand: it writes the number 0 to 5 corresponding to the option you select in the menu, to the modbus address involved.

@breinonline Thank you for quick response. You are lucky - I need to write a multiple Bitmask into one single address to change my modes as follow

#    name: "Hotwater"
    register_type: holding
    address: 0x002
    bitmask: 0x002
    
  - platform: template
    id: hotwater_only
    name: "Hot Water Only"
    lambda: |-
      if (id(mode_hvac).state == 2) {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - switch.turn_on: turn_off
      - delay: 2s
      - switch.turn_on: mode_hotwater
      - delay: 2s
      - switch.turn_on: turn_on
    turn_off_action:
      - switch.turn_on: turn_off

Probably I’ll stick with template

Hi Breinonline, how can this be done with bitmasking?

When i try to simply add bitmask: xxxx i am prompted that ‘bitmask’ is not valid.

Any ideas?

Unfortunately, no.

this solution worked flawlessly! Just add this to an existing config (you still need a fully configured text_sensor for the same “address” for it to work). Name/id doesn’t matter (e.g. it should not be the same as for text_sensor for reference) - I believe it just takes “address” and matches with that :slight_smile: