How do I write a "Switch-Case" construct?

Hi,
I am trying to write a bit of yaml code to read and display the charging state of an EPEver PWM Solar Charge controller. So far, so good. But I am now stuck on how to turn the number returned into one of four strings. My code is below and the sensor name is: “Charging Mode” (the last one).
Regards, M.

esphome:
  name: esp8266-ls1024b

esp8266:
  board: d1_mini

# Enable logging
logger:
  baud_rate: 0
  level: VERBOSE

# Enable Home Assistant API
api:
  encryption:
    key: "maUD8dAVUBRFf21ckASgX/vLvCXrAsUMp5zjJtppDOU="

ota:
  password: "46cc6ff0a9b6de5a5e26b0194706b517"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Esp8266-Ls1024B Fallback Hotspot"
    password: "5yEcepNhkjl7"

captive_portal:

uart:
  - id: uart1
    tx_pin: GPIO1 #Marked as TX on LOLIN D1 Mini
    rx_pin: GPIO3 #Marked as RX on   "    "  "
    baud_rate: 115200
    stop_bits: 1
    parity: none
   
modbus:
  uart_id: uart1
  id: modbus1
  flow_control_pin: GPIO5 #Marked as SCL on LOLIN D1 Mini
  send_wait_time: 200ms
  
modbus_controller:
  - id: ls1024b
    address: 0x01
    modbus_id: modbus1
    command_throttle: 200ms
    update_interval: 10s
    
sensor:
  - platform: modbus_controller
    modbus_controller_id: ls1024b
    id: ls1024b_batt_volts
    name: "LS1024B Batt Volts"
    icon: "mdi:emoticon-outline"
    address: 0x3104
    register_type: read
    value_type: U_WORD
    unit_of_measurement: "V"
    accuracy_decimals: 2
    filters:
    -  multiply: 0.010
    
  - platform: modbus_controller
    modbus_controller_id: ls1024b
    id: charging_current
    name: "LS1024B Batt Current"
    address: 0x3105
    unit_of_measurement: "A"
    register_type: read
    value_type: U_WORD
    accuracy_decimals: 2
    filters:
      - multiply: 0.01  
    

  - platform: modbus_controller
    modbus_controller_id: ls1024b
    id: ls1024_pv_volts
    name: "LS1024B PV Volts"
    address: 0x3100
    unit_of_measurement: "V"
    register_type: read
    value_type: U_WORD
    accuracy_decimals: 2
    filters:
      - multiply: 0.01 
      
  - platform: modbus_controller
    modbus_controller_id: ls1024b
    id: ls1024b_pv_current
    name: "LS1024B PV Current"
    address: 0x3101
    unit_of_measurement: "A"
    register_type: read
    value_type: U_WORD
    accuracy_decimals: 2
    filters:
      - multiply: 0.01
      
  - platform: modbus_controller
    modbus_controller_id: ls1024b
    id: ls1024_batt_soc
    name: "LS1024B Batt SOC"
    address: 0x311A
    unit_of_measurement: "%"
    register_type: read
    value_type: U_WORD
    accuracy_decimals: 0    
      
  - platform: modbus_controller
    modbus_controller_id: ls1024b
    id: charging_mode
    name: "Charging Mode"
    address: 0x3201
    unit_of_measurement: ""
    register_type: read
    value_type: U_WORD
    bitmask: 0x0C      #Mask off all but D3 & D2 using 0000 1100
    accuracy_decimals: 0
    #
    # Bit3Bit2 = 0b00 = 0x00 = "No Charge"
    # Bit3Bit2 = 0b01 = 0x01 = "Float"
    # Bit3Bit2 = 0b01 = 0x02 = "Boost"
    # Bit3Bit2 = 0b11 = 0x03 = "Equalizing"
    
    # I need a Switch-Case construct here!
    
    type or paste code here

This looks like an ideal place to use a lambda - a chunk of code written in C(++) that gets inserted by the YAML-to-C conversion process that occurs during the ‘compile’ phase of ESPHome processing.

While I can’t discern from your comments exactly what you want it to do, it appears that you want to have a set of bitwise comparisons, and return a pointer to one of the strings as a result of the matching comparison.

If you are not comfortable writing in C, perhaps if you add more details about the desired comparisons here, one of us will be able to compose the lambda for you.

https://esphome.io/guides/automations.html#templates-lambdas

Thank you for the offer of help, that would be most welcome.
If I understand what I’ve got so far, by the end if this snippet of code, something/somewhere holds value of the register at 0x3201. By masking that value by 0x0C, I now have a result that is 0, 1, 2 or 3.

