EFOY Fuel Cell modbus configuration example

I just wanted to put this modbus yaml config somewhere, in hope it will save some time and frustration.
This is tested on a EFOY 2800 methanol fuel cell, but I guess it will work with any of the 900/1800/2800 series cells.
The config is based on sensors I found interesting in these manuals: EFOY MODBUS TCP - Pro 900/1800/2800 : Support Centre but it’s only a fraction of the information you can get from the system.

- name: efoy
  type: tcp
  host: aaa.bbb.ccc.ddd  #Replace with the IP address of your cell
  port: 502
  delay: 5
  message_wait_milliseconds: 100
  timeout: 5
  binary_sensors:
    - name: "CurrentErrorActive"
      unique_id: "efoy.CurrentErrorActive"
      address: 2
      scan_interval: 10
      input_type: discrete_input
      device_class: problem
    - name: "CurrentWarningActive"
      unique_id: "efoy.CurrentWarningActive"
      address: 3
      scan_interval: 10
      input_type: discrete_input
      device_class: problem
    - name: "FuelBelow25"
      unique_id: "efoy.fuelBelow25"
      address: 12
      scan_interval: 30
      input_type: discrete_input
      device_class: battery
    - name: "FMport1active"
      unique_id: "efoy.FMport1active"
      address: 28
      scan_interval: 30
      input_type: discrete_input
    - name: "FMport2active"
      unique_id: "efoy.FMport2active"
      address: 29
      scan_interval: 30
      input_type: discrete_input
  sensors:
# Take note of this really important piece of information from the EFOY manuals:
# "The byte ordering for a 16-bit word is Big-endian.
#  The word order is Little-endian for all 32bit/64bit values."
# That is the reason all 'float32' sensors have the line 'swap: byte' in them.
# It took me a while to figure that out :)
    - name: "LogPOut"
      unique_id: "efoy.LogPOut"
      data_type: float32
      swap: word
      device_class: power
      scan_interval: 10
      address: 20
      input_type: input
      unit_of_measurement: "W"
    - name: "LogUBat"
      unique_id: "efoy.LogUBat"
      data_type: float32
      swap: word
      device_class: voltage
      scan_interval: 10
      address: 22
      input_type: input
      unit_of_measurement: "V"
    - name: "LogTAmb"
      unique_id: "efoy.LogTAmb"
      data_type: float32
      swap: word
      device_class: temperature
      scan_interval: 10
      address: 24
      input_type: input
      unit_of_measurement: "°C"
    - name: "CurrentErrorCode"
      unique_id: "efoy.CurrentErrorCode"
      data_type: int16
      scan_interval: 10
      address: 31
      input_type: input
    - name: "CurrentErrorCodeMinor"
      unique_id: "efoy.CurrentErrorCodeMinor"
      data_type: int16
      scan_interval: 10
      address: 32
      input_type: input
    - name: "LastErrorCode"
      unique_id: "efoy.LastErrorCode"
      data_type: int16
      scan_interval: 10
      address: 33
      input_type: input
    - name: "LastErrorCodeMinor"
      unique_id: "efoy.LastErrorCodeMinor"
      data_type: int16
      scan_interval: 10
      address: 34
      input_type: input
    - name: "CurrentWarningCode"
      unique_id: "efoy.CurrentWarningCode"
      data_type: int16
      scan_interval: 10
      address: 35
      input_type: input
    - name: "CurrentWarningCodeMinor"
      unique_id: "efoy.CurrentWarningCodeMinor"
      data_type: int16
      scan_interval: 10
      address: 36
      input_type: input
    - name: "SystemStateEnum"
      unique_id: "efoy.SystemStateEnum"
      device_class: enum
      data_type: int16
      scan_interval: 10
      address: 39
      input_type: input
    - name: "OperatingModeEnum"
      unique_id: "efoy.OperatingModeEnum"
      device_class: enum
      data_type: int16
      scan_interval: 10
      address: 40
      input_type: input
    - name: "LogStackOpTime"
      unique_id: "efoy.LogStackOpTime"
      data_type: float32
      swap: word
      device_class: duration
      scan_interval: 600
      address: 270
      input_type: input
    - name: "LogpAmb"
      unique_id: "efoy.LogpAmb"
      data_type: float32
      swap: word
      device_class: atmospheric_pressure
      scan_interval: 10
      address: 295
      input_type: input
      unit_of_measurement: "hPa"
    - name: "LogRH"
      unique_id: "efoy.LogRH"
      data_type: float32
      swap: word
      device_class: humidity
      scan_interval: 10
      address: 297
      input_type: input
      unit_of_measurement: "%"
    - name: "LogTStack"
      unique_id: "efoy.LogTStack"
      data_type: float32
      swap: word
      device_class: temperature
      scan_interval: 10
      address: 287
      input_type: input
      unit_of_measurement: "°C"
    - name: "LogTHE"
      unique_id: "efoy.LogTHE"
      data_type: float32
      swap: word
      device_class: temperature
      scan_interval: 10
      address: 289
      input_type: input
      unit_of_measurement: "°C"
    - name: "LogTMeOH"
      unique_id: "efoy.LogTMeOH"
      data_type: float32
      swap: word
      device_class: temperature
      scan_interval: 10
      address: 291
      input_type: input
      unit_of_measurement: "°C"
    - name: "CartCapStatus"
      unique_id: "efoy.CartCapStatus"
      data_type: float32
      swap: word
      device_class: volume
      scan_interval: 10
      address: 202
      input_type: input
      unit_of_measurement: "L"
    - name: "CartRlVolStatus"
      unique_id: "efoy.CartRlVolStatus"
      data_type: float32
      swap: word
      device_class: battery
      min_value: 0
      max_value: 100
      scan_interval: 10
      address: 204
      input_type: input
      unit_of_measurement: "%"
    - name: "CartAbVolStatus"
      unique_id: "efoy.CartAbVolStatus"
      data_type: float32
      swap: word
      device_class: volume_storage
      scan_interval: 10
      address: 206
      input_type: input
      unit_of_measurement: "L"
  switches:
    - name: "SystemOn"
      unique_id: "efoy.SystemOn"
      address: 0
      write_type: coil
      verify:
    - name: "SystemOff"
      unique_id: "efoy.SystemOff"
      address: 1
      write_type: coil
      verify:
    - name: "SystemAuto"
      unique_id: "efoy.SystemAuto"
      address: 2
      write_type: coil
      verify:

