A high power UPS is quite costly. An APC Smart-UPS of 2200 VA capacity is somewhere around €2000. A less expensive alternative might be to look for a solar/off grid inverter.

 

 

The problem: smoke

My old APC RM2200XL recently went up in smoke. No big deal, it was really old and needed to be replaced anyway. The main drawbacks of this APC UPS was:

  • When it died, one row of the inverter transistors actually blew up. All of them cracked. It would have been better if it had pulled the fuse...
  • The batteries are stored inside the unit which is not optimal for ventilation. Also, if a battery get shorted and expands, you'll have to disassemble the whole unit (possibly drill up some rivets) to get it out because it gets stuck.

The rm2200 ups was however compatible with apcupsd in Debian, which made it "easy" to implement server connectivity trough the serial port.

 

Temporary solution - the low frequency inverter

I did have a spare "UPS" standing around, which was actually an off-grid solar inverter. I bought it years ago from AliExpress. It is a 3kW inverter that is currently sold by http://www.xingdunpower.com (or on AliExpress for about 800 EUR).

This UPS was sold as WIPANDA_I-P-XD-3000VA at the time I bought it.

 

 Battery voltage is 48V, and it does not contain a solar panel controller/charger. The batteries has to be on a shelf beside the inverter which is fine by me, as they can't get stuck if they swell.

 

 The one I got started to beep about overload (even though it had only one PC connected), and when investigating I realized it had the fans connected backwards. Anyway, now it's quite noisy, beeping about overload now and then. It still seem to work in UPS mode though. Even so, I would not recommend this inverter because it seems to be a bit outdated.

I wrote a small python script to communicate with it (published below, in case someone can make use of it). In case of a power outage, the servers would react on another UPS to shut down so all that this script do is to query some information. I haven't tested it extensively.

When the script is run without any switch it typically outputs something like

root@z590a:/var/www/lan/sh# ./pups.py
MODEL: SIN3000wGERMANY Z2111D0002
STATUS: ONLINE [0x00, 0x94, 0x93, 0x64, 0x5e, 0x3a, 0x32, 0x0d]
LINEV  : 231.2
OUTPUTV: 231.1
BCHARGE: 100
LOADPCT: 94
BATVOLT: 54.9
LINEFREQ: 50

The firmware ID is shown as "SIN3000wGERMANY" for some reason. You can also see the LOADPCT is shown as 94% which it really isn't.

The Python3 code is attached below.
 

# -*- coding: utf-8 -*-
#!/usr/bin/python3
#
# Simple implementation for iPanda I-P-XD-3000VA (AC FIRST) UPS.
#

import sys
import serial
import time

#global ser


def print_hex_list(data):
    print ('[{}]'.format(', '.join("0x%02x"%x for x in data)),end='')


def sadd(s,t):
    if (s):   r=s + "," + t
    else:     r=t
    return r


if __name__ == '__main__':  # noqa
    import argparse

    parser = argparse.ArgumentParser(
        description='iPanda UPS dump.',
        epilog="""\
NOTE: no check is done if the device is busy.

""")

    parser.add_argument(
        "--port",
        help="serial port name",
        default='/dev/ttyUSB2')

    parser.add_argument(
        "--bps",
        type=int,
        nargs='?',
        help='set baud rate, default: %(default)s',
        default=2400)

    parser.add_argument(
        '-q', '--quiet',
        action='store_true',
        help='suppress non error messages',
        default=False)

    parser.add_argument(
        '-c', '--compact',
        action='store_true',
        help='return Model;Status;Battery Chg;Utility;UPS Load;-;Batt.Run Time',
        default=False)

    parser.add_argument(
        '--csv',
        type=float,
        help='return lines with STATUS;LINEV;OUTPUTV;BCHARGE;LOADPCT;BATFACT;LINEFREQ, --csv 1 sets interval to 1s',
        default=0)

    parser.add_argument(
        '--debug',
        action='store_true',
        help='Development mode, prints Python internals on errors',
        default=False)

