Generators, in this post, means the diesel kind that keep factories running when the grid drops. Not the Python kind, although one does turn up in the code below.
We at Enpower supply and commission diesel sets across Pakistan, where load-shedding means a backup set can run for hours a day. More of our customers now have someone in-house who writes code, and they ask the same thing: how do we get the data out of our generators and into something we can query? This is the hands-on version of our answer. You'll wire an RS-485 link, poll the controller with Python, store readings in SQLite and write a few rules that flag real problems.
What you need on the bench
A generator controller with a Modbus RTU port on RS-485, or a Modbus TCP gateway
A USB to RS-485 adapter, preferably an isolated one
A Raspberry Pi or any small Linux box that can live in the panel room
Python 3 with pymodbus installed
The controller's Modbus register map, from its manufacturer
That last item matters more than the hardware. Controller makers such as Deep Sea Electronics and ComAp publish register maps, and each one is laid out differently.
Read the register map before you write code
The register map tells you which address holds which value, whether it's a holding or an input register, how it's scaled and whether it's signed. Oil pressure might be stored in kPa, coolant temperature in whole degrees, battery voltage in tenths of a volt. Values like run hours or kWh usually span two 16-bit registers, and you need to know which word comes first.
Check whether the map's addresses are zero-based or one-based, too. Some documents list registers in the old 40001-style notation that you have to convert, and an off-by-one error will happily return a plausible but wrong value.
Make a table of the handful of values you actually want before touching Python. Keep it short. Fuel level, battery voltage, coolant temperature, oil pressure, frequency, run hours and the alarm status registers cover most of what a maintenance team needs.
Wiring RS-485 without the usual headaches
Most failed first attempts are wiring, not code. Use twisted pair, connect A to A and B to B, and if nothing answers, swap them, because manufacturers don't agree on labelling. Terminate both ends of a long bus with 120-ohm resistors. Match the controller's baud rate, parity and stop bits exactly, and note its Modbus unit ID.
A generator room is electrically noisy. Keep the RS-485 cable away from power cables, ground the shield at one end only, and use an isolated adapter so a fault on the set doesn't travel back into your Pi.
Polling the controller in Python
Here's a minimal poller. The register addresses are placeholders, so replace them with the ones from your controller's map, along with the correct scale factors and signedness.
import sqlite3
import time
from datetime import datetime, timezone
from pymodbus.client import ModbusSerialClient
from pymodbus.exceptions import ModbusException
Placeholder addresses: copy the real ones, and their scaling,
from your controller's Modbus register map.
REGISTERS = {
"fuel_level_pct": (100, 1.0, False),
"battery_v": (101, 0.1, False),
"coolant_temp_c": (102, 1.0, True),
"oil_pressure_kpa": (103, 1.0, False),
"gen_freq_hz": (104, 0.1, False),
}
def to_signed(raw):
return raw - 0x10000 if raw & 0x8000 else raw
def poll(client, unit_id=1):
"""Yield (name, value) pairs: a Python generator reading a diesel one."""
for name, (addr, scale, signed) in REGISTERS.items():
try:
rr = client.read_holding_registers(addr, count=1, device_id=unit_id)
except ModbusException as exc:
print(f"{name}: {exc}")
continue
if rr.isError():
print(f"{name}: controller returned an error")
continue
raw = rr.registers[0]
yield name, (to_signed(raw) if signed else raw) * scale
def main():
db = sqlite3.connect("genset.db")
db.execute("CREATE TABLE IF NOT EXISTS readings (ts TEXT, name TEXT, value REAL)")
client = ModbusSerialClient(port="/dev/ttyUSB0", baudrate=9600,
parity="N", stopbits=1, bytesize=8, timeout=1)
if not client.connect():
raise SystemExit("Could not open the serial port")
while True:
ts = datetime.now(timezone.utc).isoformat()
rows = [(ts, name, value) for name, value in poll(client)]
db.executemany("INSERT INTO readings VALUES (?, ?, ?)", rows)
db.commit()
time.sleep(30)
if name == "main":
main()
A few notes. Recent pymodbus 3.x releases call the unit-ID argument device_id, while older ones used slave, so match the version you've installed. Reading each register on its own is slow but easy to debug. Once it works, read contiguous blocks with a larger count and slice the result.
Turning readings into useful alerts
Data is only useful if someone acts on it. These are the rules we find most valuable, and each one is a short SQL query or a few lines of Python:
Battery voltage sliding over several days while the set is stopped, which usually means a failing charger or battery
Fuel level dropping while run hours stay flat, which points to a leak or theft
Coolant temperature trending higher at similar loads, often dust-packed radiator fins in a Pakistani summer
Run hours approaching the next service interval
Send alerts through whatever the team already reads. In most offices here that means WhatsApp or email, not a dashboard nobody opens. Keep the raw readings for at least a year. Summer and winter behave very differently here, and last June's coolant trend is the baseline you'll want this June.
Lessons from running this on real generators
Poll gently. Every 30 to 60 seconds is plenty for trend data, and hammering a controller with requests can slow its own processing. Only read. Many controllers accept write commands that start, stop or change settings, and a bug in a write loop is the last thing you want near a running engine.
Expect gaps. Mobile signal drops, a Pi reboots after an outage, SD cards wear out. Log timestamps in UTC, buffer readings locally, and let systemd restart the script. And talk to whoever services the set before connecting anything, because some service contracts have rules about third-party equipment on the controller.
Where this goes next
Once readings are in SQLite, a Grafana dashboard or a small Flask page is an afternoon's work. If your site is also choosing its next set, ask which controller comes fitted and whether its register map is available before you sign. That question applies to the Baudouin diesel sets we supply and commission as much as to any other make.
For questions about controller options on a new set, or about connecting a logger like this to sets you already run, contact Enpower.
Top comments (0)