- platform: modbus_controller
    modbus_controller_id: ls1024b
    id: charging_mode
    name: "Charging Mode"
    address: 0x3201
    unit_of_measurement: ""
    register_type: read
    value_type: U_WORD
    bitmask: 0x0C      #Mask off all but D3 & D2 using 0000 1100
    accuracy_decimals: 0

Now the bit I can’t get, is to map each of those value to 1 of 4 strings. So that when the sensor returns 0, the Dashboard displays the string “No Charge” and when the sensor returns 1, the dashboard displays “Float” etc.

If I was doing this in the Arduino/ESP32 world, I’d do something like this:

switch (value_in_modbus_map_address_0x3201) {
  case 0:
    Serial.println("No Charge");
    break;
  case 1:
    Serial.println("Float");
    break;
  case 2:
    Serial.println("Boost");
    break;
  case 3:
    Serial.println("Equalizing");
    break;
  default:
    Serial.println("Error");
    break;
}

Looking forward to your suggestions, M.

a case statement would be the most natural way to express this function, but don’t underestimate the power of a simple if/then/else cascade when it would suffice.
Case/switch statements are great when there are many cases, especially if the cases are hard-codable (e.g. ‘5’) but for stuff that has a computed result (e.g. if x&0x3|0x4), an if/else cascade might do for short lists of tests.
I’ll take a look at your testing requirement and see if I can conjure up a simple lambda that could do the trick. (Might take me a while, with distractions :wink: …)

In ESPHome you don’t ‘print’ things, but you return strings to something like a text_sensor which reports them to the device’s webpage or to HA or via the web API.

So the lambda should actually be embedded within something like a text_sensor for that reason.

In a hasty sketch of it, I come up with this:

text_sensor:
  - platform: custom
    value_template: !lambda |-
      #
      # Bit3Bit2 = 0b00 = 0x00 = "No Charge"
      # Bit3Bit2 = 0b01 = 0x01 = "Float"
      # Bit3Bit2 = 0b01 = 0x02 = "Boost"
      # Bit3Bit2 = 0b11 = 0x03 = "Equalizing"

      if( id(Bit3Bit2) == 0x00)
          return "No Change";
      if( id(Bit3Bit2) == 0x01)
          return "Float";
      ...

… and so on with other values for Bit3Bit2, and other logic, perhaps bitwise tests instead of simple equality tests.

The idea here is that the ‘value’ of the text_sensor is provided to it by the lambda which performs a set of tests and returns [a pointer to] the one string that represents the current state.

What you (or HA) do with that text is up to the rest of the YAML code.

Sorry, this is a very rough-edged explanation, but perhaps it gives you a direction in which to proceed, and an example of what that path looks like. More can follow, as we converse further.
(and, BTW, I got the indentation totally wrong - this won’t work if pasted into YAML. My goal here is to show you the general idea and help you to work it in as you see fit.)

Thank you, this is really appreciated. But first the basics!

How to I structure the yaml? In my code (as shown earlier), I think I have a sensor: with multiple inputs: id: charge_current, id: charging mode etc. What would be the relationship between a sensor: and a text_sensor or a binary_sensor etc. Would a text_sensor: be a subsidiary to sensor:? Like so:

sensor:
  - platform: modbus_controler
   id: top_level_sensor
#rest of top_level_sensor

  text_sensor: #indented from sensor:
  - platform: #where are the lists of "platforms"?
   id: second_level_sensor
#end of sensor definitions

… or do all sensors have the same scope? like so:

sensor:
  - platform: modbus_controller
    #etc...
text_sensor: #no indentation
  - platform: modbus_controller
    # etc...
#end of "sensor" definitions...

Lastly, how do I pass variables between a sensor: and a text_sensor:?
If I am collecting the data I want (which I am) from a sensor:, I will need to process it in a text_sensor:?
Sorry if this is so basic. But despite looking (long & hard) I can’t find an expressive explanation of esphome/yamal programming.

Regards, M

A Global variable might be useful if you need to have multiple integrations handling the value.
e.g. the modbus platform fetches it, and the text_sensor platform ‘displays’ text based on its value.

To use Globals, your sensor will probably have to also use a lambda to store the value in the Global so the text_sensor can access it.

https://esphome.io/guides/automations.html#global-variables

Programming something in the ESPhome enviroment can be challenging if you (like me) are used to ‘just writing it in C’ (or any other object/proocedural language). I had to just read, explore, get lost, read more, get lost more, and keep repeating that on the web docs before it started to mesh. And I still get lost whenever trying something new. Eventually, though, it starts to make sense.

Another path that might work is to define a Custom Sensor for this. (I will have to go read to learn what a modbus is/does to offer better suggestions.

Right on the modbus_controller page is an example where a text_sensor is defined and uses the modbus_controller platform.
That led me to think there might be a simpler solution…
(So I may be closing in on understanding your goal a bit better, and it looks like this is the component you want to define.)

Within it is a place where you’d stick the lambda we started sketching out above. So the value (pointer to the text string) returned by the lambda becomes the text_sensor’s ‘displayed’ value.

(the search feature on the ESPHome page seems to be broken at the moment, making it harder to find all the pieces that might relate to lambdas and their use, but the starting point is the ‘Automations’ link on the home page, and then all the bits about lambdas is along the navbar to the left.)

I have taken that example and “pushed” it around to look a bit like the sensor that works and this is as far as I have got:

text_sensor:
  - platform: modbus_controller
    modbus_controller_id: ls1024b
    id: chg_mode_text
    address: 0x3201
    register_type: read
    name: "Charge Mode (as text)"
    bitmask: 0x0C
    #How do I inspect my "working value" at this point? Where is it held?
    raw_encode: HEXBYTES # What does this do? No hit from Google!
    lambda: |-
    
      uint16_t value = modbus_controller::word_from_hex_str(x, 0);
      /*can you  strip the above down for me? My guess it that it reads something like this...
      The variable "value" of data type 16 bit unsigned int is assigned the 
      value of the function "word_from_hex_str(x, 0)"
      But what is the definition of "word_from_hex_str(x, 0)"
      ... and WHAT is "x"
      */
      
      // value = <0 | 1 | 2 | 3 | 999>; #If I do this, I can confirm the switch statement
      switch (value) {
        case 0: return std::string("Not Charging");
        case 1: return std::string("Float Charging");
        case 2: return std::string("Bost Charging");
        case 3: return std::string("Equalizing");
        default: return std::string("Unknown");
      }
      return x;
      //Return to where?    

So.
Question 1. How to I debug this code? My suspicion is that I should somehow pipe to the logger but I can not find any clues as to how this can be done.
Question 2. The Switch-Case is working. But the problem is that the preparation of “value” is wrong. Are there header files somewhere that are defining all the functions and instantiation parameters of the modbus_controller class?
Question 3 How can I just get the result from the bitmask: 0x0C into “value”? Should be simple but its driving me nuts :rage:

In lambda’s, when they are provided-for by the module (as this one is, with the ‘lambda:’ line) ‘x’ is the value that the platform ‘read’ from whatever source it’s set to.
So in this case, ‘x’ is your raw value - the one you want to do the bitwise parsing on in order to select a string to return.
The thing (in this case, a pointer to a string, because it’s within a text_sensor) that you return becomes the present ‘state’ of that object (the text_sensor). It’s what the thing then displays as its state.

First, set ‘raw_encocde’ to ‘NONE’ so ‘x’ will contain a simple integer value on which you can do bitwise operations.
Then you don’t need word_from_hex_str().

Then, to simplify, just try making your switch statement be ‘switch (x)’

or, if you need to mask the value to act on only certain bits, do some bitwise math on it, as in:
‘switch(x & 0x03)’ to mask all but the last two bits from which you will select what you want your case to choose.

The thing is, do the bitwise selection in the switch statement’s argument, in conjunction with the values you choose for the cases, or both, as appropriate. I hope you’re comfortable with bitwise operators.

Thank you. A very helpful primer.

However, after another 12hrs of playing, I think I have a “catch 22” problem!
1.The physical register that I wish to read contains a 16bit big-endian integer.
2. The ESPHome “sensor” will correctly read the value. But a Lambda within this sensor will not allow a return operation containing a string.
3. The ESPHome “text_sensor” will not read the integer from the modbus controller. Returning NaN. Although it will allow a return datatype of String.

How can a lambda behave like a genuine C++ function with defined return types:

String lambdaXtoString (uint16_t x){
//Switch-Case etc…}

Regards, M.

Then it looks like the modbus platform returns a string (pointer to a string) as ‘x’ (I hate it when module definitions aren’t clear about what a parameter like ‘NONE’ does), so you will need the line below after all. You had asked what it does earlier. It accepts a [pointer to a] string (x, supplied by modbus) and returns a (binary) integer into ‘value’ which you can then use in your tests.

and probably also the

raw_encode: HEXBYTES

as you had in the code sample you posted earlier.

Then, do your bitwise tests on ‘value’ instead of ‘x’.
And, if it’s big-endian and you only need to test the rightmost 2 bits, just mask the rest by

switch(value & 0x03)

and test for 1, 2, or 3 in the switch cases.

If, however, the endian-ness is otherwise and you need to test the rightmost 2 bits of the first byte, just insert this before the ‘switch’ statement:

value = value >> 4;

Which (if I can remember my bitwise operators :wink: ) will shift the bits you need into the lower byte, making the switch tests work as intended. And, I believe the statement can be simplified as

value >>= 4;

but don’t hold me to it.

Of course, you could instead just test the string (x) contents, but that would be messier, and can be prone to programmatic/logic errors.

Regarding this: the lambda returning a string has to be in a ‘text_sensor’ component, not a ‘sensor’ component. By virtue, text_sensors want to be supplied with [pointers to] strings, not binary values.

So, stepping back to your YAML, you’d use 'sensor’s for most of the other values you’re reading from modbus, but will need to use a ‘text_sensor’ as the module-type for the one where you want it to display a string.

and if it keeps giving you trouble after that, please re-post your current YAML config, so I can see the whole picture as it stands now.

Thank you for the constant stream of good idea’s that you are prepared to offer…

While I work through your last, can I throw in a couple of curved-ball questions?

  1. Is ESPhome the simplest place to implement my solution? For example, instead of coding in the ESP8266, could it not be achieved in a “Dashboard” somehow?
  2. When I installed Home Assistant on my Pi, it somehow magically found my network printer (HP Office Jet Pro 8600) and on the Overview Dashboard there is an Entities Card which contains an entry that shows printer state in words: “Idle”, “Printing”, “Error” etc. Is it possible to find and inspect this sensor’s yaml for clues?

Regards, M.

I use a map. Read the sensor from MODBUS as a number then

sensor:
  - platform: template
    sensors:
      komfovent_on_off:
        friendly_name: "Komfovent On Off"
        unit_of_measurement: "State"
        value_template: >-
          {% set mapper =  {
              '0' : 'Off',
              '1' : 'On'} %}
          {% set state =  states(sensor.komfovent_on_off_num) %}
          {{ mapper[state] if state in mapper else 'Unknown' }}

I found this somewhere else on the forum, so not claiming ownership of the solution :slight_smile:

There are a number of ways and places where your solution could be implemented.
I still believe that the text_sensor will be the most useful path.
Just gotta puzzle out the parsing of that bit-packed return value is all.
You’re about 1 step from having that working, I believe.

The printer component probably has its state strings embedded in the C code, much as we’ve been doing in your case.

Thank you, I’m ready to try any approach…
What context is this snippet in? Not having a lot of luck understanding how it is used!
Here is (what I hope is the relevant) part of my ESPHome .yaml:

sensor:
   - platform: modbus_controller
     modbus_controller_id: ls1024b
     id: charging_mode
     name: "Charging Mode"
     address: 0x3201
     unit_of_measurement: ""
     register_type: read
     value_type: U_WORD
     bitmask: 0x0C      #Mask off all but D3 & D2 using 0000 1100
     accuracy_decimals: 0
     
    # D3D2 = 0x00 = "No Charge"
    # D3D2 = 0x01 = "Float"
    # D3D2 = 0x02 = "Boost"
    # D3D2 = 0x03 = "Equalizing"
    
   - platform: template
     sensors:   ##################Red X "sensors is invalid option for..."
     id: komfovent_on_off
     name: "Komfovent On Off"
     unit_of_measurement: "State"
     value_template: >-   ###########Red X "value_template is invalid option for...."
       {% set mapper =  {
           '0' : 'Off',
           '1' : 'On'} %}
       {% set state =  states(sensor.komfovent_on_off) %}
       {{ mapper[state] if state in mapper else 'Unknown' }}type or paste code here
#end of file...

…or should your snippet be added to a Dashboard Card?

Regards, M.

The map (part of a template) would replace the lambda. But not by just pasting it over, you’ll have to adapt it to fit into a sensor and use what your sensor returns.
Be advised that if you go the template route (versus lambda) you’ll be in a whole 'nother language.
A template might be the best and easiest path for this in the end.
Getting templates right has, for me, always been a nightmare. I’d be unable to assist much.

Okay, staying with lambda, can you tell me how to examine variables within (and hopefully outside) the lambda construct? This is the minimal snippet that I’m working with. Line 75 is not happy - I guess that “word_from_hex_str()” is not returning the correct datatype?
image
How do I find what other functions “modbus_controller” has? Then once I have an integer in “local_var”, how do I display? See line 77.

Regards, M.