forked from networkupstools/nut
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhuawei-ups2000h.c
More file actions
2169 lines (1877 loc) · 69.3 KB
/
Copy pathhuawei-ups2000h.c
File metadata and controls
2169 lines (1877 loc) · 69.3 KB
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-ups2000h.c - Driver for Huawei UPS2000-H (6kVA-10kVA)
*
* The document describing the protocol implemented by this driver
* is not open to public, you'll need an account with registered product
* to view it at:
*
* https://support.huawei.com/enterprise/en/doc/EDOC1100376943 (English)
* https://support.huawei.com/enterprise/zh/doc/EDOC1100216924 (Simplified Chinese)
*
* Huawei UPS2000 driver implemented by
* Copyright (C) 2020, 2021 Yifeng Li <tomli@tomli.me>
* The author is not affiliated with Huawei or other manufacturers.
*
* Huawei UPS2000-H driver implemented by
* Copyright (C) 2025 Nya Candy <dev@candinya.com>
* The author is not affiliated with Huawei or other manufacturers.
*
* This model is NOT compatible with the existing UPS2000 driver,
* so we need to fork it with modification to add additional support.
*
* 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-H (6kVA-10kVA) RS-232 Modbus driver"
#define DRIVER_VERSION "0.01"
#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-H series.
*/
static const char *supported_model[] = {
"UPS2000",
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 3
#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_MANUFACTURER = 0,
UPS2000_DESC_MODEL,
UPS2000_DESC_REVISION,
};
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,
"Nya Candy <dev@candinya.com>\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, B115200);
ser_set_rts(upsfd, 0);
ser_set_dtr(upsfd, 0);
modbus_ctx = modbus_new_rtu(device_path, 115200, '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 */
0x01, /* ReadDevID: extended identification */
0x00, /* Object ID: device list */
0x70, 0x77 /* CRC-16 */
};
/*
* Response header:
* 0x01, 0x2B, 0x0E, 0x01, 0x01, 0x00, 0x00, 0x03
*
* Response fields:
* header: 0x00, 0x06 // type (厂商名称), length
* data: ASCII string
* (e.g. HUAWEI)
*
* header: 0x01, 0x07 // type (产品代码), length
* data: ASCII string
* (e.g. UPS2000)
*
* header: 0x02, 0x13 // type (主要修订本), length
* data: ASCII string
* (e.g. UPS2000 V100R021C10)
*
* CRC-16:
* 0xA9, 0x9A
*/
static const uint8_t expected_header[IDENT_RESPONSE_HEADER_LEN] = {
MODBUS_SLAVE_ID,
0x2B, 0x0E, 0x01, 0x01, 0x00, 0x00, 0x03,
};
bool serial_fail = 0; /* unable to read from serial */
uint16_t crc16_recv, crc16_calc; /* resp CRC */
bool crc16_fail = 0; /* resp CRC failure */
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++) {
ups2000_ident_desc = strdup((char *) ups2000_ident[i].val);
switch (ups2000_ident[i].type) {
case UPS2000_DESC_MANUFACTURER:
case UPS2000_DESC_MODEL:
case UPS2000_DESC_REVISION:
break;
default:
// Unknown property
continue;
}
memcpy(ups2000_desc[ups2000_ident[i].type], ups2000_ident_desc, strlen(ups2000_ident_desc) + 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_MANUFACTURER; i <= UPS2000_DESC_REVISION; 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", "%s",
ups2000_desc[UPS2000_DESC_MANUFACTURER]);
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", "%s",
ups2000_desc[UPS2000_DESC_MANUFACTURER]);
dstate_setinfo("ups.model", "%s",
ups2000_desc[UPS2000_DESC_MODEL]);
dstate_setinfo("ups.firmware", "%s",
ups2000_desc[UPS2000_DESC_REVISION]);
// 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");
dstate_setinfo("input.phases", "3");
dstate_setinfo("output.phases", "1");
dstate_setinfo("ambient.count", "2");
dstate_setinfo("ambient.1.name", "Input Cable Terminal Temperature");
dstate_setinfo("ambient.2.name", "Output Cable Terminal Temperature");
/* 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, // Unsigned
REG_INT16, // Signed
REG_UINT32, /* occupies two registers */
};
#define REG_UINT16_INVALID 0xFFFFU
#define REG_INT16_INVALID 0x7FFF
#define REG_UINT32_INVALID 0xFFFFFFFFU
/*
* 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.L1.voltage", "%03.1f", 1000, REG_UINT16, 10.0 },
{ "input.L2.voltage", "%03.1f", 1001, REG_UINT16, 10.0 },
{ "input.L3.voltage", "%03.1f", 1002, REG_UINT16, 10.0 },
{ "input.frequency", "%02.1f", 1003, REG_UINT16, 10.0 },
{ "input.bypass.L1.voltage", "%03.1f", 1004, REG_UINT16, 10.0 },
{ "input.bypass.L2.voltage", "%03.1f", 1005, REG_UINT16, 10.0 },
{ "input.bypass.L3.voltage", "%03.1f", 1006, REG_UINT16, 10.0 },
{ "input.bypass.frequency", "%02.1f", 1007, REG_UINT16, 10.0 },
{ "output.voltage", "%03.1f", 1008, REG_UINT16, 10.0 },
// { "output.L2.voltage", "%.0f", 1009, REG_UINT16, 10.0 },
// { "output.L3.voltage", "%.0f", 1010, REG_UINT16, 10.0 },
{ "output.current", "%03.1f", 1011, REG_UINT16, 10.0 },
// { "output.L2.current", "%.0f", 1012, REG_UINT16, 10.0 },
// { "output.L3.current", "%.0f", 1013, REG_UINT16, 10.0 },
{ "output.frequency", "%02.1f", 1014, REG_UINT16, 10.0 },
{ "output.realpower", "%04.1f", 1015, REG_UINT16, 0.01 }, // 10 / K
// { "output.L2.realpower", "%.0f", 1016, REG_UINT16, 0.01 }, // 10 / K
// { "output.L3.realpower", "%.0f", 1017, REG_UINT16, 0.01 }, // 10 / K
{ "output.power", "%04.1f", 1018, REG_UINT16, 0.01 }, // 10 / K
// { "output.L2.power", "%.0f", 1019, REG_UINT16, 0.01 }, // 10 / K
// { "output.L3.power", "%.0f", 1020, REG_UINT16, 0.01 }, // 10 / K
{ "ups.load", "%03.1f", 1021, REG_UINT16, 10.0 }, // %
// { "output.L2.load", "%.0f", 1022, REG_UINT16, 10.0 }, // %
// { "output.L3.load", "%.0f", 1023, REG_UINT16, 10.0 }, // %
{ "ups.temperature", "%02.1f", 1027, REG_INT16, 10.0 },
{ "output.inverter.voltage", "%03.1f", 1038, REG_UINT16, 10.0 },
// { "output.inverter.L2.voltage", "%.0f", 1039, REG_UINT16, 10.0 },
// { "output.inverter.L3.voltage", "%.0f", 1040, REG_UINT16, 10.0 },
{ "input.L1-L2.voltage", "%03.1f", 1055, REG_UINT16, 10.0 },
{ "input.L2-L3.voltage", "%03.1f", 1056, REG_UINT16, 10.0 },
{ "input.L3-L1.voltage", "%03.1f", 1057, REG_UINT16, 10.0 },
{ "output.inverter.current", "%03.1f", 1058, REG_UINT16, 10.0 },
// { "output.inverter.L2.current", "%.0f", 1059, REG_UINT16, 10.0 },
// { "output.inverter.L3.current", "%.0f", 1060, REG_UINT16, 10.0 },
{ "output.inverter.frequency", "%02.1f", 1063, REG_UINT16, 10.0 },
// { "ambient.1.temperature", "%.0f", 1064, REG_INT16, 10.0 }, // 环境温度
// { "ambient.1.humidity", "%.0f", 1065, REG_UINT16, 10.0 }, // 环境湿度
// { "旁路运行时间", "%.0f", 1066, REG_UINT32, 3600.0 }, // Hour
// { "逆变运行时间", "%.0f", 1068, REG_UINT32, 3600.0 }, // Hour
// { "风扇寿命", "%.0f", 1047, REG_UINT32, 10.0 }, // Year
// { "母线电容寿命", "%.0f", 1048, REG_UINT32, 10.0 }, // Year
{ "ambient.1.temperature", "%02.1f", 1347, REG_INT16, 10.0 }, // 输入线缆端子温度
{ "device.uptime", "%.0f", 1348, REG_UINT32, 1.0 },
{ "ambient.2.temperature", "%02.1f", 1350, REG_INT16, 10.0 }, // 输出线缆端子温度
{ "input.L1.current", "%03.1f", 1360, REG_UINT16, 10.0 },
{ "input.L2.current", "%03.1f", 1361, REG_UINT16, 10.0 },
{ "input.L3.current", "%03.1f", 1362, REG_UINT16, 10.0 },
{ "battery.voltage", "%03.1f", 2000, REG_UINT16, 10.0 },
{ "battery.current", "%03.1f", 2001, REG_INT16, 10.0 },
{ "battery.charge", "%03.1f", 2003, REG_UINT16, 1.0 },
{ "battery.runtime", "%.0f", 2004, REG_UINT32, 1.0 },
// { "battery.temperature", "%.0f", 2006, REG_INT16, 10.0 },
{ "battery.packs", "%.0f", 2007, REG_UINT16, 1.0 },
// { "正组电池电压", "%.0f", 2024, REG_UINT16, 10.0 },
// { "负组电池电压", "%.0f", 2025, REG_UINT16, 10.0 },
// { "正组电池电流", "%.0f", 2026, REG_INT16, 10.0 },
// { "负组电池电流", "%.0f", 2027, REG_INT16, 10.0 },
{ "battery.capacity", "%.0f", 2033, REG_UINT16, 1.0 },
// { "电池运行时间", "%.0f", 2088, REG_UINT32, 3600.0 }, // Hour
// { "电池SOH", "%.0f", 2098, REG_UINT16, 1.0 }, // %
// { "battery.temperature.cell.max", "%.0f", 2101, REG_INT16, 10.0 },
// { "battery.temperature.cell.min", "%.0f", 2102, REG_INT16, 10.0 },
// { "锂电在位状态", "%.0f", 2103, REG_UINT32, 1.0 }, // %
// { "battery.voltage.cell.max", "%.0f", 2105, REG_UINT16, 10.0 }, // 注1:x1363 == 0或者1时,增益为10,x1363 == 2时,增益为1000
// { "battery.voltage.cell.min", "%.0f", 2106, REG_UINT16, 10.0 }, // 注1:x1363 == 0或者1时,增益为10,x1363 == 2时,增益为1000
// { "A相并机输出有功功率", "%.0f", 4000, REG_UINT16, 10.0 },
// { "B相并机输出有功功率", "%.0f", 4001, REG_UINT16, 10.0 },
// { "C相并机输出有功功率", "%.0f", 4002, REG_UINT16, 10.0 },
// { "A相并机输出视在功率", "%.0f", 4003, REG_UINT16, 10.0 },
// { "B相并机输出视在功率", "%.0f", 4004, REG_UINT16, 10.0 },
// { "C相并机输出视在功率", "%.0f", 4005, REG_UINT16, 10.0 },
// { "A相并机负载率", "%.0f", 4006, REG_UINT16, 10.0 },
// { "B相并机负载率", "%.0f", 4007, REG_UINT16, 10.0 },
// { "C相并机负载率", "%.0f", 4008, REG_UINT16, 10.0 },
// { "UPS并机在线状态", "%.0f", 4014, REG_UINT16, 1.0 },
// { "活动告警流水号", "%.0f", 9000, REG_UINT32, 1.0 },
// { "历史告警流水号", "%.0f", 9002, REG_UINT32, 1.0 },
// { "设备列表变化流水号", "%.0f", 9004, REG_UINT16, 1.0 },
// { "配置信号变化流水号", "%.0f", 9005, REG_UINT16, 1.0 },
{ "ups.power.nominal", "%.0f", 9009, REG_UINT16, 0.01 }, // 10 / K
{ NULL, NULL, 0, 0, 0 },
};
static int ups2000_update_info(void)
{
int i;
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.
*/
for (i = 0; ups2000_var[i].name != NULL; i++) {
uint16_t reg[2];
uint16_t reg_id = 10000 + ups2000_var[i].reg;
uint32_t raw_val;
float val;
bool invalid = 0;
switch (ups2000_var[i].datatype) {
case REG_UINT16:
// 读取
if (ups2000_read_registers(modbus_ctx, reg_id, 1, ®[0]) != 1) {
return 1;
}
// 校验
raw_val = reg[0];
if (raw_val == REG_UINT16_INVALID) {
invalid = 1;
}
// 转换
val = (float) raw_val;
break;
case REG_INT16:
// 读取
if (ups2000_read_registers(modbus_ctx, reg_id, 1, ®[0]) != 1) {
return 1;
}
// 校验
raw_val = reg[0];
if (raw_val == REG_INT16_INVALID) {
invalid = 1;
}
// 转换
val = (float) (int16_t) raw_val;
break;
case REG_UINT32:
// 读取
if (ups2000_read_registers(modbus_ctx, reg_id, 2, ®[0]) != 2) {
return 1;
}
// 校验
raw_val = (uint32_t)(reg[0]) << 16;
raw_val |= (uint32_t)(reg[1]);
if (raw_val == REG_UINT32_INVALID){
invalid = 1;
}
// 转换
val = (float) raw_val;
break;
default:
fatalx(EXIT_FAILURE, "invalid data type in register table!");
}
if (invalid) {
upslogx(LOG_ERR, "register %04d has invalid value %04x,", reg_id, raw_val);
return 0; // 丢弃数据而不是触发错误重启
}
#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,
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.
*/
// { "供电模式", 1024, }, // 0 均不供电, 1 旁路供电, 2 主路供电, 3 电池供电, 5 主路 ECO, 6 电池 ECO, 7 联合供电
// { "输入制式", 1025, }, // 0 单相, 1 三相
// { "输出制式", 1026, }, // 0 单相, 1 三相
// { "开机状态", 1028, }, // 二进制:00 关机(可开机), 01 开机中(中间状态), 10 开机失败(可开机), 11 开机完成(可关机)
// { "工作模式", 1061, }, // 0 变频器模式, 1 ECO模式, 2 自老化模式, 3 正常模式, 4 智能在线模式
// { "选配卡类型", 1070, }, // 0 无, 1 SNMP卡, 2 MODBUS卡, 3 干接点卡
// { "SNMP卡IP地址自动分配结果", 1057, }, // 0 未分配, 1 分配中, 2 分配成功, 3 分配失败
// { "蜂鸣器状态", 1305, }, // 0 未蜂鸣, 1 蜂鸣
// { "切换提示", 1306, }, // 二进制:000 无提示, 001 关机会导致系统过载提示, 010 关机会导致系统切旁路提示, 011 关机会导致系统间断旁路提示, 100 关机会导致系统掉电提示
// { "并机开机状态", 1307, }, // 二进制:00 关机(可开机), 01 开机中(中间状态), 10 开机失败(可开机), 11 开机完成(可关机)
// { "并机切换提示", 1308, }, // 二进制:000 无提示, 001 关机会导致系统过载提示, 010 关机会导致系统切旁路提示, 011 关机会导致系统间断旁路提示, 100 关机会导致系统掉电提示
// { "并机关机状态", 1309, }, // 二进制:00 关机完成(可开机), 01 关机中(中间状态), 10 关机失败(可开机), 11 开机(可关机)
// { "机型信息", 1344, }, // 硬件版本: 0 6k, 1 10k, 2 15k~20k
// { "ESN写入状态", 1346, }, // 0 未写入, 1 已写入
// { "首次上电状态", 1354, }, // 1 首次上电, 2 非首次上电
// { "UPS系列", 1363, }, // 0 UPS2000-G, 1 UPS2000-H-Li, 2 UPS2000-H-L
// { "品牌类型", 1372, }, // 0 标准版, 1 双品牌, 2 品牌替换, 3 白牌
// { "电池状态", 2002, }, // 0 未接入, 1 非充非放, 2 休眠, 3 浮充, 4 均充, 5 放电
// { "电池是否可以浮充转均充", 2011, }, // 0 可以, 非0 不可以
// { "电池是否可以均充转浮充", 2015, }, // 0 可以, 非0 不可以
// { "电池是否可测试状态", 2019, }, // 0:浅放电、核对容量测试都可以;Bit15=1,且Bit14~8=0:仅浅放电测试可以 Bit15~1=0且Bit0=1(1):核对容量测试中 Bit8=1且其余为0(0x100):浅放电测试中 其它:不可以
// { "电池是否可结束测试", 2022, }, // 0 可以, 非0 不可以
// { "电池是否可测试状态2", 2107, }, // 0:分组核对性容量测试不允许 1:分组核对性容量测试允许 2:分组核对性容量测试中 其他:非法值(不允许)
// { "电池测试状态", 2108, }, // 0:非测试状态,不显示 1:强制均充 2:浅放电测试 3:定时浅放电测试 4:核对性容量测试 5:分组核对性容量测试
// { "UPS硬件功率等级", 9007, }, // 16 6k, 32 10k, 64 20k
// { "UPS设备连接状态", 9008, }, // 0 断连, 1 正常, 2 通讯正常,业务功能无效(不通过此信号,判断UPS是否在线)
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 },
{ "OB ECO", 6, -1, NULL, NULL },
{ "OL OB", 7, -1, 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, {
{ "", 1, -1, "battery.charger.status", "resting" }, // 非充非放
{ "", 2, -1, "battery.charger.status", "resting" }, // 休眠
{ "CHRG", 3, -1, "battery.charger.status", "floating" }, // 浮充
{ "CHRG", 4, -1, "battery.charger.status", "charging" }, // 均充
{ "DISCHRG", 5, -1, "battery.charger.status", "discharging" }, // 放电
{ NULL, -1, -1, NULL, NULL },
}},
{ 2108, 0, {
{ "CAL", 2, -1, NULL, NULL }, // 浅放电测试
{ "CAL", 3, -1, NULL, NULL }, // 定时浅放电测试
{ "CAL", 4, -1, NULL, NULL }, // 核对性容量测试
{ "CAL", 5, -1, NULL, NULL }, // 分组核对性容量测试
{ 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 voltage",
"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, "LB", "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, 40174, 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, 40156, 3, ALARM_CLEAR_DEPENDING, LOG_ALERT,
30, 1, NULL, "UPS internal overtemperature",
"The ambient temperature is over 50 degree C, "
"switching to bypass mode.",
},
{
false, 40173, 3, ALARM_CLEAR_MANUAL, LOG_ALERT,
64, 1, "OVER", "Output overload shutdown",
"UPS has shutdown or transferred to bypass mode.",
},
{
false, 40174, 2, ALARM_CLEAR_MANUAL, LOG_ALERT,
64, 2, "OVER", "Bypass output overload shutdown",
"UPS has shutdown, bypass output was overload and exceeded "
"time limit.",
},
{
false, 40182, 5, ALARM_CLEAR_MANUAL, LOG_ALERT,
85, 1, "EPO", "Emergency power off",
"UPS has been shutdown by EPO switch.",
},
{ 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,