Optionally, you can also add these template sensors to your Configuration.yaml, to get human readable output for the Operational mode and System state

template:
  - sensor:
    - name: "SystemState"
      unique_id: "efoy.SystemState"
      device_class: enum
      state: >
          {% set mapper =  {
              '0' : 'Off',
              '1' : 'Standby',
              '2' : 'Operational',
              '3' : 'Shut down',
              '4' : 'Frost protection',
              '5' : 'Deep discharge protection',
              '6' : 'Transport locking',
              '7' : 'Transport locked',
              '8' : 'Reset',
              '9' : 'Factory default',
              '10' : 'Error',
              '11' : 'Frost protection',
              '12' : 'pending',
              '13' : 'pending',
              '14' : 'Update accessories'} %}
          {% set state =  states('sensor.SystemStateEnum') %}
          {{ mapper[state] if state in mapper else 'Unknown' }}
    - name: "OperatingMode"
      unique_id: "efoy.OperatingMode"
      device_class: enum
      state: >
          {% set mapper =  {
              '0' : 'Auto',
              '1' : 'Off'} %}
          {% set state =  states('sensor.OperatingModeEnum') %}
          {{ mapper[state] if state in mapper else 'Unknown' }}

This looks interesting but i couldn’t get it to work with my eFoy 900. I have the eFoy connected to my home assistant via a cellular modem and vpn. I could ping the device no problem but i couldn’t get it to establish a connection via homeassistant. It was my first try using modbus so i may be overlooking something critical. If you have any thoughts i’d be happy to hear them.

Sorry, haven’t tried anything but a straight wired (ethernet) connection as of now.
Do you have the possibility to make a test by connecting the devices using direct cable?
I tried this on a 900 unit last week, and it worked right away.
My guess is that your issues may have something to do with port 502 not being forwarded properly in your link between the eFoy and your HA setup. But that’s just a guess…

Unfortunately no I cannot do a straight connection. Thanks for the suggestion regarding port forwarding but because it is on the VPN I didn’t think i would have too. I did test it as a possible issue by directing it to the WAN address instead of the local address and opening up the port but i had the same results.

