-
-
Notifications
You must be signed in to change notification settings - Fork 366
/
Copy pathhuawei-ups2000.c
2149 lines (1859 loc) · 63.4 KB
/
huawei-ups2000.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* huawei-ups2000.c - Driver for Huawei UPS2000 (1kVA-3kVA)
*
* Note: If you're trying to debug the driver because it doesn't work,
* please BE SURE to read the manual in "docs/man/huawei-ups2000.txt"
* first! Otherwise you are guaranteed to waste your time!
*
* Long story short, Huawei UPS2000 (1kVA-3kVA) can be accessed via
* RS-232, USB, or an optional RMS-MODBUS01B (RS-485) adapter. Only
* RS-232 and USB are supported, RS-485 is not. Also, for most UPS
* units, their USB ports are implemented via the MaxLinear RX21V1410
* USB-to-serial converter, and they DO NOT WORK without a special
* "xr_serial" driver, only available on Linux 5.12+ (not BSD or Solaris).
* Without this driver, the USB can still be recognized as a generic
* USB ACM device, but it DOES NOT WORK. Alternatively, some newer UPS
* units use the WCH CH341 chip, which should have better compatibility.
* Detailed information will not be repeated here, please read
* "docs/man/huawei-ups2000.txt".
*
* A document describing the protocol implemented by this driver can
* be found online at:
*
* https://support.huawei.com/enterprise/en/doc/EDOC1000110696
*
* Huawei UPS2000 driver implemented by
* Copyright (C) 2020, 2021 Yifeng Li <[email protected]>
* The author is not affiliated with Huawei or other manufacturers.
*
* 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 "config.h" /* must be the first header */
#include <stdbool.h>
#include <modbus.h>
#include "main.h"
#include "serial.h"
#include "nut_stdint.h"
#include "timehead.h" /* fallback gmtime_r() variants if needed (e.g. some WIN32) */
#define DRIVER_NAME "NUT Huawei UPS2000 (1kVA-3kVA) RS-232 Modbus driver"
#define DRIVER_VERSION "0.08"
#define CHECK_BIT(var,pos) ((var) & (1<<(pos)))
#define MODBUS_SLAVE_ID 1
/*
* Known UPS models. We only attempt to load the driver if
* the initial communication indicates the UPS is a known
* model of the UPS2000 series.
*/
static const char *supported_model[] = {
"UPS2000", "UPS2000A", "UPS2000G",
NULL
};
/*
* UPS2000 device identification. The information is obtained during
* initial communication using Modbus command 0x2B (read device identi-
* fication) to read the object 0x87 (device list). The object contains
* a list of fields, each with a type, length, and value. The object is
* parsed by ups2000_device_identification() and filled into the array
* of struct ups2000_ident.
*
* Fields of interest are:
*
* 0x87, int32 (Device Count): Only one UPS unit is supported,
* the driver aborts if more than one device is detected.
*
* 0x88, string (Device Description of the 1st unit): This is a
* ASCII string that contains information about the 1st UPS unit.
* This string, again, contains a list of fields. They are parsed
* further into the array ups2000_desc.
*
*/
#define UPS2000_IDENT_MAX_FIELDS 9
#define UPS2000_IDENT_MAX_LEN 128
#define UPS2000_IDENT_OFFSET
static struct {
uint8_t type;
uint8_t len;
uint8_t val[UPS2000_IDENT_MAX_LEN];
} ups2000_ident[UPS2000_IDENT_MAX_FIELDS];
/*
* UPS2000 device description. The information is initially obtained
* as field 0x88 in the UPS2000 device identification. This field is
* a semicolon seperated ASCII string that contains multiple fields.
* It is parsed again by ups2000_device_identification() and filled
* into the ups2000_desc[] 2D array. The first dimension is used as
* a key to select the wanted field (defined in the following enmu,
* the second dimension is a NULL-terminated ASCII string.
*
* Note that ups2000_desc[0] is deliberately unused, the array begins
* at one, allowing mapping from UPS2000_DESC_* to ups2000_desc[]
* directly without using offsets.
*/
#define UPS2000_DESC_MAX_FIELDS 9
#define UPS2000_DESC_MAX_LEN 128
enum {
UPS2000_DESC_MODEL = 1,
UPS2000_DESC_FIRMWARE_REV,
UPS2000_DESC_PROTOCOL_REV,
UPS2000_DESC_ESN,
UPS2000_DESC_DEVICE_ID, /* currently unused */
UPS2000_DESC_PARALLEL_ID /* currently unused */
};
static char ups2000_desc[UPS2000_DESC_MAX_FIELDS][UPS2000_DESC_MAX_LEN] = { { 0 } };
/* global variable for modbus communication */
static modbus_t *modbus_ctx = NULL;
/*
* How many seconds to wait before switching off/on/reboot the UPS?
*
* This can be set at startup time via a command-line argument,
* or at runtime by writing to RW variables "ups.delay.shutdown"
* and "ups.delay.start". See ups2000_delay_get/set.
*/
#define UPS2000_DELAY_INVALID 0xFFFF
static uint16_t ups2000_offdelay = UPS2000_DELAY_INVALID;
static uint16_t ups2000_ondelay = UPS2000_DELAY_INVALID;
static uint16_t ups2000_rebootdelay = UPS2000_DELAY_INVALID;
/*
* Time when the current shutdown/reboot request is expected
* to complete. This is used to calculate the ETA, See
* ups2000_update_timers().
*/
static time_t shutdown_at = 0;
static time_t reboot_at = 0;
static time_t start_at = 0;
/*
* Is it safe to enter bypass mode? It's checked by ups2000_update_alarm()
* and used by ups2000_instcmd_bypass_start().
*/
static bool bypass_available = 0;
/* function prototypes */
static int ups2000_update_info(void);
static int ups2000_update_status(void);
static int ups2000_update_alarm(void);
static int ups2000_update_timers(void);
static void ups2000_device_identification(void);
static size_t ups2000_read_serial(uint8_t *buf, size_t buf_len);
static int ups2000_read_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest);
static int ups2000_write_register(modbus_t *ctx, int addr, uint16_t val);
static int ups2000_write_registers(modbus_t *ctx, int addr, int nb, uint16_t *src);
static uint16_t crc16(uint8_t *buffer, size_t buffer_length);
static time_t time_seek(time_t t, int seconds);
/* rw variables function prototypes */
static int ups2000_update_rw_var(void);
static int setvar(const char *name, const char *val);
static int ups2000_autostart_set(const uint16_t reg, const char *string);
static int ups2000_autostart_get(const uint16_t reg);
static int ups2000_beeper_set(const uint16_t reg, const char *string);
static int ups2000_beeper_get(const uint16_t reg);
static void ups2000_delay_get(void);
static int ups2000_delay_set(const char *var, const char *string);
/* instant command function prototypes */
static void ups2000_init_instcmd(void);
static int instcmd(const char *cmd, const char *extra);
static int ups2000_instcmd_load_on(const uint16_t reg);
static int ups2000_instcmd_bypass_start(const uint16_t reg);
static int ups2000_instcmd_beeper_toggle(const uint16_t reg);
static int ups2000_instcmd_shutdown_stayoff(const uint16_t reg);
static int ups2000_instcmd_shutdown_return(const uint16_t reg);
static int ups2000_instcmd_shutdown_reboot(const uint16_t reg);
static int ups2000_instcmd_shutdown_reboot_graceful(const uint16_t reg);
/* driver description structure */
upsdrv_info_t upsdrv_info = {
DRIVER_NAME,
DRIVER_VERSION,
"Yifeng Li <[email protected]>\n",
DRV_EXPERIMENTAL,
{ NULL }
};
void upsdrv_initups(void)
{
int r;
upsdebugx(2, "upsdrv_initups");
/*
* This is an ugly workaround to a serious problem: libmodbus doesn't
* support device identification. Although there's a function called
* modbus_send_raw_request() for custom commands, but modbus_receive_
* confirmation() assumes a message length in the header, which is
* incompatible with device identification - It simply stops reading
* in the middle of the message and cannot receive our message. Worse,
* there's no public API to receive a raw response.
*
* See: https://github.com/stephane/libmodbus/issues/231
*
* Thus, the only thing we could do is opening it as a serial device
* for device identification, and reopen it via libmodbus for other
* commands as usual. We also have to copy the CRC-16 function from
* the libmodbus source code since there's no public API to use that...
*/
upsfd = ser_open(device_path);
ser_set_speed(upsfd, device_path, B9600);
ser_set_rts(upsfd, 0);
ser_set_dtr(upsfd, 0);
modbus_ctx = modbus_new_rtu(device_path, 9600, 'N', 8, 1);
if (modbus_ctx == NULL)
fatalx(EXIT_FAILURE, "Unable to create the libmodbus context");
#if LIBMODBUS_VERSION_CHECK(3, 1, 2)
/*
* Although it rarely occurs, it can take as slow as 2 sec. for the
* UPS to respond a read and finish transmitting the message.
*/
modbus_set_response_timeout(modbus_ctx, 2, 0);
#else
{
struct timeval timeout;
timeout.tv_sec = 2;
timeout.tv_usec = 0;
modbus_set_response_timeout(modbus_ctx, &timeout);
}
#endif
r = modbus_set_slave(modbus_ctx, MODBUS_SLAVE_ID);
if (r < 0) {
modbus_free(modbus_ctx);
fatalx(EXIT_FAILURE, "Invalid 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));
}
}
#define IDENT_REQUEST_LEN 7
#define IDENT_RESPONSE_MAX_LEN 128
#define IDENT_RESPONSE_HEADER_LEN 8
#define IDENT_RESPONSE_CRC_LEN 2
#define IDENT_FIELD_HEADER_LEN 2
static void ups2000_device_identification(void)
{
static const uint8_t ident_req[IDENT_REQUEST_LEN] = {
MODBUS_SLAVE_ID, /* addr */
0x2B, /* command: device identification */
0x0E, /* MEI type */
0x03, /* ReadDevID: extended identification */
0x87, /* Object ID: device list */
0x31, 0x75 /* CRC-16 */
};
/*
* Response header:
* 0x01, 0x2B, 0x0E, 0x03, 0x03, 0x00, 0x00, 0x02
*
* Response fields:
* header: 0x87, 0x04 // type (device counts), length
* data: uint32_t
* (e.g. 0x00, 0x00, 0x00, 0x01)
*
* header: 0x88, 0x?? // type (1st dev desc), length
* data: ASCII string
* (e.g. 1=UPS2000;2=V100R001C01SPC120;3=...)
*
* header: 0x89, 0x?? // type (2nd dev desc), length
* data: ASCII string
*
* ...
* header: 0xFF, 0x?? // type (120th dev desc), length
* data: ASCII string
*
* CRC-16:
* 0x??, 0x??
*/
static const uint8_t expected_header[IDENT_RESPONSE_HEADER_LEN] = {
MODBUS_SLAVE_ID,
0x2B, 0x0E, 0x03, 0x03, 0x00, 0x00, 0x02,
};
bool serial_fail = 0; /* unable to read from serial */
uint16_t crc16_recv, crc16_calc; /* resp CRC */
bool crc16_fail = 0; /* resp CRC failure */
uint32_t ups_count = 0; /* number of UPS in the resp list */
uint8_t ident_response[IDENT_RESPONSE_MAX_LEN]; /* resp buf */
size_t ident_response_len; /* buf len */
uint8_t *ident_response_end = NULL; /* buf end marker (excluding CRC) */
uint8_t *ptr = NULL; /* buf iteratior */
/* a desc string copied from ups2000_ident[] */
char *ups2000_ident_desc = NULL;
int i;
ssize_t r;
/* attempt to obtain a response header with valid CRC. */
for (i = 0; i < 3; i++) {
/* step 1: record response length and initialize ptr */
upsdebugx(2, "ser_send_buf");
ser_flush_in(upsfd, "", nut_debug_level);
r = ser_send_buf(upsfd, ident_req, IDENT_REQUEST_LEN);
if (r != IDENT_REQUEST_LEN) {
fatalx(EXIT_FAILURE, "unable to send request!");
}
ident_response_len = ups2000_read_serial(ident_response, IDENT_RESPONSE_MAX_LEN);
ptr = ident_response;
ident_response_end = ptr + ident_response_len - IDENT_RESPONSE_CRC_LEN;
/* step 2: check response length */
if (ident_response_len == 0) {
upslogx(LOG_ERR, "unable to read from serial port %s, retry...", device_path);
serial_fail = 1;
continue;
}
else
serial_fail = 0;
upsdebug_hex(2, "ups2000_read_serial() received", ptr, ident_response_len);
if (ptr + IDENT_RESPONSE_HEADER_LEN > ident_response_end) {
fatalx(EXIT_FAILURE, "response header too short! "
"expected %d, received %" PRIuSIZE ".",
IDENT_RESPONSE_HEADER_LEN, ident_response_len);
}
/* step 3: check response CRC-16 */
crc16_recv = (uint16_t) ident_response_end[0] << 8 | ident_response_end[1];
crc16_calc = crc16(ident_response, ident_response_len - IDENT_RESPONSE_CRC_LEN);
if (crc16_recv == crc16_calc) {
crc16_fail = 0;
break;
}
crc16_fail = 1;
}
/* step 4: check serial & CRC-16 verification status */
if (serial_fail)
fatalx(EXIT_FAILURE, "unable to read from serial port %s!", device_path);
if (crc16_fail)
fatalx(EXIT_FAILURE, "response CRC verification failed!");
/* step 5: check response header */
if (memcmp(expected_header, ident_response, IDENT_RESPONSE_HEADER_LEN))
fatalx(EXIT_FAILURE, "unexpected response header!");
ptr += IDENT_RESPONSE_HEADER_LEN;
/* step 6: extract ident fields */
memset(ups2000_ident, 0x00, sizeof(ups2000_ident));
for (i = 0; i < UPS2000_IDENT_MAX_FIELDS; i++) {
uint8_t type, len;
if (ptr + 2 > ident_response_end)
break;
type = *ptr++;
len = *ptr++;
if (len + 1 > UPS2000_IDENT_MAX_LEN)
fatalx(EXIT_FAILURE, "response field too long!");
ups2000_ident[i].type = type;
ups2000_ident[i].len = len;
/*
* Always zero-terminate the bytes, in case the data
* is an ASCII string (i.e. device desc string), libc
* string functions can be used.
*/
ups2000_ident[i].val[len] = '\0';
if (ptr + len > ident_response_end)
fatalx(EXIT_FAILURE, "response field too short!");
memcpy(ups2000_ident[i].val, ptr, len);
ptr += len;
}
/* step 7: validate device identification field 0x87 and 0x88 */
for (i = 0; i < UPS2000_IDENT_MAX_FIELDS; i++) {
/* only one device is supported */
if (ups2000_ident[i].type == 0x87) {
/* so we assume 0x87 must be 1 */
ups_count =
(uint32_t)(ups2000_ident[i].val[0]) << 24 |
(uint32_t)(ups2000_ident[i].val[1]) << 16 |
(uint32_t)(ups2000_ident[i].val[2]) << 8 |
(uint32_t)(ups2000_ident[i].val[3]);
}
if (ups2000_ident[i].type == 0x88) {
/*
* And only check 0x88, not 0x89, etc. Also copy the
* string for later parsing via strtok().
*/
ups2000_ident_desc = strdup((char *) ups2000_ident[i].val);
break;
}
}
if (ups_count != 1)
fatalx(EXIT_FAILURE, "only 1 UPS is supported, %u found", ups_count);
if (!ups2000_ident_desc)
fatalx(EXIT_FAILURE, "device desc string not found");
/*
* step 8: extract fields from the desc string.
* (1=UPS2000;2=V100R001C01SPC120;3=...)
*/
for (i = 0; i < UPS2000_DESC_MAX_FIELDS; i++) {
char *key; /* "1", "2", "3", ... */
char *val; /* "UPS2000", "V100R001C01SPC120", ... */
unsigned int idx = 0;
if (i == 0)
key = strtok(ups2000_ident_desc, "=");
else
key = strtok(NULL, "=");
if (!key)
break;
val = strtok(NULL, ";");
if (!val)
break;
r = str_to_uint_strict(key, &idx, 10);
if (!r || idx + 1 > UPS2000_DESC_MAX_FIELDS || idx < 1)
fatalx(EXIT_FAILURE, "desc index %u is invalid!", idx);
if (strlen(val) + 1 > UPS2000_DESC_MAX_LEN)
fatalx(EXIT_FAILURE, "desc field %u too long!", idx);
memcpy(ups2000_desc[idx], val, strlen(val) + 1);
}
free(ups2000_ident_desc);
/*
* step 9: Validate desc fields that we are going to use are valid.
*
* Note: UPS2000_DESC_DEVICE_ID and UPS2000_DESC_PARALLEL_ID are
* currently unused and unchecked.
*/
for (i = UPS2000_DESC_MODEL; i <= UPS2000_DESC_ESN; i++) {
if (strlen(ups2000_desc[i]) == 0)
fatalx(EXIT_FAILURE, "desc field %d is missing!", i);
}
}
void upsdrv_initinfo(void)
{
bool in_list = 0;
int i = 0;
upsdebugx(2, "upsdrv_initinfo");
ups2000_device_identification();
/* check whether the UPS is a known model */
for (i = 0; supported_model[i] != NULL; i++) {
if (!strcmp(supported_model[i],
ups2000_desc[UPS2000_DESC_MODEL])
) {
in_list = 1;
}
}
if (!in_list) {
fatalx(EXIT_FAILURE, "Unknown UPS model %s",
ups2000_desc[UPS2000_DESC_MODEL]);
}
dstate_setinfo("device.mfr", "Huawei");
dstate_setinfo("device.type", "ups");
dstate_setinfo("device.model", "%s",
ups2000_desc[UPS2000_DESC_MODEL]);
dstate_setinfo("device.serial", "%s",
ups2000_desc[UPS2000_DESC_ESN]);
dstate_setinfo("ups.mfr", "Huawei");
dstate_setinfo("ups.model", "%s",
ups2000_desc[UPS2000_DESC_MODEL]);
dstate_setinfo("ups.firmware", "%s",
ups2000_desc[UPS2000_DESC_FIRMWARE_REV]);
dstate_setinfo("ups.firmware.aux", "%s",
ups2000_desc[UPS2000_DESC_PROTOCOL_REV]);
dstate_setinfo("ups.serial", "%s",
ups2000_desc[UPS2000_DESC_ESN]);
dstate_setinfo("ups.type", "online");
/* RW variables */
upsh.setvar = setvar;
/* instant commands */
ups2000_init_instcmd();
upsh.instcmd = instcmd;
}
/*
* All registers are uint16_t. But the data they represent can
* be either an integer or a float. This information is used for
* error checking (int and float have different invalid values).
*/
enum {
REG_UINT16,
REG_UINT32, /* occupies two registers */
REG_FLOAT, /* actually a misnomer, it should really be called
* fixed-point number, but we follow the datasheet */
};
#define REG_UINT16_INVALID 0xFFFFU
#define REG_UINT32_INVALID 0xFFFFFFFFU
#define REG_FLOAT_INVALID 0x7FFFU
/*
* Declare UPS attribute variables, format strings, registers,
* and their scaling factors in a lookup table to avoid spaghetti
* code.
*/
static struct {
const char *name;
const char *fmt;
const uint16_t reg;
const int datatype; /* only UINT32 occupies 2 regs */
const float scaling; /* scale it down to get the original */
} ups2000_var[] =
{
{ "input.voltage", "%03.1f", 1000, REG_FLOAT, 10.0 },
{ "input.frequency", "%02.1f", 1003, REG_FLOAT, 10.0 },
{ "input.bypass.voltage", "%03.1f", 1004, REG_FLOAT, 10.0 },
{ "input.bypass.frequency", "%03.1f", 1007, REG_FLOAT, 10.0 },
{ "output.voltage", "%03.1f", 1008, REG_FLOAT, 10.0 },
{ "output.current", "%03.1f", 1011, REG_FLOAT, 10.0 },
{ "output.frequency", "%03.1f", 1014, REG_FLOAT, 10.0 },
{ "output.realpower", "%02.1f", 1015, REG_FLOAT, 0.01 }, /* 10 / 1 kW */
{ "output.power", "%03.1f", 1018, REG_FLOAT, 0.01 }, /* 10 / 1 kVA */
{ "ups.load", "%02.1f", 1021, REG_FLOAT, 10.0 },
{ "ups.temperature", "%02.1f", 1027, REG_FLOAT, 10.0 },
{ "battery.voltage", "%02.1f", 2000, REG_FLOAT, 10.0 },
{ "battery.charge", "%02.1f", 2003, REG_UINT16, 1.0 },
{ "battery.runtime", "%.0f", 2004, REG_UINT32, 1.0 },
{ "battery.packs", "%.0f", 2007, REG_UINT16, 1.0 },
{ "battery.capacity", "%.0f", 2033, REG_UINT16, 1.0 },
{ "ups.power.nominal", "%.0f", 9009, REG_FLOAT, 0.01 }, /* 10 / 1 kVA */
{ NULL, NULL, 0, 0, 0 },
};
static int ups2000_update_info(void)
{
uint16_t reg[3][34];
int i;
int r;
upsdebugx(2, "ups2000_update_info");
/*
* All status registers have an offset of 10000 * ups_number.
* We only support 1 UPS, thus it's always 10000. Register
* 1000 becomes 11000.
*/
r = ups2000_read_registers(modbus_ctx, 11000, 28, reg[0]);
if (r != 28)
return 1;
r = ups2000_read_registers(modbus_ctx, 12000, 34, reg[1]);
if (r != 34)
return 1;
r = ups2000_read_registers(modbus_ctx, 19009, 1, ®[2][9]);
if (r != 1)
return 1;
for (i = 0; ups2000_var[i].name != NULL; i++) {
uint16_t reg_id = ups2000_var[i].reg;
uint8_t page = (uint8_t)(reg_id / 1000 - 1);
uint8_t idx = (uint8_t)(reg_id % 1000);
uint32_t val;
bool invalid = 0;
if (page == 8) /* hack for the lonely register 9009 */
page = 2;
if (page > 2 || idx > 33) /* also suppress compiler warn */
fatalx(EXIT_FAILURE, "register calculation overflow!");
switch (ups2000_var[i].datatype) {
case REG_FLOAT:
val = reg[page][idx];
if (val == REG_FLOAT_INVALID)
invalid = 1;
break;
case REG_UINT16:
val = reg[page][idx];
if (val == REG_UINT16_INVALID)
invalid = 1;
break;
case REG_UINT32:
val = (uint32_t)(reg[page][idx]) << 16;
val |= (uint32_t)(reg[page][idx + 1]);
if (val == REG_UINT32_INVALID)
invalid = 1;
break;
default:
fatalx(EXIT_FAILURE, "invalid data type in register table!");
}
if (invalid) {
upslogx(LOG_ERR, "register %04d has invalid value %04x,", reg_id, val);
return 1;
}
#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic push
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_SECURITY
#pragma GCC diagnostic ignored "-Wformat-security"
#endif
dstate_setinfo(ups2000_var[i].name, ups2000_var[i].fmt,
(float) val / ups2000_var[i].scaling);
#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic pop
#endif
}
return 0;
}
/*
* A lookup table of all the status registers and the list of
* corresponding flags they represent. A register may set multiple
* status flags, represented by an array of flags_t.
*
* There are two types of flags. If the flag is a "status flag"
* for status_set(), for example, "OL" or "OB", the field
* "status_name" is used. If the flag is a "data variable" for
* dstate_setinfo(), the variable name and value is written in
* "var_name" and "var_val" fields.
*
* For each flag, if it's indicated by a specific value in a
* register, the "val" field is used. If a flag is indicated by
* a bit, the "bit" field should be used. Fields "val" and "bit"
* cannot be used at the same time, at least one must be "-1".
*
* Also, some important registers indicate basic system status
* (e.g. whether the UPS is on line power or battery), this info
* must always be available, and they are always expected to set
* at least one flag. If the important register does not set any
* flag, it means we've received an invalid or unknown value,
* and we must report an error. The "must_set_flag" field is used
* for this purpose.
*/
static struct {
const uint16_t reg;
bool must_set_flag;
struct flags_t {
const char *status_name;
const int16_t val;
const int bit;
const char *var_name, *var_val;
} flags[10];
} ups2000_status_reg[] =
{
{ 1024, 1, {
{ "OFF", 0, -1, NULL, NULL },
{ "BYPASS", 1, -1, NULL, NULL },
{ "OL", 2, -1, NULL, NULL },
{ "OB", 3, -1, NULL, NULL },
{ "OL ECO", 5, -1, NULL, NULL },
{ NULL, -1, -1, NULL, NULL },
}},
{ 1043, 0, {
{ "CAL", -1, 2, NULL, NULL }, /* battery self-test */
{ "LB", -1, 6, NULL, NULL },
{ NULL, -1, -1, NULL, NULL },
}},
/*
* Note: 3 = float charging, 4 = equalization charging, but
* both of them are reported as "charging", not "floating".
* The definition of "floating" in NUT is: "battery has
* completed its charge cycle, and waiting to go to resting
* mode", which is not true for UPS2000.
*/
{ 2002, 1, {
{ "", 2, -1, "battery.charger.status", "resting" },
{ "CHRG", 3, -1, "battery.charger.status", "charging" },
{ "CHRG", 4, -1, "battery.charger.status", "charging" },
{ "DISCHRG", 5, -1, "battery.charger.status", "discharging" },
{ NULL, -1, -1, NULL, NULL },
}},
{ 0, 0, { { NULL, -1, -1, NULL, NULL } } }
};
static int ups2000_update_status(void)
{
int i, j;
int r;
upsdebugx(2, "ups2000_update_status");
for (i = 0; ups2000_status_reg[i].reg != 0; i++) {
uint16_t reg, val;
struct flags_t *flag;
int flag_count = 0;
reg = ups2000_status_reg[i].reg;
r = ups2000_read_registers(modbus_ctx, reg + 10000, 1, &val);
if (r != 1)
return 1;
if (val == REG_UINT16_INVALID) {
upslogx(LOG_ERR, "register %04d has invalid value %04x,", reg, val);
return 1;
}
flag = ups2000_status_reg[i].flags;
for (j = 0; flag[j].status_name != NULL; j++) {
/*
* if the register is equal to the "val" we are looking
* for, or if register has its n-th "bit" set...
*/
if ((flag[j].val != -1 && flag[j].val == val) ||
(flag[j].bit != -1 && CHECK_BIT(val, flag[j].bit))
) {
/* if it has a corresponding status flag */
if (strlen(flag[j].status_name) != 0)
status_set(flag[j].status_name);
/* or if it has a corresponding dstate variable (or both) */
if (flag[j].var_name && flag[j].var_val)
dstate_setinfo(flag[j].var_name, "%s", flag[j].var_val);
flag_count++;
}
}
if (ups2000_status_reg[i].must_set_flag && flag_count == 0) {
upslogx(LOG_ERR, "register %04d has invalid value %04x,", reg, val);
return 1;
}
}
return 0;
}
/*
* A lookup table of all the alarm registers and the list of
* corresponding alarms they represent. Each alarm condition is
* listed by its register base address "reg" and its "bit"
* position.
*
* Each alarm condition has an "alarm_id", "alarm_cause_id",
* and "alarm_name". In addition, a few alarms conditions also
* indicates conditions related to batteries that is needed to
* be set via status_set(), those are listed in "status_name".
* Unused "status_name" is set to NULL.
*
* After an alarm is reported/cleared by the UPS, the "active"
* flag is changed to reflect its status. The error logging
* code uses this variable to issue warnings only when needed
* (i.e. only after a change, avoid issuing the same warning
* repeatedly).
*/
#define ALARM_CLEAR_AUTO 1
#define ALARM_CLEAR_MANUAL 2
#define ALARM_CLEAR_DEPENDING 3
static struct {
bool active; /* runtime: is this alarm currently active? */
const uint16_t reg; /* alarm register to check */
const int bit; /* alarm bit to check */
const int alarm_clear; /* auto or manual clear */
const int loglevel; /* warning or error */
const int alarm_id, alarm_cause_id;
const char *status_name; /* corresponding NUT status word */
const char *alarm_name; /* alarm string */
const char *alarm_desc; /* brief explanation */
} ups2000_alarm[] =
{
{
false, 40156, 3, ALARM_CLEAR_AUTO, LOG_ALERT,
30, 1, NULL, "UPS internal overtemperature",
"The ambient temperature is over 50-degree C. "
"Startup from standby mode is prohibited.",
},
{
false, 40161, 1, ALARM_CLEAR_AUTO, LOG_WARNING,
10, 1, NULL, "Abnormal bypass voltage",
"Bypass input is unavailable or out-of-range. Wait for "
"bypass input to recover, or change acceptable bypass "
"range via front panel.",
},
{
false, 40161, 2, ALARM_CLEAR_AUTO, LOG_WARNING,
10, 2, NULL, "Abnormal bypass frequency",
"Bypass input is unavailable or out-of-range. Wait for "
"bypass input to recover, or change acceptable bypass "
"range via front panel.",
},
{
false, 40163, 3, ALARM_CLEAR_DEPENDING, LOG_WARNING,
25, 1, NULL, "Battery overvoltage",
"When the UPS is started, voltage of each battery exceeds 15 V. "
"Or: current battery voltage exceeds 14.7 V.",
},
{
false, 40164, 1, ALARM_CLEAR_AUTO, LOG_WARNING,
29, 1, "RB", "Battery needs maintenance",
"During the last battery self-check, the battery voltage "
"was lower than the replacement threshold (11 V).",
},
{
false, 40164, 3, ALARM_CLEAR_AUTO, LOG_WARNING,
26, 1, NULL, "Battery undervoltage",
NULL,
},
{
false, 40170, 4, ALARM_CLEAR_AUTO, LOG_ALERT,
22, 1, NULL, "Battery disconnected",
"Battery is not connected, has loose connection, or faulty.",
},
{
false, 40173, 5, ALARM_CLEAR_AUTO, LOG_ALERT,
66, 1, "OVER", "Output overload (105%-110%)",
"UPS will shut down or transfer to bypass mode in 5-10 minutes.",
},
{
false, 40173, 3, ALARM_CLEAR_AUTO, LOG_ALERT,
66, 2, "OVER", "Output overload (110%-130%)",
"UPS will shut down or transfer to bypass mode in 30-60 seconds.",
},
{
false, 40174, 0, ALARM_CLEAR_DEPENDING, LOG_ALERT,
14, 1, NULL, "UPS startup timeout",
"The inverter output voltage is not within +/- 2 V of the "
"rated output. Or: battery is overdischarged.",
},
{
false, 40179, 14, ALARM_CLEAR_MANUAL, LOG_ALERT,
42, 15, NULL, "Rectifier fault (internal fault)",
"Bus voltage is lower than 320 V.",
},
{
false, 40179, 15, ALARM_CLEAR_MANUAL, LOG_ALERT,
42, 17, NULL, "Rectifier fault (internal fault)",
"Bus voltage is higher than 450 V.",
},
{
false, 40180, 1, ALARM_CLEAR_MANUAL, LOG_ALERT,
42, 18, NULL, "Rectifier fault (internal fault)",
"Bus voltage is lower than 260 V.",
},
{
false, 40180, 5, ALARM_CLEAR_AUTO, LOG_ALERT,
42, 24, NULL, "EEPROM fault (internal fault)",
"Faulty EEPROM. All settings are restored to "
"factory default and cannot be saved.",
},
{
false, 40180, 6, ALARM_CLEAR_MANUAL, LOG_ALERT,
42, 27, NULL, "Inverter fault (internal fault)",
"Inverter output overvoltage, undervoltage or "
"undercurrent.",
},
{
false, 40180, 7, ALARM_CLEAR_DEPENDING, LOG_ALERT,
42, 28, NULL, "Inverter fault (internal fault)",
"The inverter output voltage is lower than 100 V.",
},
{
false, 40180, 10, ALARM_CLEAR_MANUAL, LOG_ALERT,
42, 31, NULL, "Inverter fault (internal fault)",
"The difference between the absolute value of the positive bus "
"voltage and that of the negative bus voltage is 100 V.",
},
{
false, 40180, 11, ALARM_CLEAR_DEPENDING, LOG_ALERT,
42, 32, NULL, "UPS internal overtemperature",
"The ambient temperature is over 50 degree C, "
"switching to bypass mode.",
},
{
false, 40180, 13, ALARM_CLEAR_MANUAL, LOG_ALERT,
42, 36, NULL, "Charger fault (internal fault)",
"The charger has no output. Faulty internal connections.",
},
{
false, 40182, 4, ALARM_CLEAR_MANUAL, LOG_ALERT,
42, 42, NULL, "Charger fault (internal fault)",
"The charger has no output while the inverter is on, "
"battery undervoltage. Faulty switching transistor.",
},
{
false, 40182, 13, ALARM_CLEAR_MANUAL, LOG_ALERT,
66, 3, "OVER", "Output overload shutdown",
"UPS has shutdown or transferred to bypass mode.",
},
{
false, 40182, 14, ALARM_CLEAR_MANUAL, LOG_ALERT,
66, 4, "OVER", "Bypass output overload shutdown",
"UPS has shutdown, bypass output was overload and exceeded "
"time limit.",
},
{ false, 0, -1, -1, -1, -1, -1, NULL, NULL, NULL }
};
/* don't spam the syslog */
static time_t alarm_logged_since = 0;
#define UPS2000_LOG_INTERVAL 600 /* 10 minutes */
static int ups2000_update_alarm(void)
{
uint16_t val[27];
int i;
int r;
char alarm_buf[128];
size_t all_alarms_len = 0;
int alarm_count = 0;
bool alarm_logged = 0;
bool alarm_rtfm = 0;
time_t now = time(NULL);
upsdebugx(2, "ups2000_update_alarm");
/*
* All alarm registers have an offset of 1024 * ups_number.
* We only support 1 UPS, it's always 1024.
*/
r = ups2000_read_registers(modbus_ctx, ups2000_alarm[0].reg + 1024, 27, val);
if (r != 27)
return 1;
bypass_available = 1; /* register 40161 hack, see comments below */
for (i = 0; ups2000_alarm[i].alarm_id != -1; i++) {
int idx = ups2000_alarm[i].reg - ups2000_alarm[0].reg;
if (idx > 26 || idx < 0)
fatalx(EXIT_FAILURE, "register calculation overflow!");
if (CHECK_BIT(val[idx], ups2000_alarm[i].bit)) {
int gotlen;
if (ups2000_alarm[i].reg == 40161)
/*
* HACK: special treatment for register 40161. If this
* register indicates an alarm, we need to lock the
* "bypass.on" command as a software foolproof mechanism.
* It's written to the global "bypass_available" flag.
*/
bypass_available = 0;
alarm_count++;
gotlen = snprintf(alarm_buf, 128, "(ID %02d/%02d): %s!",
ups2000_alarm[i].alarm_id,
ups2000_alarm[i].alarm_cause_id,
ups2000_alarm[i].alarm_name);
if (gotlen < 0 || (uintmax_t)gotlen > SIZE_MAX) {
fatalx(EXIT_FAILURE, "alarm_buf preparation over/under-flow!");
}
all_alarms_len += (size_t)gotlen;
alarm_set(alarm_buf);
if (ups2000_alarm[i].status_name)
status_set(ups2000_alarm[i].status_name);
/*
* Log the warning only if it's a new alarm, or if a long time
* has paseed since we first warned it.
*/
if (!ups2000_alarm[i].active ||
difftime(now, alarm_logged_since) >= UPS2000_LOG_INTERVAL
) {