nut-debian/drivers/socomec_jbus.c
2022-07-10 09:23:45 +02:00

493 lines
14 KiB
C

/* socomec_jbus.c - Driver for Socomec JBUS UPS
*
* Copyright (C)
* 2021 Thanos Chatziathanassiou <tchatzi@arx.net>
*
* Based on documentation found freely on
* https://www.socomec.com/files/live/sites/systemsite/files/GB-JBUS-MODBUS-for-Delphys-MP-and-Delphys-MX-operating-manual.pdf
* but with dubious legal license. The document itself states:
* ``CAUTION : “This is a product for restricted sales distribution to informed partners.
* Installation restrictions or additional measures may be needed to prevent disturbances''
* YMMV
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
#include "main.h"
#include <modbus.h>
#define DRIVER_NAME "Socomec jbus driver"
#define DRIVER_VERSION "0.06"
#define CHECK_BIT(var,pos) ((var) & (1<<(pos)))
#define MODBUS_SLAVE_ID 1
#define BATTERY_RUNTIME_CRITICAL 15
/* Variables */
static modbus_t *modbus_ctx = NULL;
static int mrir(modbus_t * arg_ctx, int addr, int nb, uint16_t * dest);
/* driver description structure */
upsdrv_info_t upsdrv_info = {
DRIVER_NAME,
DRIVER_VERSION,
"Thanos Chatziathanassiou <tchatzi@arx.net>\n",
DRV_BETA,
{NULL}
};
void upsdrv_initinfo(void)
{
upsdebugx(2, "upsdrv_initinfo");
uint16_t tab_reg[12];
int r;
dstate_setinfo("device.mfr", "socomec jbus");
dstate_setinfo("device.model", "Socomec Generic");
upsdebugx(2, "initial read");
/*
this is a neat trick, but not really helpful right now
https://stackoverflow.com/questions/25811662/spliting-an-hex-into-2-hex-values/41733170#41733170
uint8_t *lowbyte;
uint8_t *hibyte;
*/
r = mrir(modbus_ctx, 0x1000, 12, tab_reg);
if (r == -1) {
fatalx(EXIT_FAILURE, "failed to read UPS code from JBUS. r is %d error %s", r, modbus_strerror(errno));
}
upsdebugx(2, "read UPS Code %d", tab_reg[0]);
if (tab_reg[1]) {
upsdebugx(2, "read UPS Power %d (kVA * 10)", tab_reg[1]);
dstate_setinfo("ups.power", "%u", tab_reg[1]*100 );
}
/* known Socomec Models */
switch (tab_reg[0]) {
case 130:
dstate_setinfo("ups.model", "%s", "DIGYS");
break;
case 515:
dstate_setinfo("ups.model", "%s", "DELPHYS MX");
break;
case 516:
dstate_setinfo("ups.model", "%s", "DELPHYS MX elite");
break;
default:
dstate_setinfo("ups.model", "Unknown Socomec JBUS. Send id %u and specify the model", tab_reg[0]);
}
if (tab_reg[3] && tab_reg[4] && tab_reg[5] && tab_reg[6] && tab_reg[7]) {
dstate_setinfo("ups.serial", "%c%c%c%c%c%c%c%c%c%c",
(tab_reg[3]&0xFF), (tab_reg[3]>>8),
(tab_reg[4]&0xFF), (tab_reg[4]>>8),
(tab_reg[5]&0xFF), (tab_reg[5]>>8),
(tab_reg[6]&0xFF), (tab_reg[6]>>8),
(tab_reg[7]&0xFF), (tab_reg[7]>>8)
);
}
/* upsh.instcmd = instcmd; */
/* upsh.setvar = setvar; */
}
void upsdrv_updateinfo(void)
{
upsdebugx(2, "upsdrv_updateinfo");
uint16_t tab_reg[64];
int r;
status_init();
/* ups configuration */
r = mrir(modbus_ctx, 0x10E0, 32, tab_reg);
if (r == -1 || !tab_reg[0]) {
upsdebugx(2, "Did not receive any data from the UPS at 0x10E0 ! Going stale r is %d error %s", r, modbus_strerror(errno));
dstate_datastale();
return;
}
dstate_setinfo("input.voltage", "%u", tab_reg[0]);
dstate_setinfo("output.voltage", "%u", tab_reg[1]);
dstate_setinfo("input.frequency", "%u", tab_reg[2]);
dstate_setinfo("output.frequency", "%u", tab_reg[3]);
upsdebugx(2, "battery capacity (Ah * 10) %u", tab_reg[8]);
upsdebugx(2, "battery elements %u", tab_reg[9]);
/* time and date */
r = mrir(modbus_ctx, 0x1360, 4, tab_reg);
if (r == -1) {
upsdebugx(2, "Did not receive any data from the UPS at 0x1360 ! Ignoring ? r is %d error %s", r, modbus_strerror(errno));
}
dstate_setinfo("ups.time", "%02d:%02d:%02d", (tab_reg[1]&0xFF), (tab_reg[0]>>8), (tab_reg[0]&0xFF) );
dstate_setinfo("ups.date", "%04d/%02d/%02d", (tab_reg[3]+2000), (tab_reg[2]>>8), (tab_reg[1]>>8) );
/* ups status */
r = mrir(modbus_ctx, 0x1020, 6, tab_reg);
if (r == -1) {
upsdebugx(2, "Did not receive any data from the UPS at 0x1020 ! Ignoring ? r is %d error %s", r, modbus_strerror(errno));
/*
dstate_datastale();
return;
*/
}
if (CHECK_BIT(tab_reg[0], 0))
upsdebugx(2, "Rectifier Input supply present");
if (CHECK_BIT(tab_reg[0], 1))
upsdebugx(2, "Inverter ON ");
if (CHECK_BIT(tab_reg[0], 2))
upsdebugx(2, "Rectifier ON");
if (CHECK_BIT(tab_reg[0], 3))
upsdebugx(2, "Load protected by inverter");
if (CHECK_BIT(tab_reg[0], 4))
upsdebugx(2, "Load on automatic bypass");
if (CHECK_BIT(tab_reg[0], 5))
upsdebugx(2, "Load on battery");
if (CHECK_BIT(tab_reg[0], 6))
upsdebugx(2, "Remote controls disable");
if (CHECK_BIT(tab_reg[0], 7))
upsdebugx(2, "Eco-mode ON");
if (CHECK_BIT(tab_reg[0], 14))
upsdebugx(2, "Battery Test failed");
if (CHECK_BIT(tab_reg[0], 15))
upsdebugx(2, "Battery near end of backup time");
if (CHECK_BIT(tab_reg[0], 16))
upsdebugx(2, "Battery disacharged");
if (CHECK_BIT(tab_reg[1], 0))
upsdebugx(2, "Battery OK");
if (CHECK_BIT(tab_reg[1], 10))
upsdebugx(2, "Bypass input supply present");
if (CHECK_BIT(tab_reg[1], 11))
upsdebugx(2, "Battery charging");
if (CHECK_BIT(tab_reg[1], 12))
upsdebugx(2, "Bypass input frequency out of tolerance");
if (CHECK_BIT(tab_reg[2], 0))
upsdebugx(2, "Unit operating");
if (CHECK_BIT(tab_reg[3], 0))
upsdebugx(2, "Maintenance mode active");
if (CHECK_BIT(tab_reg[4], 0))
upsdebugx(2, "Boost charge ON");
if (CHECK_BIT(tab_reg[4], 2))
upsdebugx(2, "Inverter switch closed");
if (CHECK_BIT(tab_reg[4], 3))
upsdebugx(2, "Bypass breaker closed");
if (CHECK_BIT(tab_reg[4], 4))
upsdebugx(2, "Maintenance bypass breaker closed");
if (CHECK_BIT(tab_reg[4], 5))
upsdebugx(2, "Remote maintenance bypass breaker closed");
if (CHECK_BIT(tab_reg[4], 6))
upsdebugx(2, "Output breaker closed (Q3)");
if (CHECK_BIT(tab_reg[4], 9))
upsdebugx(2, "Unit working");
if (CHECK_BIT(tab_reg[4], 12))
upsdebugx(2, "normal mode active");
/* alarms */
r = mrir(modbus_ctx, 0x1040, 4, tab_reg);
alarm_init();
if (r == -1) {
upsdebugx(2, "Did not receive any data from the UPS at 0x1040 ! Ignoring ? r is %d error %s", r, modbus_strerror(errno));
/*
dstate_datastale();
return;
*/
}
if (CHECK_BIT(tab_reg[0], 0)) {
upsdebugx(2, "General Alarm");
alarm_set("General Alarm present.");
}
if (CHECK_BIT(tab_reg[0], 1)) {
upsdebugx(2, "Battery failure");
alarm_set("Battery failure.");
}
if (CHECK_BIT(tab_reg[0], 2)) {
upsdebugx(2, "UPS overload");
alarm_set("Overload fault.");
}
if (CHECK_BIT(tab_reg[0], 4)) {
upsdebugx(2, "Control failure (com, internal supply...)");
alarm_set("Control failure (com, internal supply...)");
}
if (CHECK_BIT(tab_reg[0], 5)) {
upsdebugx(2, "Rectifier input supply out of tolerance ");
alarm_set("Rectifier input supply out of tolerance.");
}
if (CHECK_BIT(tab_reg[0], 6)) {
upsdebugx(2, "Bypass input supply out of tolerance ");
alarm_set("Bypass input supply out of tolerance.");
}
if (CHECK_BIT(tab_reg[0], 7)) {
upsdebugx(2, "Over temperature alarm ");
alarm_set("Over temperature fault.");
}
if (CHECK_BIT(tab_reg[0], 8)) {
upsdebugx(2, "Maintenance bypass closed");
alarm_set("Maintenance bypass closed.");
}
if (CHECK_BIT(tab_reg[0], 10)) {
upsdebugx(2, "Battery charger fault");
alarm_set("Battery charger fault.");
}
if (CHECK_BIT(tab_reg[1], 1))
upsdebugx(2, "Improper condition of use");
if (CHECK_BIT(tab_reg[1], 2))
upsdebugx(2, "Inverter stopped for overload (or bypass transfer)");
if (CHECK_BIT(tab_reg[1], 3))
upsdebugx(2, "Microprocessor control system");
if (CHECK_BIT(tab_reg[1], 5))
upsdebugx(2, "Synchronisation fault (PLL fault)");
if (CHECK_BIT(tab_reg[1], 6))
upsdebugx(2, "Rectifier input supply fault");
if (CHECK_BIT(tab_reg[1], 7))
upsdebugx(2, "Rectifier preventive alarm");
if (CHECK_BIT(tab_reg[1], 9))
upsdebugx(2, "Inverter preventive alarm");
if (CHECK_BIT(tab_reg[1], 10))
upsdebugx(2, "Charger general alarm");
if (CHECK_BIT(tab_reg[1], 13))
upsdebugx(2, "Bypass preventive alarm");
if (CHECK_BIT(tab_reg[1], 15)) {
upsdebugx(2, "Imminent STOP");
alarm_set("Imminent STOP.");
}
if (CHECK_BIT(tab_reg[2], 12)) {
upsdebugx(2, "Servicing alarm");
alarm_set("Servicing alarm.");
}
if (CHECK_BIT(tab_reg[2], 15))
upsdebugx(2, "Battery room alarm");
if (CHECK_BIT(tab_reg[3], 0)) {
upsdebugx(2, "Maintenance bypass alarm");
alarm_set("Maintenance bypass.");
}
if (CHECK_BIT(tab_reg[3], 1)) {
upsdebugx(2, "Battery discharged");
alarm_set("Battery discharged.");
}
if (CHECK_BIT(tab_reg[3], 3))
upsdebugx(2, "Synoptic alarm");
if (CHECK_BIT(tab_reg[3], 4)) {
upsdebugx(2, "Critical Rectifier fault");
alarm_set("Critical Rectifier fault.");
}
if (CHECK_BIT(tab_reg[3], 6)) {
upsdebugx(2, "Critical Inverter fault");
alarm_set("Critical Inverter fault.");
}
if (CHECK_BIT(tab_reg[3], 10))
upsdebugx(2, "ESD activated");
if (CHECK_BIT(tab_reg[3], 11)) {
upsdebugx(2, "Battery circuit open");
alarm_set("Battery circuit open.");
}
if (CHECK_BIT(tab_reg[3], 14)) {
upsdebugx(2, "Bypass critical alarm");
alarm_set("Bypass critical alarm.");
}
/* measurements */
r = mrir(modbus_ctx, 0x1060, 48, tab_reg);
if (r == -1) {
upsdebugx(2, "Did not receive any data from the UPS at 0x1060 ! Ignoring ? r is %d error %s", r, modbus_strerror(errno));
/*
dstate_datastale();
return;
*/
}
if (tab_reg[1] == 0xFFFF && tab_reg[2] == 0xFFFF) {
/* this a 1-phase model */
dstate_setinfo("input.phases", "1" );
dstate_setinfo("ups.load", "%u", tab_reg[0] );
dstate_setinfo("input.bypass.voltage", "%u", tab_reg[6] );
dstate_setinfo("output.voltage", "%u", tab_reg[9] );
if (tab_reg[15] != 0xFFFF)
dstate_setinfo("output.current", "%u", tab_reg[15] );
}
else {
/* this a 3-phase model */
dstate_setinfo("input.phases", "3" );
dstate_setinfo("ups.load", "%u", tab_reg[3] );
dstate_setinfo("ups.L1.load", "%u", tab_reg[0] );
dstate_setinfo("ups.L2.load", "%u", tab_reg[1] );
dstate_setinfo("ups.L3.load", "%u", tab_reg[2] );
dstate_setinfo("input.bypass.L1-N.voltage", "%u", tab_reg[6] );
dstate_setinfo("input.bypass.L2-N.voltage", "%u", tab_reg[7] );
dstate_setinfo("input.bypass.L3-N.voltage", "%u", tab_reg[8] );
dstate_setinfo("output.L1-N.voltage", "%u", tab_reg[9] );
dstate_setinfo("output.L2-N.voltage", "%u", tab_reg[10] );
dstate_setinfo("output.L3-N.voltage", "%u", tab_reg[11] );
if (tab_reg[15] != 0xFFFF)
dstate_setinfo("output.L1.current", "%u", tab_reg[15] );
if (tab_reg[16] != 0xFFFF)
dstate_setinfo("output.L2.current", "%u", tab_reg[16] );
if (tab_reg[17] != 0xFFFF)
dstate_setinfo("output.L3.current", "%u", tab_reg[17] );
}
dstate_setinfo("battery.charge", "%u", tab_reg[4] );
dstate_setinfo("battery.capacity", "%u", (tab_reg[5]/10) );
dstate_setinfo("battery.voltage", "%.2f", (double) (tab_reg[20]) / 10);
dstate_setinfo("battery.current", "%.2f", (double) (tab_reg[24]) / 10 );
dstate_setinfo("battery.runtime", "%u", tab_reg[23] );
dstate_setinfo("input.bypass.frequency", "%u", (tab_reg[18]/10) );
dstate_setinfo("output.frequency", "%u", (tab_reg[19]/10) );
if (tab_reg[22] != 0xFFFF) {
dstate_setinfo("ambient.1.present", "yes");
dstate_setinfo("ambient.1.temperature", "%u", tab_reg[22] );
}
if (tab_reg[23] == 0xFFFF) {
/* battery.runtime == 0xFFFF means we're on mains */
status_set("OL");
}
else if (tab_reg[23] > BATTERY_RUNTIME_CRITICAL) {
/* we still have mora than BATTERY_RUNTIME_CRITICAL min left ? */
status_set("OB");
}
else {
status_set("LB");
}
/*TODO:
--essential
ups.status TRIM/BOOST/OVER
ups.alarm
--dangerous
ups.shutdown
shutdown.return
shutdown.stop
shutdown.reboot
shutdown.reboot.graceful
bypass.start
beeper.enable
beeper.disable
*/
alarm_commit();
status_commit();
dstate_dataok();
return;
}
void upsdrv_shutdown(void)
__attribute__((noreturn));
void upsdrv_shutdown(void)
{
fatalx(EXIT_FAILURE, "shutdown not supported");
}
void upsdrv_help(void)
{
}
/* list flags and values that you want to receive via -x */
void upsdrv_makevartable(void)
{
}
void upsdrv_initups(void)
{
int r;
upsdebugx(2, "upsdrv_initups");
modbus_ctx = modbus_new_rtu(device_path, 9600, 'N', 8, 1);
if (modbus_ctx == NULL)
fatalx(EXIT_FAILURE, "Unable to create the libmodbus context");
r = modbus_set_slave(modbus_ctx, MODBUS_SLAVE_ID); /* slave ID */
if (r < 0) {
modbus_free(modbus_ctx);
fatalx(EXIT_FAILURE, "Invalid modbus slave ID %d",MODBUS_SLAVE_ID);
}
if (modbus_connect(modbus_ctx) == -1) {
modbus_free(modbus_ctx);
fatalx(EXIT_FAILURE, "modbus_connect: unable to connect: %s", modbus_strerror(errno));
}
}
void upsdrv_cleanup(void)
{
if (modbus_ctx != NULL) {
modbus_close(modbus_ctx);
modbus_free(modbus_ctx);
}
}
/* Modbus Read Input Registers */
static int mrir(modbus_t * arg_ctx, int addr, int nb, uint16_t * dest)
{
int r, i;
/* zero out the thing, because we might have reused it */
for (i=0; i<nb; i++) {
dest[i] = 0;
}
/*r = modbus_read_input_registers(arg_ctx, addr, nb, dest);*/
r = modbus_read_registers(arg_ctx, addr, nb, dest);
if (r == -1) {
upslogx(LOG_ERR, "mrir: modbus_read_input_registers(addr:%d, count:%d): %s (%s)", addr, nb, modbus_strerror(errno), device_path);
}
return r;
}