### Commands

    parser.add_argument(
        '--test',
        action='store_true',
        help='(T) Perform UPS self test for one minute. Send again to cancel',
        default=False)

    parser.add_argument(
        '--unplug',
        action='store_true',
        help='(u) Simulate mains failure, remove from grid. Send again to cancel',
        default=False)

    parser.add_argument(
        '--alarm',
        action='store_true',
        help='(A) Sound buzzer',
        default=False)

    parser.add_argument(
        '--shutdown',
        action='store_true',
        help='(k) Shut down after 2s',
        default=False)

    parser.add_argument(
        '--restart',
        action='store_true',
        help='(o) Restart UPS',
        default=False)

    parser.add_argument(
        '--batlow',
        action='store_true',
        help='(m) battery cut-off low voltage shutdown',
        default=False)

    parser.add_argument(
        '--calibrate',
        action='store_true',
        help='(d) battery calibration. Note: when low battery time, correction is automatically cancelled. Repeat the command cancel calibration',
        default=False)
###


    group = parser.add_argument_group('serial port')

    group.add_argument(
        "--bytesize",
        choices=[5, 6, 7, 8],
        type=int,
        help="set bytesize, one of {5 6 7 8}, default: 8",
        default=8)

    group.add_argument(
        "--parity",
        choices=['N', 'E', 'O', 'S', 'M'],
        type=lambda c: c.upper(),
        help="set parity, one of {N E O S M}, default: N",
        default='N')

    group.add_argument(
        "--stopbits",
        choices=[1, 1.5, 2],
        type=float,
        help="set stopbits, one of {1 1.5 2}, default: 1",
        default=1)

    group.add_argument(
        '--rtscts',
        action='store_true',
        help='enable RTS/CTS flow control (default off)',
        default=False)

    group.add_argument(
        '--xonxoff',
        action='store_true',
        help='enable software flow control (default off)',
        default=False)

    group.add_argument(
        '--rts',
        type=int,
        help='set initial RTS line state (possible values: 0, 1)',
        default=None)

    group.add_argument(
        '--dtr',
        type=int,
        help='set initial DTR line state (possible values: 0, 1)',
        default=None)

    args = parser.parse_args()

    # connect to serial port
    ser = serial.serial_for_url(args.port, do_not_open=True)
    ser.baudrate = args.bps
    ser.bytesize = args.bytesize
    ser.parity = args.parity
    ser.stopbits = args.stopbits
    ser.rtscts = args.rtscts
    ser.xonxoff = args.xonxoff
    ser.timeout = 1

    if args.rts is not None:
        ser.rts = args.rts

    if args.dtr is not None:
        ser.dtr = args.dtr

    if args.debug:
        sys.stderr.write(
            '--- iPanda UPS on {p.name}  {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'
            ''.format(p=ser))

    try:
        ser.open()
    except serial.SerialException as e:
        sys.stderr.write('Could not open serial port {}: {}\n'.format(ser.name, e))
        sys.exit(1)

    if (args.csv==0):
          # I Read mfg id
          # SIN3000wGERMANY Z2111D0002
          # :I-P-XD-3000VA (AC FIRST)
          # DC 48V TO AC 230V 50HZ
          # 2000W continuous power inverter
          # 4000W watt peak power
          #
          ser.write(b"I")
          r=ser.read_until('\n',26)
          sModel=r.decode("utf-8")

    if args.test:
      print("Starting Self test")
      ser.write(b"T")
    else:
      if args.unplug:
        print("Unplugging")
        ser.write(b"U")
      else:
        if args.alarm:
          print("Alarm")
          ser.write(b"A")
        else:
          if args.shutdown:
            print("Shutdown in 2s")
            ser.write(b"K") # not tested
          else:
            if args.restart:
              print("Restarting")
              ser.write(b"O")
            else:
              if args.batlow:
                print("Starting bat cut-off shutdown")
                ser.write(b"M") # not tested
              else:
                if args.calibrate:
                  print("Starting calibration")
                  ser.write(b"D")
    bvf=0.946
    lvf=0.64 #0.65
    ovf=0.636 #0.65
    timestamp=0
    try:
        while True:
             # Y Get status
             # First character definition  b7b6b5b4b3b2b1b0
             #       b6¡ªBattery low
             #     b5-- Buzzer cut
             #     b4-- Short-circuit fault
             #     b3¡ªUPS Self-test
             #     b2-- High temperature
             #     b1¡ªUPS Shutdown
             #     b0¡ªoverload
             # Second; Input voltage   Calculate factor :0.65 (Or self-adjusting)
             # Third; Output voltage   Calculate factor :0.65 (Or self-adjusting)
             # Fourth; Percentage of battery capacity
             # Fifth; Percentage of load
             # Sixth; Battery voltage Calculation factors are adjusted according to different models
             # Seventh; Frequency
             # Eighth; HEX(0D)

             ser.write(b"Y")                    # SIN3000wGERMANY Z2111D0002
             r=ser.read_until('\x0D',8)

             s=""
             if (r[7] == 0x0d):
                  if (r[0]==0):
                    s="ONLINE"
                  else:
                    if (r[0] & 0x80 != 0): s = sadd(s,"INVERTER_MODE")
                    if (r[0] & 0x40 != 0): s = sadd(s,"BATLOW")
                    if (r[0] & 0x20 != 0): s = sadd(s,"BUZZER")
                    if (r[0] & 0x10 != 0): s = sadd(s,"SHORT_FAULT")
                    if (r[0] & 0x8  != 0): s = sadd(s,"SELF_TEST")
                    if (r[0] & 0x4  != 0): s = sadd(s,"HIGH_TEMP")
                    if (r[0] & 0x2  != 0): s = sadd(s,"SHUTDOWN")
                    if (r[0] & 0x1  != 0): s = sadd(s,"OVERLOAD")

                  if args.compact:
                        print("I-P-XD-3000VA;" + sModel + ";" + s + ";" + str(r[3]) + " %;" + '{0:.1f}'.format(r[2]/ovf) + " VAC;" + str(r[4]) + " %;-;" + '{0:.1f}'.format(r[5]*bvf) + " V" )
                  else:
                        if (args.csv>0):
         #                      STATUS;LINEV;OUTPUTV;BCHARGE;LOADPCT;BATFACT;LINEFREQ
                              print('{0:.2f}'.format(timestamp) + ";" + s + ";" + '{0:.1f}'.format(r[1]/lvf)  + ";" + '{0:.1f}'.format(r[2]/ovf) + ";"  + str(r[3]) + ";"  + str(r[4]) + ";" + '{0:.1f}'.format(r[5]*bvf) + ";" + str(r[6]) )
                        else:
                              print("MODEL: "+ sModel)
                              print("STATUS: " + s + " ",end='')
                              print_hex_list(r)
                              print()

                              print("LINEV  : " + '{0:.1f}'.format(r[1]/lvf))
                              print("OUTPUTV: " + '{0:.1f}'.format(r[2]/ovf))
                              print("BCHARGE: " +str (r[3]))
                              print("LOADPCT: " +str (r[4]))
                              print("BATVOLT: " +'{0:.1f}'.format(r[5]*bvf))
                              print("LINEFREQ: " +str (r[6]))
             else:
                  sys.stderr.write('Error reading status string\n')
                  print_hex_list(r)

             if (args.csv==0): break
             if (args.csv>0):
                try:
                   time.sleep(args.csv)
                   timestamp += args.csv
                except KeyboardInterrupt:
                   break

    except KeyboardInterrupt:
       pass

    if args.debug:
        sys.stderr.write('\n--- exit ---\n')
