Skip to content

Modbus TCP support - #996

Open
ReinhardGruber wants to merge 29 commits into
heishamon:mainfrom
ReinhardGruber:master
Open

ReinhardGruber wants to merge 29 commits into
heishamon:mainfrom
ReinhardGruber:master

Conversation

@ReinhardGruber

Copy link
Copy Markdown

This pull request adds Modbus TCP support to HeishaMon.

The implementation allows external systems such as PLCs, Loxone, Home Assistant or other Modbus TCP clients to read HeishaMon values directly via standard Modbus registers.

Main features

  • Modbus TCP server integrated into HeishaMon
  • Access to relevant Panasonic heat pump values via Modbus registers
  • Register mapping available through the HeishaMon web interface
  • ESP32 support
  • Existing HeishaMon functionality remains unchanged

I originally developed this as a separate fork and would now like to contribute the functionality back to the main HeishaMon project.

The implementation has already been tested in real operation with a Panasonic heat pump and external Modbus TCP clients.

If a direct integration into the main project is currently not desired, I would also be very happy with a reference to the fork in the README, so users who need Modbus TCP support can find it easily.

There is no urgency from my side, so I am also happy to wait if you prefer to review or consider this at a later point.

Feedback and suggestions for changes to better integrate it into the main project are very welcome.

@IgorYbema IgorYbema left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for contributing this back! Modbus TCP support is welcome in principle, and the register map design (fixed expandable blocks, version register, float word pairs, host-side tests) is well thought out. That said, there are a few things that need to change before we can merge it into the main project.

CI build fails

The build stops at "Install dependencies":

Error installing eModbus: Library 'eModbus@latest' not found

