33 * as a single UPS via its Modbus TCP interface.
44 *
55 * Copyright (C)
6- * 2026 Miklos Bagi <mb_sigennut @mbag.at>
6+ * 2026 Miklos Bagi <mb_nutdco @mbag.at>
77 *
88 * This program is free software; you can redistribute it and/or modify
99 * it under the terms of the GNU General Public License as published by
2323/*
2424 * Register addresses and their meanings are sourced from Sigenergy's
2525 * own official Modbus Protocol spec (V2.9), cross-checked against real
26- * device captures. See ../../nut-sigenergy/docs/driver-coding-standards.md
27- * and ../../sigennut/docs/register-map.md (sibling research repos) for
28- * the full provenance, including one still-open item this driver's
29- * OL/OB mapping depends on:
26+ * device captures on a live installation. One item this driver's OL/OB
27+ * mapping depends on is still open:
3028 *
31- * R11 (from sigennut's PRD): `on_off_grid_status` (30009) is documented
32- * by the spec as the plant's real on/off-grid signal, and that mapping
33- * is used directly below -- but it has not yet been validated against
34- * an actual real-world grid-loss/grid-return transition on the specific
35- * installation this was developed against (no grid was ever wired to
36- * that unit). Treat OL/OB as spec-correct-by-design but not yet
37- * field-proven; revisit if a real transition is ever observed to behave
38- * differently than expected.
29+ * `on_off_grid_status` (30009) is documented by the spec as the plant's
30+ * real on/off-grid signal, and that mapping is used directly below --
31+ * but it has not yet been validated against an actual real-world
32+ * grid-loss/grid-return transition on the specific installation this
33+ * was developed against (no grid was ever wired to that unit). Treat
34+ * OL/OB as spec-correct-by-design but not yet field-proven; revisit if
35+ * a real transition is ever observed to behave differently than
36+ * expected.
3937 *
4038 * This is a deliberately minimal first driver (project outline "Phase
4139 * 1"): OL / OB / LB / CHRG / DISCHRG, battery.charge, input.voltage,
9290upsdrv_info_t upsdrv_info = {
9391 DRIVER_NAME ,
9492 DRIVER_VERSION ,
95- "Miklos Bagi <mb_sigennut @mbag.at>" ,
93+ "Miklos Bagi <mb_nutdco @mbag.at>" ,
9694 DRV_EXPERIMENTAL ,
9795 { NULL }
9896};
@@ -114,8 +112,7 @@ static int errcount = 0;
114112 * here). No IPv6 bracket handling needed: Sigenergy's Modbus TCP
115113 * interface is IPv4/hostname only.
116114 */
117- static int parse_host_port (const char * input , char * host , size_t host_buf_size ,
118- char * port , size_t port_buf_size , uint16_t default_port )
115+ static int parse_host_port (const char * input , char * host , size_t host_buf_size , char * port , size_t port_buf_size , uint16_t default_port )
119116{
120117 const char * colon ;
121118 size_t host_size , port_size ;
@@ -175,8 +172,7 @@ static int read_regs(int unit, int addr, int count, uint16_t *dest)
175172 }
176173
177174 if (modbus_read_registers (mbctx , addr , count , dest ) == -1 ) {
178- upslogx (LOG_ERR , "read_regs: unit %d addr %d count %d: %s" ,
179- unit , addr , count , modbus_strerror (errno ));
175+ upslogx (LOG_ERR , "read_regs: unit %d addr %d count %d: %s" , unit , addr , count , modbus_strerror (errno ));
180176 errcount ++ ;
181177 return -1 ;
182178 }
@@ -267,7 +263,13 @@ void upsdrv_initinfo(void)
267263 * "Low-Battery Policy" section: BESS reserve is a policy choice,
268264 * not something to blindly inherit from the device. */
269265 if (getval ("low_battery_soc" )) {
270- low_battery_soc = strtod (getval ("low_battery_soc" ), NULL );
266+ char * soc_endptr ;
267+ const char * soc_arg = getval ("low_battery_soc" );
268+
269+ low_battery_soc = strtod (soc_arg , & soc_endptr );
270+ if (soc_endptr == soc_arg || * soc_endptr != '\0' ) {
271+ fatalx (EXIT_FAILURE , "upsdrv_initinfo: low_battery_soc '%s' is not a valid number" , soc_arg );
272+ }
271273 } else if (read_regs (plant_addr , REG_ESS_DISCHARGE_CUTOFF_SOC , 1 , & reg ) == 0 && reg > 0 ) {
272274 low_battery_soc = reg / 10.0 ;
273275 }
@@ -284,9 +286,7 @@ void upsdrv_updateinfo(void)
284286
285287 errcount = 0 ;
286288
287- if (read_regs (plant_addr , REG_ON_OFF_GRID_STATUS , 1 , & grid_status ) == -1
288- || read_regs (plant_addr , REG_ESS_SOC , 1 , & soc ) == -1
289- || read_regs (plant_addr , REG_ESS_POWER , 2 , ess_power_regs ) == -1 ) {
289+ if (read_regs (plant_addr , REG_ON_OFF_GRID_STATUS , 1 , & grid_status ) == -1 || read_regs (plant_addr , REG_ESS_SOC , 1 , & soc ) == -1 || read_regs (plant_addr , REG_ESS_POWER , 2 , ess_power_regs ) == -1 ) {
290290 dstate_datastale ();
291291 return ;
292292 }
@@ -330,12 +330,7 @@ void upsdrv_updateinfo(void)
330330 * spec's Appendix 2-13 enumerations is deferred, see this file's
331331 * header comment (R2/Phase 3). general_alarm7 (30281) is
332332 * deliberately excluded here, see header comment. */
333- if (read_regs (plant_addr , REG_GENERAL_ALARM1 , 1 , & alarm1 ) == 0
334- && read_regs (plant_addr , REG_GENERAL_ALARM2 , 1 , & alarm2 ) == 0
335- && read_regs (plant_addr , REG_GENERAL_ALARM3 , 1 , & alarm3 ) == 0
336- && read_regs (plant_addr , REG_GENERAL_ALARM4 , 1 , & alarm4 ) == 0
337- && read_regs (plant_addr , REG_GENERAL_ALARM5 , 1 , & alarm5 ) == 0
338- && read_regs (plant_addr , REG_GENERAL_ALARM6 , 1 , & alarm6 ) == 0 ) {
333+ if (read_regs (plant_addr , REG_GENERAL_ALARM1 , 1 , & alarm1 ) == 0 && read_regs (plant_addr , REG_GENERAL_ALARM2 , 1 , & alarm2 ) == 0 && read_regs (plant_addr , REG_GENERAL_ALARM3 , 1 , & alarm3 ) == 0 && read_regs (plant_addr , REG_GENERAL_ALARM4 , 1 , & alarm4 ) == 0 && read_regs (plant_addr , REG_GENERAL_ALARM5 , 1 , & alarm5 ) == 0 && read_regs (plant_addr , REG_GENERAL_ALARM6 , 1 , & alarm6 ) == 0 ) {
339334 if (alarm1 || alarm2 || alarm3 || alarm4 || alarm5 || alarm6 ) {
340335 status_set ("ALARM" );
341336 }
@@ -367,12 +362,7 @@ void upsdrv_shutdown(void)
367362
368363void upsdrv_help (void )
369364{
370- printf ("\nSigenergy-specific options:\n"
371- " plant_addr: Modbus unit id for plant-level registers (default: %d)\n"
372- " inverter_addr: Modbus unit id for inverter-level registers (default: %d)\n"
373- " low_battery_soc: LB threshold in %% state-of-charge (default: device's own\n"
374- " configured reserve floor if nonzero, else %.0f)\n" ,
375- DEFAULT_PLANT_ADDR , DEFAULT_INVERTER_ADDR , DEFAULT_LOW_BATTERY_SOC );
365+ printf ("\nSigenergy-specific options:\n" " plant_addr: Modbus unit id for plant-level registers (default: %d)\n" " inverter_addr: Modbus unit id for inverter-level registers (default: %d)\n" " low_battery_soc: LB threshold in %% state-of-charge (default: device's own\n" " configured reserve floor if nonzero, else %.0f)\n" , DEFAULT_PLANT_ADDR , DEFAULT_INVERTER_ADDR , DEFAULT_LOW_BATTERY_SOC );
376366}
377367
378368void upsdrv_tweak_prognames (void )
0 commit comments