#    serial_worker.stop()
    ser.close()

Note: if this script is not executed by root, you will have to add the user to the "dialout" group to be able to access /dev/ttyUSB2.

 

Next attempt - a hybrid solar inverter as offline/standby UPS

Due to the annoying overload alarm I bought a new High frequency inverter from AliExpress; a EAsun ISolar-SMH-II-7KW.

Cost was about 400 EUR, while the cost of a SMX2200HV (modern APC replacement for the RM2200XL) is like 2700 EUR...

With this solution I'll have a 6kVA UPS for a fraction of the APC price.

One difference between this and the previously described iPanda solar inverter is that the EAsun contains a MPPT solar panel charger. We do have solar panels, but they are connected to a SMA grid inverter (selling the unused power back to the grid). In case we would have to go "off grid" however, I'd be able to connect the panels to this new inverter, and hopefully run most things from it.

The first observations when unpacking the EAsun inverter was that the manual is quite comprehensive and well made. The Serial port protocol is not specified however, and it is needed to have the computer(s) shut down properly in the event of battery depletion.

The cable delivered with the wifi option (DB9F) can be directly connected to a USB-serial converter. I found this protocol documentation on the Internet. At least some commands work, but I have not tried everything yet.

As compared to the previous iPanda, this inverter is:

  • Smaller weight (high frequency operation require less heavy transformer)
  • More powerful (can drive higher loads)
  • Better graphical display
  • Doesn't freakin beep about overload all the time
  • Built- in solar panel MPPT charger
  • Serial port (to attach optional wifi dongle or server)
  • Wall mount (maybe not really a benifit but could be)
  • Better manual and menu system

... to mention some things.

 

 

EAsun ISolar SMH II 7kW Software for UPS usage

I wrote a simple Python program ups-easun.py which works as a server or command line utility.

The current state of the program can be downloaded from here. Note that it's still under development. Use at your own risk. I do not accept any responsibility whatsoever for you using it, but I welcome comments/suggestions as I am no skilled Python developer.

When started with "read" and "verbose" options, it gives the following output:

root@z590a:/var/www/lan/sh# python3 -u ups-easun.py --port /dev/ttyUSB0 --read --verbose
STATUS : BYPASS
LINEV : 231.4
LINEFREQ : 50.0
OUTPUTV : 231.4
OUTFREQ : 50.0
OUTVA : 0556 VA
OUTPWR : 0543 W
LOADPCT : 008
BUSVOLT : 427
BATVOLT : 54.00
CHRGCURR : 000 A
BCAPACITY : 100
HEATSKTEMP : 0020
PVINCURR : 0000
PVINVOLT : 000.0
BATVOLTSCC : 00.00
BDISCHGCURR: 00000

 The normal use of the program is given in the script ups_server.sh which starts it with options to act as a tcp server and monitoring program, making it possible to query the state as in (when listening on port 8081)

j2admin@z590a:~$ echo "q" | nc localhost 8081
20231121190221 BYPASS 229.6 50.0 229.6 50.0 0574 0555 008 427 54.00 001 100 0020 0000 000.0 00.00 00000 01010101 00 00 00001 110

The program can also store csv logs of battery voltage (when entering INVERTER_MODE) to a specified location and send emails, so that the battery status can be evaluated. It can also run shutdown/park/unpark scripts at specified voltage levels.

 

Installation

 Installation was pretty much plug and play. You'll have to make the cables for the (utility) grid connection as well as the load yourself - the inverter is shipped with screw connections for P-N-PG (in) and P-N (out), as well as a separate PG screw. I connected a simple power strip on the output, and connected the servers to it.

To avoid fire hazard the inverter is recommended to be hanged on a concrete surface, so I hanged it on a concrete fiber plate on the wall of the server room. Be careful with the top vent holes of the inverter so you don't get dust or cable clippings in the inverter.

I use the inverter with the default parameters set, but you can change a lot (battery type, charge current, cut-off voltages etc) from the front panel. The front panel user interface is easy and described well in the instructions.


 

Output in Bypass mode

In bypass mode the output simply reflects the input. In my case, P and N output looks like below, and the math (P-N) is simply 230VAC RMS.

Below, CH1 = P (Phase) and CH2=N (Neutral). Reference is PG (Protective ground).

 

 

Output in Inverter mode

In inverter mode, the inverter is generating a 115VAC sine wave on P and an oppositly signed on N. The difference (shown in math) is 230VAC, and this is what your equipment "see". There is no real "Neutral" anymore - rather two phases. This is also how army generators works, and the reason you can use 115VAC equipment in field army hospitals by connecting them between one phase and protective ground.

 