eModbus is not in the Arduino library registry (it's only in the PlatformIO registry), so arduino-cli lib install eModbus can't find it. Also, "AsyncTCP" in the Arduino registry resolves to the old dvarrel 1.1.4 fork; the maintained ESP32Async version is registered as "Async TCP". Something like this should work:

      - name: Install dependencies
        env:
          ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL: "true"
        run: |
          arduino-cli lib install ringbuffer pubsubclient arduinojson dallastemperature onewire "Adafruit NeoPixel" "Async TCP"
          arduino-cli lib install --git-url https://github.com/eModbus/eModbus.git#v1.7.5stable

The .devcontainer, LIBSUSED.md and AGENTS.md build instructions need the same two libraries. Please make sure both the ESP8266 and ESP32 builds pass with arduino-cli (the scripts/build_*.sh commands), not only PlatformIO.

Blockers

1. Modbus callbacks run in the AsyncTCP task, not in loop(). From there:

  • FC_06 calls send_heatpump_command(), which calls log_message(), which calls mqtt_client.publish() and websocket_write_all(). Neither PubSubClient nor the webserver is thread-safe, so this races with the main loop and can cause random crashes or a corrupted MQTT connection.
  • The optional-PCB setters change the shared optionalPCBQuery[] without any locking, while the serial task and the MQTT and web command paths also use it.
  • FC_03 decodes actData/actDataExtra while readSerial() may be overwriting them, so a read can return a mix of old and new bytes.
  • logNonNumericTopicValue() also calls log_message() from the async task.

Please hand writes over to the main loop (for example through a queue that loop() drains and then calls send_heatpump_command from there) and log via logQueue. For reads, use a consistent snapshot, for example protected by a mutex.

2. Modbus is always on, with no authentication. Port 502 opens on every ESP32 boot, and anyone on the LAN can write any command, including SetReset (22000) and both relays. The README in this PR says "keep write access disabled until your logic is proven", but no such option exists. Please add Settings options: Enable Modbus TCP (default off) and Allow Modbus writes (default off).

3. Fork-specific changes need to be removed. This PR needs to contain only the feature:

  • version.h hardcoded to 4.2.2-ModbusTCP
  • Page title and topbar text ("HeishaMon ModBus TCP") and the accent color change (#3a7bd5 → #368DF4)
  • The "Modbus-Enabled Fork" section at the top of README.md (a short Modbus section further down, linking to Modbus-Register-Mapping.md, is fine)
  • In main.yml: the -ModbusTCP version suffixes and the renamed binaries (HeishaMon_ESP32-ModbusTCP-…). The renamed binaries would also break the MD5 lookup in the firmware upload page, which splits the filename on -.
  • The committed binaries under binaries/model-type-large/ (v4.04ALPHA and v4.2.2-ModbusTCP)
  • platformio.ini, scripts/platformio_export.py and scripts/export_firmware.py, plus the change to build_*.sh that copies every local build into binaries/ (this dirties the working tree for every developer)

Should fix

  • commands.h: please don't add an id field to cmdStruct/commands[]. Keep the Modbus IDs in ModbusRegisterMap.h as a name→ID table, the same way you already do for OPTIONAL_COMMANDS, so the core command table and anything that parses it (like the rules harness regex you had to change) stay untouched. A static_assert or a host test for duplicate IDs would also help.
  • Writes are int16 ×1: optional-PCB temperatures such as SetPoolTemp and SetZ1RoomTemp can't receive values like 21.5. Reads use ×100 for temperatures, so writes should probably be symmetric for temperature commands.
  • Optional-PCB writes when the optional PCB is disabled: FC_06 reports success, but send_heatpump_command silently ignores the command. It should return an exception instead (e.g. ILLEGAL_DATA_ADDRESS or SERVER_DEVICE_FAILURE).
  • Extra-block registers: when extraDataBlockAvailable is false, these return values decoded from an empty buffer. It's better to return an exception.
  • Sidebar navigation: moving it from per-page JS into webBodyStart is a nice cleanup, but it's unrelated to Modbus and changes pages that previously had no nav. Please submit it as a separate PR.
  • Rules tests step in CI: reasonable, but also separate from this feature.

Minor

  • File name casing: HeishaModbusServer.h vs HeishaModBusServer.cpp. Please pick one.
  • The comment in the header is in German; please translate it to English.
  • Stray blank lines added in HeishaMon.ino (e.g. in the OpenTherm setup block).

To sum up: a PR with just the Modbus server, the minimal hooks in HeishaMon.ino/webfunctions/gpio/s0, opt-in settings, and main-loop command handling would be much easier to review and merge. Thanks again for the work on this!

Reinhard Gruber and others added 2 commits September 30, 2026 10:10
Modbus TCP server (port 502) on the large board: read heat pump, optional
PCB and S0 values (FC03), send commands (FC06), switch relays (FC05).

- Off by default: "Enable Modbus TCP server" and "Allow Modbus writes"
  in a new Modbus TCP settings panel
- AsyncTCP callbacks only read a data snapshot and queue writes; loop()
  executes them
- Modbus command IDs in ModbusRegisterMap.h, commands.h unchanged
- Optional PCB temperatures are written x100; unavailable extra/optional
  registers return ILLEGAL_DATA_ADDRESS
- Register page at /modbus, docs, Loxone template, host tests
- CI/devcontainer: install Async TCP (ESP32Async) and eModbus from git
- Keep platformio.ini for pioarduino builds

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Add FC16 float32 writes for all commands (unscaled real value)
- Add FC01 read coils and relay state registers for relay readback
- Temperature commands are x100 in int16 registers, whole degrees
  only for heat pump commands
- Sort address space by type: int16 measurements, int16 commands,
  float32 measurements, float32 commands, coils, device information
- Register page: single list sorted by address, no command
  addresses in the description
- Bump register map to v3, update docs, Loxone template and tests
@IgorYbema

Copy link
Copy Markdown
Member

Thanks for the rework, this addresses almost everything from the previous review. The snapshot/write-queue approach and the opt-in settings look good.

A few remaining points:

  • CI still fails, now in the new Test Modbus register map step. The C++ tests pass, but the Loxone check at the end of tests/modbus/run_tests.py still expects the old addresses (ErrorInformation 44, SetHeatpump 5000, ...), while the XML and register map now use the sorted layout (10088, 20000, ...). Please update the expected dict. Because this step runs before compiling, the ESP8266/ESP32 builds have not been verified yet.
  • platformio.ini is still part of the PR, please remove it.
  • The Windows toolchain handling in run_tests.py (MSVC vcvars lookup, emscripten from the .NET workload) isn't used by CI; please keep the script to the plain g++ path.
  • Minor: stray double blank lines in gpio.cpp.

  function code and name the entries that differ; keep only the
  plain g++ path (drop MSVC/emscripten handling not used by CI).
- gpio.cpp: remove stray double blank line.
@ReinhardGruber

Copy link
Copy Markdown
Author

Thanks for the review! I've pushed an update:

  • run_tests.py: Now only uses g++. The MSVC and emscripten handling is gone.
  • gpio.cpp: Double blank line removed.
  • platformio.ini: I'd like to keep this one, if that's OK with you. It doesn't affect the arduino-cli build or CI, but it makes local development and flashing a lot faster for me. If you'd rather not have it in the repo root, I'm happy to move it (e.g. to scripts/ or .devcontainer/) or add a short note that it's unofficial and not maintained by CI.

Once this PR is merged, I have a few follow-ups planned as separate PRs:

  • Setting commands has to be explicitly allowed in the config.
  • Wi-Fi: WPA Enterprise support.
  • Hostname shown in the web UI header. With several heat pumps under remote maintenance, telling them apart by IP alone is error-prone.
  • Setting values directly in the web UI. Going through the URL is often too cumbersome.
  • Only send a value to the heat pump if it differs from the current one, whether it arrives via Modbus, MQTT, URL or elsewhere. This reduces EEPROM write cycles on the heat pump.

Let me know if you'd like any of these discussed in an issue first.

Reinhard Gruber and others added 2 commits September 30, 2026 22:51
- Add SetSterilizationTemp (5047, x100) and SetSterilizationMaxTime
  (5048) from upstream to the Modbus command map and docs.
- Loxone template: int16 commands use FC06 at 5000+, Z1 setpoints
  FC16 float32 at 20008/20010, ErrorInformation back on int16
  register 44 (the float register reads 0 for error text).
- run_tests.py: expect ErrorInformation at 44.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IgorYbema

Copy link
Copy Markdown
Member

I am wondering if I would allow the modbus functionality for the ESP8266 variant. Did you test it on that also? It does have much memory available and the speed isn't that great either.

Yes ok, keep the platform.io.

  • Setting commands has to be explicitly allowed in the config. -> Then it must clearly be what the difference is between listen only mode. Think about that please.
  • Wi-Fi: WPA Enterprise support. -> Yes, but probably only for the ESP32?
  • Hostname shown in the web UI header. With several heat pumps under remote maintenance, telling them apart by IP alone is error-prone. -> Yes good thinking
  • Setting values directly in the web UI. Going through the URL is often too cumbersome. -> Original design was to not do anything on the device itself but let's do this indeed.
  • Only send a value to the heat pump if it differs from the current one, whether it arrives via Modbus, MQTT, URL or elsewhere. This reduces EEPROM write cycles on the heat pump. -> Ok for this also

Do make seperate PR's for each of this changes.

@ReinhardGruber

ReinhardGruber commented Oct 1, 2026 via email

Copy link
Copy Markdown
Author

@heppth

heppth commented Oct 1, 2026

Copy link
Copy Markdown

I very much hope that the Modbus functionality, in particular, will be added to the project. This would make it easy to integrate HeishaMon into the existing building automation system. Almost all control systems support Modbus, but very few support MQTT. The Modbus functionality would really enhance the project.

@IgorYbema

IgorYbema commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

I agree to enable it for the ESP32 only at first. Can you add the #ifdef lines so the modbus code isn't added to the esp8266 tree? edit: i just noticed that is already the current situation
I'll just add it to the next release if the tests seem fine.

Comment thread HeishaMon/HeishaMon.ino
@ReinhardGruber

ReinhardGruber commented Oct 1, 2026 via email

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants