Write a Modbus R32 (word-swapped float32) register

Hello,

I am trying to write to a R32 Modbus value (Kostal Plenticore solar inverter). I’ve succeeded using the modbus.write_register service, e.g. with value: [0x0000, 0x42a0] I can write the value 80.0:

service: modbus.write_register
data:
  hub: plenticore10_mb
  slave: 71
  address: 1044
  value:
    - 0
    - 17056

Now I am struggling to come up with a value_template that can actually take a sensor/input reading and convert that into the proper hex form. Any ideas?

Thanks in advance!

2 Likes

What I’ve found so far:

{{(90.0).hex()}}

prints out a “human readable” hex. Not quite what I need.

In Python, you’d use the struct module, but not sure how it’s accessible from jinja/templates.

Looks like unpack/pack are available, so something like this should work:

{{ (80.0) | pack("<f") | unpack("<hh") }}

However, it yields 0. In Python, the following works

struct.unpack('<hh', struct.pack('<f', 80.0))

It yields (0, 17056) (which I could pass to a value template).

So what’s the difference between the template and the Python code?

Continuing my monologue: Looks like the unpack filter/function only returns the first value in a list. hence the above example returns 0. If I swap the endianness, I get the expected lower bytes:

{{ '%x' % (80.0) | pack(">f") | unpack(">hh") }}

I checked in the code to confirm it: core/template.py at 90e5d691848dc2eb327023504bebc8fc86fd44b9 · home-assistant/core · GitHub

Maybe there is a reason for it, but I will file a bug.

For my problem, looks like this is a value_template that could work (need to test):

{{ '%x' % (80.0) | pack(">f") | unpack(">I", offset=0) }}

So, finally a solution (maybe someone else will find it helpful):

service: modbus.write_register
data:
  address: 1044
  slave: 71
  hub: plenticore10_mb
  value: >
    [ {{ '0x%x' % unpack(pack(states('input_number.byd_max_soc') |float(0),
    ">f"), ">h", offset=2) | abs }}, {{ '0x%04x' %
    unpack(pack(states('input_number.byd_max_soc')|float(0), ">f"), ">h")|abs }}
    ] 
9 Likes

Thx a lot! Worked for me!

Thank you!
With switching place of the first and second value i can use it to send values to OpenPlc registers.

Thanks for the Code.

I’ve done a small change ‘>h’ => ‘>H’.
It supports now negative values.
The unpack needs to read the packed bytes as unsigned for a native byte stream.

    [ {{ '0x%x' % unpack(pack(states('input_number.byd_max_soc') |float(0),
    ">f"), ">H", offset=2) | abs }}, {{ '0x%04x' %
    unpack(pack(states('input_number.byd_max_soc')|float(0), ">f"), ">H")|abs }}
    ] 
2 Likes

I’m not quite sure if the ‘|abs’ is useful here. I removed it then I was really able to write negative values!

Thank you!! It works! This is my first post on the HA forum and it’s for you.

Jonas, thank you very much for this excellent solution! I made myself simple automation to control my wallbox (I have Compleo/Innogy eBox Professional) to use for charging only excess power from PV panels. I was able to set it only to few predefined levels as I was not able to compute this values for two words representation of float32 numbers. After few months of searching and trying I was today very close to open feature request in home assistant modbus integration to enable us writing float32 automatically, not like an array of 16bit numbers - then I found this treasure. THANK YOU again very much!

Yeah, thank you very much for your solution. That was the hint I was looking for.

I target a different register, as I am using an AC based PlenticoreBi.
For reference:

service: modbus.write_register
data:
  hub: PlenticoreBi
  address: 1030
  slave: 71
  value: >
    [ {{ '0x%x' % unpack(pack(states('input_number.battery_ladung') |float(0),
    ">f"), ">H", offset=2)  }}, {{ '0x%04x' %
    unpack(pack(states('input_number.battery_ladung')|float(0), ">f"), ">H") }}
    ]

Remember, negative percentage for loading the battery. And be careful, never deplete it to 0%, most batteries will suffer orbreak below 5%.

Thank you very much for this template, I have searched for a long time and here I found the solution.
I am communicating with an ESP8266 with a custom modbus rtu and I had to reverse the order of the two 16-bit registers so that it would read the float correctly.

  mode: single
- id: '1720551225953'
  alias: Actualitzar temp min
  description: ''
  trigger:
  - platform: state
    entity_id:
    - input_number.ajudanttempmin
    to: '*'
    from: '*'
    for:
      hours: 0
      minutes: 0
      seconds: 2
  condition: []
  action:
  - service: modbus.write_register
    data:
      hub: modbus_hub_rtu1
      address: 7
      slave: 1
      value: '[ {{ ''0x%04x'' % unpack(pack(states(''input_number.ajudanttempmin'')|float(0),
        ">f"), ">H")|abs }}, {{ ''0x%x'' % unpack(pack( states(''input_number.ajudanttempmin'')
        |float(0), ">f"), ">H", offset=2) | abs }} ]'
  mode: single
1 Like

i did it that way , in decimal value instead of hex

    {% set max_soc = states('input_number.testgetal') | float(0) %}
    {% set packed_value = pack(max_soc, ">f") %}
    {% set high_word = unpack(packed_value, ">H", offset=2) | abs %}
    {% set low_word = unpack(packed_value, ">H") | abs %}

    [ {{ low_word }}, {{ high_word }} ]