The math wave is very clean and looks even better than the one in bypass mode, i.e the inverter is "cleaning up" the grid power!

This working principle is not mentioned in the manual so a word of warning:

NEVER connect N on the primary side to N of the inverter output - they are NOT the same!

 

This also means that you should be careful connecting this inverter to a central in an old house, where N and PG might be connected. In old houses, the N and PG rail are often connected in the central. N and PG should be connected at the feeding point (with 4 wire systems) but I have seen them connected in the first (or even sub) central as well.

 

 

Transition BYPASS to INVERTER_MODE

The time to move from BYPASS to INVERTER_MODE might affect some old (transformer) equipment, but not likely computers with switch mode power supplies.

The picture below shows how the output reacts when the input drops.

 

CH1= Output (N), CH2=Output (P), CH3=Input (P).

Math (red) is the inverter output signal, should be 230VAC / 50Hz.

When CH3 (input Phase) disappear (at first cursor), there is a dead time of about 20ms (one period) before a decent sine is produced at the output.

The inverter has two selectable modes: "Appliance" and "UPS". When I did this measurement the inverter was set up in Appliance mode, not UPS mode. Changing mode to UPS might even shorten the switch-over time as the inverter will detect the voltage drop earlier. But I have not tested. Appliance mode works for me.

My loads are switch mode power supplies (computer SMPS). SMPS rectifies the mains at the input, and the rectified voltage is fed to a large capacitor which will keep the charge for a short time. So a 20ms glitch should be ok.

 

My (old) batteries

I removed the battery packs from the RM2200XL and used them with the EAsun inverter. The battery packs are made up of four 12V 18Ah FG21805, fairly old.

I made a small logging program for the EAsun, logging timestamp, state, voltage in, freq in, voltage out, load (VA), load (W), load (%), battery voltage, battery charge (A), battery %, heatsink temp (C).

I logged for a while and pulled the plug for 1000s:

 

0.62

ONLINE

227.3

49.9

227.3

499

481

7

54

0

100

22

2.22

ONLINE

226.9

49.9

226.9

453

440

7

54

0

100

22

3.77

ONLINE

226.9

49.9

226.9

460

460

7

54.1

0

100

22

5.37

ONLINE

226.8

49.9

226.8

476

468

7

54

0

100

22

6.97

ONLINE

226.8

49.9

226.8

476

467

7

54

0

100

22

8.6

INVERTER_MODE

0

0

230.2

484

484

8

51.8

0

96

22

10.2

INVERTER_MODE

0

0

230.5

482

472

7

51.5

0

93

22

11.8

INVERTER_MODE

0

0

229.9

438

434

7

51.2

0

90

22

13.36

INVERTER_MODE

0

0

229.7

505

486

8

50.8

0

86

22

14.96

INVERTER_MODE

0

0

229.7

482

479

8

50.6

0

84

22

16.56

INVERTER_MODE

0

0

230.4

460

429

7

50.4

0

82

22

...and so on

 
The indicated column is the battery voltage, and as can be seen it drops rapidly when we enter INVERTER_MODE. I imported the csv data into Excel and fit a curve to it.

The model became something like v=27.24/(t+8)-0.00155*t+48.78 where t=time in seconds.

It fits roughly, and since the 1/x part can be disregarded after a while I can calculate the time t when v hits the 46V limit. The reason I use 46V as a limit is that this is the threshold where the inverter will switch back to the grid, if set to use "battery first", to not damage the batteries by deep discharge. The "DC cutoff voltage" to prevent permanent damage is even lower, 42V.

The result was disappointing. I am left with about 30 minutes run-time on batteries. If I let it run until the 42V limit I get like 73 minutes, but then the batteries will be damaged.

Damaged? They already seem to be in pretty bad shape! Possibly they have been deep discharged already at some time.

 

At 1000s I restored the grid power and the EAsun starts to charge the batteries (peak charge current was 13A).

The charging doesn't start immediately when power returns; it waits about 20s before it starts and peaks at about 13A:

 

Added newer batteries

After adding another (newer) battery pack from 2017 I couldn't actually see the slope of the voltage on the discharge curve (red):