Add UPS functionality
This commit is contained in:
		
							
								
								
									
										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" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user