Ok, then my next suggestion is a bit more technical: You start a network trace (to a file) on your HA using the ‘tcpdump’ command. Here is a link for how to do it; read the last post: Tcpdump on Raspberry Pi 5 HAOS - #2 by dan-shaqfu
Then you download the trace-file to your computer, and examine it using WireShark. Here is a starting point: Wireshark · Display Filter Reference: Modbus/TCP
Hopefully, the trace can give you some clues to what goes wrong.
Good luck!

Thanks, I did the tcpdump and dropped into Wireshark but it’s all just gibberish to me, there appeared to be communication attempts. Rather than read up on it I’ll try to find some time to get the eFoy hardwired into a RPi for testing. If that works it’s probably just a port forward or VPN issue I need to work on. Thanks for your help. I’ll post a follow up when I’ve had some time to tinker.

Ok. That sounds like a logical path forward.
FYI, the steps I took to make this work, were

  1. Clean install of Hassio on a RPi4
  2. Adding the Modbus integration to the configuration.yaml as described in the documentation here: Modbus - Home Assistant
  3. Spending quite some hours figuring out how to translate the modbus coils and inputs into HA sensors and switches, most of it spent on countless trial-n-error until I found the swap directive. :upside_down_face:

That’s pretty much it. I had never touched on modbus previously, but stubbornness made it work eventually. And of course all the hard work done by the devs and members of the community!
You’ll get there too, no worries :slight_smile:

I had also had trouble connecting remotely,
we found out the port 502 will only answer locally in the LAN network,
behind a gateway over VPN / Router the device will not answer the modbus request. Efoy did confirm this to us, and this is how it has been designed.

Thanks so much for posting @CrazyBlue. I wasted a couple hours yesterday with ChatGPT trying different things to get it to work.

Knowing it has to be on the LAN I noticed that the Microhard cellular modem has modbus and modbus to MQTT features. Maybe the Mircohard could relay the info from the eFoy to my home assistant. Time to do some research!

Nice I could help, I wasted much time too, with all kinds of router experiments :sweat_smile:

I’m currently writing a modbus client in C that can run on a Teltonika 4G Mobile router, so i can do a SSH command over VPN remotely and run my console app command “locally”, this will poll some registers over modbus TCP in LAN.

I’m sorry, but this makes no sense to me.
How can the eFoy even tell if a TCP packet originated from behind a VPN, unless the VPN trashes those packets somehow? To me this sounds like VPN issue.
A proper VPN setup should be transparent, and at most introduce latency. I do know that certain VPN protocols are more intrusive compared to others; they tend to cap the max transfer sizes, or don’t reassemble packet sizes and order of arrival properly. But that is still a VPN issue, and should be fixed at the origin of the problem.

I totally agree, if you’ve established the VPN it shouldn’t even know you’re not on the same network. I’m away from the office so I can’t physically test this until next week.
I’m purchasing another 900 this month and I’ll be reaching out to the sales team for confirmation and request a fix on their end.

There is another way, or maby “workaround”, and that would be to place a(nother) RPi with HA locally at the eFoy site, and access that instead over the VPN.

Blocking requests from outside the local LAN is also done by more manufacturers.
I have seen this before because Modbus has no authorization, so they actively block these Modbus requests in the device for security reasons.

The device will not respond to a request if the source IP of the TCP packet is not from inside the LAN segment.

When I scan ports from my laptop using VPN,
I see port 502 is open on the EFOY device.
This means the gateway is correctly set up, and the device should be able to communicate to my laptop.

I have done a Wireshark sniff, and you can see the [SYN] will be [SYN, ACK]'ed, and the Modbus request is sent to the device.
But after this, the device sends a [RST, ACK], terminating the request without answering it.

Also, EFOY has documented this “functionality” in the manual for the system integration Modbus TCP documentation.

That “functionality” on the eFoy side, is indeed unfortunate. Or more correctly, its unfortunate that it isn’t configurable. If they made it possible to switch modbus/TCP on and off, why not add an option for the “local only”?
Still there is a way to circumvent this, and that would be to use a VPN that can handle L2 bridging. Here is an example: Tutorial (Legacy): Site-to-Site Layer 2 Bridging Using Access Server and a Linux Gateway Client
Not all VPN types support this, so it may not be applicable in your case. (In my opinion, this is what makes a VPN truely “private”, otherwise it’s just routing; and that’s what the wole internet does anyway :slight_smile: )