Hi everyone,
I found two bugs—well, actually, Claude found them in the NibeGW binding. I hope there are experts here who can look into these bugs; I’m a mechanical engineer, not a software engineer.
Bug 1:
There is a critical boolean logic bug in the NibeGW Arduino firmware source code included in the repository contrib folder. This bug causes frequent, random “Alarm 251 (COM error ACC Modbus 40)” on Nibe SMO 40 / F-series controllers (and identical Alpha Innotec units like the NP-CS40).
Component: org.openhab.binding.nibeheatpump / Arduino NibeGW firmware
File: contrib/NibeGW/Arduino/NibeGW/NibeGw.cpp
Function: checkNibeMessage()
Affected versions: All current versions including v5.10
Expected Behavior
A frame with a mismatched checksum should only be accepted if both checksum == 0x5C AND msg_checksum == 0xC5 are true at the same time.
Current Behavior
The checksum escape handling in checkNibeMessage() uses incorrect boolean logic. The Nibe RS-485 protocol substitutes a computed checksum of 0x5C (the frame start byte) with 0xC5. This special case is currently handled with OR-like inequality logic instead of AND logic.
As a result, the gateway silently accepts frames with an incorrect checksum whenever either condition is accidentally true independently. This leads to corrupted data being forwarded to the heat pump. Because invalid frames are not NAK’d correctly, it triggers Alarm 251 on the heat pump controller (about 2 to 3 times a day in my environment).
Affected Code
cpp
// NibeGw.cpp, function checkNibeMessage() — CURRENT (buggy):
if (checksum != msg_checksum)
{
// if checksum is 0x5C (start character),
// heat pump seems to send 0xC5 checksum
if (checksum != 0x5C && msg_checksum != 0xC5)
return -2;
}
The current condition accepts a frame (does NOT return -2) when:
checksum == 0x5C→ regardless of whatmsg_checksumis, ORmsg_checksum == 0xC5→ regardless of whatchecksumis
Both conditions must be true simultaneously for the escape logic to be valid.
Proposed Solution
cpp
// FIXED CODE:
if (checksum != msg_checksum)
{
if (!(checksum == 0x5C && msg_checksum == 0xC5))
return -2;
}
Truth Table
| checksum | msg_checksum | Current behaviour | Correct behaviour |
|---|---|---|---|
0x5C |
0xC5 |
Accepted | Accepted |
0x5C |
0xAB |
Wrongly Accepted | Rejected |
0xAB |
0xC5 |
Wrongly Accepted | Rejected |
0xAB |
0xCD |
Rejected | Rejected |
My Environment
- Hardware: Arduino Mega 2560 with WaveShare TTL to RS485 module
- Heat Pump: Alpha Innotec NP-CS40 controller / NP-AW 20-08 (identical to Nibe SMO40 / F2120-8)
Bug 2: State Machine Deadlock due to Missing Inter-Frame Timeout
In addition to the checksum bug, I found a second severe issue in the Arduino firmware that causes the gateway to lock up permanently until a manual reset, also triggering Alarm 251.
Component: org.openhab.binding.nibeheatpump / Arduino NibeGW firmware
File: contrib/NibeGW/Arduino/NibeGW/NibeGw.cpp
Function: loop(), state STATE_WAIT_DATA
Affected versions: All current versions including v5.10
Expected Behavior
If a partial frame is received (e.g., due to RS-485 line noise, electrical interference, or a brief physical disconnection), the state machine should automatically reset to STATE_WAIT_START after a safe timeout period so that normal communication can resume.
Current Behavior
The RS-485 receive state machine has no timeout mechanism while in STATE_WAIT_DATA. If a frame is interrupted and incomplete, the state machine stalls indefinitely waiting for bytes that will never arrive.
While stalled in this state, the gateway stops responding to the heat pump’s token requests. After approximately 15 seconds without a valid ACK, the Nibe SMO 40 / F-series controller raises Alarm 251 (COM error ACC Modbus 40), locking up the heating system until it is manually power-cycled.
Root Cause
cpp
// NibeGw.cpp — CURRENT (no timeout):
case STATE_WAIT_DATA:
if (RS485->available() > 0)
{
byte b = RS485->read();
// ... processes byte, but if no more bytes arrive:
// state stays STATE_WAIT_DATA forever
}
break;
If RS485->available() drops to 0 after an incomplete frame, the state machine does nothing on subsequent loops and stays stuck in STATE_WAIT_DATA forever.
Proposed Solution
Adding a millis()-based timeout resolves this. At 9600 baud, a complete frame (max ~80 bytes) takes less than 85 ms. A 100 ms timeout is perfectly safe—long enough for legitimate intra-frame gaps, but short enough to recover thousands of milliseconds before Alarm 251 would trigger.
NibeGw.h — Add constant and private member:
cpp
#define FRAME_TIMEOUT_MS 100 // safe inter-frame timeout at 9600 baud
// In private section of class NibeGw:
unsigned long lastByteTimestamp;
NibeGw.cpp — Initialize in constructor:
cpp
lastByteTimestamp = 0;
NibeGw.cpp — STATE_WAIT_START, start timeout on frame detection:
cpp
if (b == 0x5C) {
buffer = b;
index = 1;
state = STATE_WAIT_DATA;
lastByteTimestamp = millis(); // start timeout
NibeGw.cpp — STATE_WAIT_DATA, add check at the top of the case:
cpp
case STATE_WAIT_DATA:
if (millis() - lastByteTimestamp > FRAME_TIMEOUT_MS)
{
state = STATE_WAIT_START; // recover from incomplete frame
break;
}
if (RS485->available() > 0)
{
byte b = RS485->read();
lastByteTimestamp = millis(); // refresh timeout on each byte
// ... rest of the logic remains unchanged
I wanted to open a GitHub issue, but my GitHub account is currently caught in a shadowban/404 loop by their automated spam filters. I hope a maintainer can pick this up here and apply the fix to the repository.
I would also like to take this opportunity to thank all the developers and maintainers of this binding and the NibeGW firmware.
Thank you so much for your hard work, dedication, and for maintaining this project for the community! It is an amazing piece of software that makes integrating our heat pumps so much easier. I really appreciate your time and effort in looking into these fixes to make it even more stable for everyone.
Keep up the great work! Thank you!