ModBus how to read multiple registers? How to use data_type and structure?

I am trying to improve my SolaX ModBus implementation by reading multiple registers at once.

According to the manual, if I read the holding registers 0 - 6 I get the 14char serial number. The manual also states the data format is uint16. I tried uint, but it didn’t work.

The following does work and results in the 14digit serial number.

sensor:
- platform: modbus
  scan_interval: 2
  registers:
  # Holding Registers
    - name: SolaX Serial Number
      hub: SolaX
      register: 0
      count: 7
      data_type: string

So I know fundamentally that read multiple registers does work!
A1A111A1111111

But I am trying to read multiple Input registers at once and I can’t figure it out?

I am trying to read register 3 - 7

    - name: SolaX Group Test
      hub: SolaX
      register: 3
      register_type: input
      count: 5

I know I should be using a combination of

- name: SolaX Group Test
  hub: SolaX
  register: 3
  register_type: input
  count: 5
  data_type: custom
  structure: what goes here though?

But I can’t figure out the structure?
Each register value should be a 2byte chunk (16bit / 1 word) so how do I split them into chunks?

If I read each register individualy I get something like the following:
Register 3: 1207
Register 4: 1233
Register 5: 2010
Register 6: 2020
Register 7: 5001

If I read them as a string I get random stuff like

q l((~

The doumentation is quite vague to a non programmer!

If data_type is custom specify here a double quoted Python struct format string to unpack the value. See Python documentation for details. Ex: >i.

Where’s the Python documentation?

I have made some progress…

    - name: SolaX Group Test
      hub: SolaX
      register: 3
      register_type: input
      count: 5
      data_type: custom
      structure: ">HHHHH"

As I have 5 registers being read I found that if I use 5x H under structure: I wouldn’t anymore errors.

I got the “>HHHHH” from following https://docs.python.org/3.2/library/struct.html
I take it that is the correct method of the structure?

I can also see the first register being displayed correct. (Red line)

I am guessing the other 4 registers are hidden and I need to use some form of template sensor to extract the other register values? (Green box)
The values in the green box are formatted, they are all 4 digit numbers.
2673
2532
etc

I have tried a few combinations on splitting them into 4 number segments, but not having any luck yet.

- platform: template
  sensors:
    solax_split_1:
      friendly_name: "SolaX Split Sensor 1"
      value_template: '{{ states.sensor.solax_group_test("    ")[0] }}'
    solax_split_2:
      friendly_name: "SolaX Split Sensor 2"
      value_template: '{{ states.sensor.solax_group_test("    ")[1] }}'

I have also tried:

    solax_split_1:
      friendly_name: "SolaX Split Sensor 1"
      value_template: '{{ states.sensor.solax_group_test[0:3] }}'
    solax_split_2:
      friendly_name: "SolaX Split Sensor 2"
      value_template: '{{ states.sensor.solax_group_test[4:7] }}'

Thinking it might read the values from 0-3 and 4-7

Edit:

I was trying to follow:


So I could split the value of my sensor sensor.solax_group_test
    solax_split_1:
      friendly_name: "SolaX Split Sensor 1"
      value_template: "{{ 'sensor.solax_group_test'[0:4] }}"
    solax_split_2:
      friendly_name: "SolaX Split Sensor 2"
      value_template: "{{ 'sensor.solax_group_test'[4:8] }}"

But in reality I am splitting the name into sections! Not the value of the sensor :man_shrugging:

Any pointers anyone?

Edit 14 Oct 2020:

I have worked out how to template the results correctly:

- platform: template
  sensors:
    solax_split_1:
      friendly_name: "SolaX Split Sensor 1"
      value_template: "{{ states('sensor.solax_group_test')[0:2] }}"
    solax_split_2:
      friendly_name: "SolaX Split Sensor 2"
      value_template: "{{ states('sensor.solax_group_test')[2:5] }}"

First will display char 0 and 1
Second will display char 3 and 4

But I still can’t view any registers other than the first one out of the five that I am reading, when I try to read multiple registers at once by using count:

Just wanted to update on this issue.
I raised an issue on GitHub as I couldn’t return more than a single register value when reading more than one register at a time.

@vzahradnik Has issued a PR which fixes this issue.
When reading multiple registers using data_type: custom and correctly structured structure: ">10h" you now get comma separated values.

Initial test of:

    - name: SolaX Group Test
      hub: SolaX
      register: 0
      register_type: input
      count: 10
      data_type: custom
      structure: ">10h"

Results in a comma separated string of register values from my SolaX Inverter ie:
2423,15,294,1246,1207,0,1,5007,34,2
Which matches reading each register individually.

3 Likes

I’ve tested single value requests - ok. Now trying to read in bulk. Below is my config file. Config pass the validation. As result there no any sign of modbus requests in the log, no errors…

Can you explain where the template should be defined in the configuration?

modbus:
  name: modbus1
  type: serial
  method: rtu
  port: /dev/ttyUSB0
  baudrate: 9600
  stopbits: 1
  parity: N
  bytesize: 8

logger:
  logs:
    homeassistant.components.modbus: debug
    pymodbus.client: debug

sensor:
  - platform: modbus
    entity_namespace: outside
    scan_interval: 20
    registers:
      - name: xy_md02_01
        hub: modbus1
        slave: 8
        register: 1
        count: 2
        register_type: input
        data_type: custom
        structure: ">xhh"
        scale: 0.1
  - platform: template
    sensors:
      outside_temperature:
        friendly_name: "Temperature"
        value_template: "{{ states('sensor.xy_md02_01')[1] }}"
      outside_humidity:
        friendly_name: "Humidity"
        value_template: "{{ states('sensor.xy_md02_01')[2] }}"

Try:

sensor:
- platform: modbus
  scan_interval: 20
  registers:
    - name:xy_md02_01
      hub: modbus1
      slave: 8
      register: 1
      register_type: input
      count: 2
      data_type: custom
      structure: ">2h"

That should result in the two expected values joined together comma seperated.
So say:

register 1 = 0220
register 2 = 0221

You will have sensor.xy_md02_01 = 0220,0221

You can then split that down with a template sensor and do any scaling.

- platform: template
  sensors:
    outside_temperature:
        friendly_name: "Temperature"
        value_template: "{{ states('sensor.xy_md02_01').split(',')[0] }}"
    outside_humidity:
        friendly_name: "Humidity"
        value_template: "{{ states('sensor.xy_md02_01').split(',')[1] }}"

If you want to scale say Humidity change it to "{{ states('sensor.xy_md02_01').split(',')[1]| float / 10 }}"

3 Likes

Thanks, got it working! I was confused by the log output - it was mentioned that the values also include the count of bytes received. In fact only value are available in the response message. Looks like the log output needs a fix.

In the sensor.py code I found that scale will not work without precision > 0. Nice feature to post-process values with Python. I’ve end up this this config:

modbus:
  name: modbus1
  type: serial
  method: rtu
  port: /dev/ttyUSB0
  baudrate: 9600
  stopbits: 1
  parity: N
  bytesize: 8

logger:
  logs:
    homeassistant.components.modbus: debug
    pymodbus.client: debug

sensor:
  - platform: modbus
    scan_interval: 10
    registers:
      - name: xy_md02_01
        hub: modbus1
        slave: <device id here>
        register: 1
        count: 2
        register_type: input
        data_type: custom
        structure: ">2h"
  - platform: template
    sensors:
      outside_temperature:
        friendly_name: "Outside Temperature"
        value_template: "{{ (states('sensor.xy_md02_01').split(',')[0]|float * 0.1)|round(1)  }}"
      outside_humidity:
        friendly_name: "Outside Humidity"
        value_template: "{{ (states('sensor.xy_md02_01').split(',')[1]|float * 0.1)|round(1)  }}"

To find out port: /dev/ttyUSB0 number, enter lsusb in Terminal.

Hope this example will help someone else too.

1 Like

Thanks for your work :+1:

Now I read 6 registers and got >20 sensors

Just an update for template part. I had situation when there is total crap coming from modbus sensor and decided to add validation and value range filtering in to the template value. Here is how you can do it:

      value_template: >-
        {% set valid = not is_state('sensor.modbus_foo', 'unavailable') %}
        {% set values = states('sensor.modbus_foo').split(',') %}
        {{ (values[0]|round(1)) if valid else 'unavailable' }}

or

      value_template: >-
        {% set value = (states('sensor.modbus_foo2').split(',')[1]|float / 10) %}
        {% set valid = not is_state('sensor.modbus_foo2', 'unavailable') and (value > 0.0 and value < 99.9 ) %}
        {{ value if valid else 'unavailable' }}