Add UPS functionality
This commit is contained in:
parent
af97e393f6
commit
b672c9f5ae
13
bmspy-ups.service
Normal file
13
bmspy-ups.service
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=UPS monitoring for BMS
|
||||||
|
Requires=bmspy-server.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=exec
|
||||||
|
WorkingDirectory=/usr/local/bmspy
|
||||||
|
ExecStart=poetry run bmspy-ups
|
||||||
|
RestartSec=5
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
194
bmspy/ups.py
Normal file
194
bmspy/ups.py
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
from collections import deque
|
||||||
|
import argparse
|
||||||
|
import atexit, datetime, os, re, sys, time
|
||||||
|
import smtplib, ssl, socket
|
||||||
|
from bmspy import client
|
||||||
|
|
||||||
|
DAEMON_UPDATE_PERIOD = 30
|
||||||
|
|
||||||
|
scheduled_shutdown = False
|
||||||
|
critical_sent = False
|
||||||
|
warning_sent = False
|
||||||
|
alert_sent = False
|
||||||
|
|
||||||
|
def handle_shutdown(action = 'cancel', delay = 0, debug = 0):
|
||||||
|
global scheduled_shutdown
|
||||||
|
|
||||||
|
if action == 'shutdown':
|
||||||
|
if scheduled_shutdown is False:
|
||||||
|
scheduled_shutdown = time.time() + delay * 60 * 1000
|
||||||
|
os.system("/sbin/shutdown {}".format(delay))
|
||||||
|
|
||||||
|
elif action == 'cancel':
|
||||||
|
os.system("/sbin/shutdown -c")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def handle_email(text, level, recipient = "root", mailserver = "localhost", port = 25, mailuser = None, mailpass = None, debug = 0):
|
||||||
|
isSSL = False
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
|
||||||
|
if re.match("/@/", recipient) is None:
|
||||||
|
recipient = "{}@{}".format(recipient, hostname)
|
||||||
|
sender = "bmspy on {} <nobody@{}>".format(hostname, hostname)
|
||||||
|
|
||||||
|
if port == 465 or port == 587:
|
||||||
|
isSSL = True
|
||||||
|
|
||||||
|
if level is not None:
|
||||||
|
msg = "From: {}\r\nTo: {}\r\nSubject: {} from BMSPY UPS on {}\r\n\r\n{}\r\n".format(sender, recipient, level, hostname, text)
|
||||||
|
|
||||||
|
if isSSL:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
|
||||||
|
with smtplib.SMTP(mailserver, port) as server:
|
||||||
|
if isSSL:
|
||||||
|
server.starttls(context=context)
|
||||||
|
server.ehlo()
|
||||||
|
if mailuser is not None and mailpass is not None:
|
||||||
|
server.login(mailuser, mailpass)
|
||||||
|
server.sendmail(sender, recipient, msg)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global alert_sent, warning_sent, critical_sent
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Query JBD BMS and alert or shutdown when certain thresholds are reached',
|
||||||
|
add_help=True,
|
||||||
|
)
|
||||||
|
parser.add_argument('--alert', '-a', dest='alert', action='store_true',
|
||||||
|
default=True, help='Email an alert when UPS detects A/C loss (default: true)')
|
||||||
|
parser.add_argument('--warning', '-w', dest='warning_threshold', action='store', type=int,
|
||||||
|
default=75, help='Email an alert when remaining capacity percentage drops below this figure (default: 75)')
|
||||||
|
parser.add_argument('--critical', '-c', dest='critical_threshold', action='store', type=int,
|
||||||
|
default=30, help='Shut system down when remaining capacity percentage drops below this figure (default: 30)')
|
||||||
|
parser.add_argument('--delay', '-d', dest='shutdown_delay', action='store', type=int,
|
||||||
|
default=5, help='Delay system shutdown (default: 5 minutes)')
|
||||||
|
parser.add_argument('--mailserver', '-m', dest='mailserver', action='store',
|
||||||
|
default="localhost", help='Mail server (default: localhost)')
|
||||||
|
parser.add_argument('--port', '-p', dest='port', action='store',
|
||||||
|
default=25, help='Mail server port (default: 25)')
|
||||||
|
parser.add_argument('--user', dest='mailuser', action='store',
|
||||||
|
default=None, help='Mail server user')
|
||||||
|
parser.add_argument('--pass', dest='mailpass', action='store',
|
||||||
|
default=None, help='Mail server password')
|
||||||
|
parser.add_argument('--to', '-t', dest='recipient', action='store',
|
||||||
|
default="root", help='Email recipient (default: root)')
|
||||||
|
parser.add_argument('--socket', '-s', dest='socket', action='store',
|
||||||
|
default='/run/bmspy/bms', help='Socket to communicate with daemon')
|
||||||
|
parser.add_argument('--verbose', '-v', action='count',
|
||||||
|
default=0, help='Print more verbose information (can be specified multiple times)')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
debug = args.verbose
|
||||||
|
|
||||||
|
print("Running BMS UPS daemon on socket {}".format(args.socket))
|
||||||
|
|
||||||
|
client.handle_registration(args.socket, 'ups', debug)
|
||||||
|
atexit.register(client.handle_registration, args.socket, 'ups', debug)
|
||||||
|
|
||||||
|
history = deque()
|
||||||
|
while True:
|
||||||
|
data = client.read_data(args.socket, 'ups')
|
||||||
|
history.append(data)
|
||||||
|
|
||||||
|
# Remove the oldest data from the history
|
||||||
|
while len(history) > 10:
|
||||||
|
history.popleft()
|
||||||
|
|
||||||
|
if len(history) > 3:
|
||||||
|
comparison_1 = history[1]
|
||||||
|
comparison_2 = history[2]
|
||||||
|
comparison_3 = history[3]
|
||||||
|
else:
|
||||||
|
if debug > 1:
|
||||||
|
print("Not enough readings, sleeping...")
|
||||||
|
time.sleep(DAEMON_UPDATE_PERIOD)
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_amps = float(data['bms_current_amps']['raw_value'])
|
||||||
|
charge_ratio = float(data['bms_capacity_charge_ratio']['raw_value']) * 100
|
||||||
|
comparison_1_current_amps = float(comparison_1['bms_current_amps']['raw_value'])
|
||||||
|
comparison_1_charge_ratio = float(comparison_1['bms_capacity_charge_ratio']['raw_value']) * 100
|
||||||
|
comparison_2_current_amps = float(comparison_2['bms_current_amps']['raw_value'])
|
||||||
|
comparison_2_charge_ratio = float(comparison_2['bms_capacity_charge_ratio']['raw_value']) * 100
|
||||||
|
comparison_3_current_amps = float(comparison_3['bms_current_amps']['raw_value'])
|
||||||
|
comparison_3_charge_ratio = float(comparison_3['bms_capacity_charge_ratio']['raw_value']) * 100
|
||||||
|
|
||||||
|
if debug > 1:
|
||||||
|
print("current: {:>3.2f}A\ncapacity remaining: {:>4.0f}%".format(current_amps, charge_ratio))
|
||||||
|
|
||||||
|
if charge_ratio <= args.critical_threshold and \
|
||||||
|
comparison_1_charge_ratio <= args.critical_threshold:
|
||||||
|
if debug > 0:
|
||||||
|
print("Below critical threshold, shutting down")
|
||||||
|
handle_shutdown(action = 'shutdown', delay = args.shutdown_delay, debug = debug)
|
||||||
|
if critical_sent is False:
|
||||||
|
handle_email(text = "remaining capacity below {}%, shutting down".format(args.critical_threshold),
|
||||||
|
level = "Critical alert",
|
||||||
|
recipient = args.recipient,
|
||||||
|
mailserver = args.mailserver,
|
||||||
|
port = args.port,
|
||||||
|
mailuser = args.mailuser,
|
||||||
|
mailpass = args.mailpass,
|
||||||
|
debug = debug)
|
||||||
|
critical_sent = True
|
||||||
|
|
||||||
|
elif charge_ratio <= args.warning_threshold and \
|
||||||
|
comparison_1_charge_ratio <= args.warning_threshold:
|
||||||
|
if debug > 0:
|
||||||
|
print("Below warning threshold")
|
||||||
|
if warning_sent is False:
|
||||||
|
handle_email(text = "remaining capacity below {}%".format(args.warning_threshold),
|
||||||
|
level = "Warning",
|
||||||
|
recipient = args.recipient,
|
||||||
|
mailserver = args.mailserver,
|
||||||
|
port = args.port,
|
||||||
|
mailuser = args.mailuser,
|
||||||
|
mailpass = args.mailpass,
|
||||||
|
debug = debug)
|
||||||
|
warning_sent = True
|
||||||
|
|
||||||
|
# Current needs to be negative for two consecutive reads
|
||||||
|
elif args.alert and current_amps < 0 and \
|
||||||
|
comparison_1_current_amps < 0 and \
|
||||||
|
comparison_2_current_amps >= 0:
|
||||||
|
if debug > 0:
|
||||||
|
print("Alert: discharging!")
|
||||||
|
if alert_sent is False:
|
||||||
|
handle_email(text = "power lost", level = "Power loss alert",
|
||||||
|
recipient = args.recipient,
|
||||||
|
mailserver = args.mailserver,
|
||||||
|
port = args.port,
|
||||||
|
mailuser = args.mailuser,
|
||||||
|
mailpass = args.mailpass,
|
||||||
|
debug = debug)
|
||||||
|
alert_sent = True
|
||||||
|
|
||||||
|
# Current needs to be zero or positive for two consecutive reads
|
||||||
|
elif args.alert and current_amps >= 0 and \
|
||||||
|
comparison_1_current_amps >= 0 and \
|
||||||
|
comparison_2_current_amps < 0:
|
||||||
|
if debug > 0:
|
||||||
|
print("Alert: power regained!")
|
||||||
|
handle_shutdown(action = 'cancel', debug = debug)
|
||||||
|
handle_email(text = "power regained", level = "Recovery alert",
|
||||||
|
recipient = args.recipient,
|
||||||
|
mailserver = args.mailserver,
|
||||||
|
port = args.port,
|
||||||
|
mailuser = args.mailuser,
|
||||||
|
mailpass = args.mailpass,
|
||||||
|
debug = debug)
|
||||||
|
critical_sent = False
|
||||||
|
warning_sent = False
|
||||||
|
alert_sent = False
|
||||||
|
|
||||||
|
time.sleep(DAEMON_UPDATE_PERIOD)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -25,4 +25,5 @@ bmspy = "bmspy:main"
|
|||||||
bmspy-server = "bmspy.server:main"
|
bmspy-server = "bmspy.server:main"
|
||||||
bmspy-influxdb = "bmspy.influxdb:main"
|
bmspy-influxdb = "bmspy.influxdb:main"
|
||||||
bmspy-prometheus = "bmspy.prometheus:main"
|
bmspy-prometheus = "bmspy.prometheus:main"
|
||||||
|
bmspy-ups = "bmspy.ups:main"
|
||||||
bmspy-usbd = "bmspy.usbhid:main"
|
bmspy-usbd = "bmspy.usbhid:main"
|
||||||
|
Loading…
Reference in New Issue
Block a user