Add multi-device support
server.py
- --device is now repeatable (-d ups1:/dev/ttyUSB0 -d ups2:/dev/ttyUSB1). Bare paths (/dev/ttyUSB0) auto-name from the last path component (ttyUSB0).
- Maintains {name: {ser, data, timestamp}} per UPS — each device has independent data freshness.
- GET response is now {ups_name: JBDUPS}. Accepts optional ups key in the request to return only one.
client.py
- read_data() gains ups=None parameter — pass a name to filter server-side, or omit for all.
- Always returns {ups_name: JBDUPS}.
influxdb.py
- influxdb_create_snapshot() iterates {name: JBDUPS} and tags every InfluxDB point with ups=name.
- influxdb_export() / bmspy-influxdb gain --ups to export only a specific UPS.
__init__.py
- bmspy CLI gains --ups to display only a named UPS.
- Displays each UPS under a === name === header.
This commit is contained in:
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
bmspy is a tool to get information from a [xiaoxiang-type](https://www.lithiumbatterypcb.com/product/4s-or-3s-12v-li-ion-or-lifepo4-battery-smart-bms-with-bluetooth-function-uart-and-rs485-communication-with-60a-to-120a-constant-current/?attribute_specification-selection=4S+Lifepo4+120A+with+UART+and+RS485) BMS system, using some sort of serial connection.
|
bmspy is a tool to get information from a [xiaoxiang-type](https://www.lithiumbatterypcb.com/product/4s-or-3s-12v-li-ion-or-lifepo4-battery-smart-bms-with-bluetooth-function-uart-and-rs485-communication-with-60a-to-120a-constant-current/?attribute_specification-selection=4S+Lifepo4+120A+with+UART+and+RS485) BMS system, using some sort of serial connection.
|
||||||
|
|
||||||
It can display the information as text, in JSON, or export the data continuously to a Prometheus exporter.
|
It can display the information as text, in JSON, or export the data continuously to InfluxDB or a Prometheus exporter.
|
||||||
|
|
||||||
|
Multiple BMS/UPS devices can be connected at once. Each is identified by a name, and data from all of them (or just one) can be pushed to InfluxDB or Prometheus in the same connection, with each measurement tagged with the UPS name.
|
||||||
|
|
||||||
To install:
|
To install:
|
||||||
git clone https://git.treehouse.org.za/tim/bmspy
|
git clone https://git.treehouse.org.za/tim/bmspy
|
||||||
@@ -12,17 +14,42 @@ To install:
|
|||||||
Or, to install with influxdb and/or prometheus support:
|
Or, to install with influxdb and/or prometheus support:
|
||||||
poetry install -E influxdb -E prometheus
|
poetry install -E influxdb -E prometheus
|
||||||
|
|
||||||
To run:
|
## Running the server
|
||||||
poetry run bmspyd &
|
|
||||||
|
The server daemon reads from one or more serial devices and makes the data available over a Unix socket.
|
||||||
|
|
||||||
|
Single device (defaults to `/dev/ttyUSB0`):
|
||||||
|
|
||||||
|
poetry run bmspy-server
|
||||||
|
|
||||||
|
Multiple devices, with optional names (default name is derived from the device path, e.g. `ttyUSB0`):
|
||||||
|
|
||||||
|
poetry run bmspy-server -d network:/dev/ttyUSB0 -d nas:/dev/ttyUSB1
|
||||||
|
|
||||||
|
To run via systemd, copy `bmspy-server.service` to `/etc/systemd/system`, adjust `WorkingDirectory` and the `ExecStart` line as needed, then enable and start it:
|
||||||
|
|
||||||
Or to run via systemd, copy bmspyd.service to /etc/systemd/system, adjust WorkingDirectory to point to the installation location, and enable and start the service:
|
|
||||||
cp bmspy-server.service /etc/systemd/system
|
cp bmspy-server.service /etc/systemd/system
|
||||||
$EDITOR /etc/systemd/system/bmspy-server.service
|
$EDITOR /etc/systemd/system/bmspy-server.service
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable bmspy-server
|
systemctl enable bmspy-server
|
||||||
systemctl start bmspy-server
|
systemctl start bmspy-server
|
||||||
|
|
||||||
To run a client to get the data, choose one of the following options:
|
## Running a client
|
||||||
poetry run bmspy
|
|
||||||
poetry run bmspy-influxdb --url ...
|
|
||||||
|
|
||||||
|
To print a summary of all connected UPSes:
|
||||||
|
|
||||||
|
poetry run bmspy
|
||||||
|
|
||||||
|
To show only a specific UPS:
|
||||||
|
|
||||||
|
poetry run bmspy --ups network
|
||||||
|
|
||||||
|
To push data for all UPSes to InfluxDB (each measurement is tagged `ups=<name>`):
|
||||||
|
|
||||||
|
poetry run bmspy-influxdb --url https://influx.example.com --org myorg --token mytoken
|
||||||
|
|
||||||
|
To push data for a single UPS only:
|
||||||
|
|
||||||
|
poetry run bmspy-influxdb --ups network --url ...
|
||||||
|
|
||||||
|
InfluxDB connection details can also be supplied via environment variables (`INFLUXDB_V2_URL`, `INFLUXDB_V2_ORG`, `INFLUXDB_V2_TOKEN`) instead of command-line flags.
|
||||||
|
|||||||
+21
-16
@@ -12,10 +12,10 @@ def parse_args():
|
|||||||
description='Query JBD BMS and report status',
|
description='Query JBD BMS and report status',
|
||||||
add_help=True,
|
add_help=True,
|
||||||
)
|
)
|
||||||
parser.add_argument('--device', '-d', dest='device', action='store',
|
|
||||||
default='/dev/ttyUSB0', help='USB device to read')
|
|
||||||
parser.add_argument('--socket', '-s', dest='socket', action='store',
|
parser.add_argument('--socket', '-s', dest='socket', action='store',
|
||||||
default='/run/bmspy/bms', help='Socket to communicate with daemon')
|
default='/run/bmspy/bms', help='Socket to communicate with daemon')
|
||||||
|
parser.add_argument('--ups', dest='ups', action='store', default=None,
|
||||||
|
help='Only show data for this UPS name (default: all)')
|
||||||
parser.add_argument('--json', '-j', dest='report_json', action='store_true',
|
parser.add_argument('--json', '-j', dest='report_json', action='store_true',
|
||||||
default=False, help='Report data as JSON')
|
default=False, help='Report data as JSON')
|
||||||
parser.add_argument('--prometheus', '-p', dest='report_prometheus', action='store_true',
|
parser.add_argument('--prometheus', '-p', dest='report_prometheus', action='store_true',
|
||||||
@@ -44,11 +44,11 @@ def main():
|
|||||||
try:
|
try:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
|
||||||
debug=args.verbose
|
debug = args.verbose
|
||||||
|
|
||||||
if args.report_influxdb:
|
if args.report_influxdb:
|
||||||
num_args = 0
|
num_args = 0
|
||||||
for arg in [ args.influx_url, args.influx_org, args.influx_token ]:
|
for arg in [args.influx_url, args.influx_org, args.influx_token]:
|
||||||
if arg is not False:
|
if arg is not False:
|
||||||
num_args += 1
|
num_args += 1
|
||||||
if num_args != 0 and num_args != 3:
|
if num_args != 0 and num_args != 3:
|
||||||
@@ -61,15 +61,18 @@ def main():
|
|||||||
if args.report_influxdb:
|
if args.report_influxdb:
|
||||||
from bmspy import influxdb as bms_influx
|
from bmspy import influxdb as bms_influx
|
||||||
|
|
||||||
bms_influx.influxdb_export(bucket=args.influx_bucket, \
|
bms_influx.influxdb_export(
|
||||||
url=args.influx_url, \
|
bucket=args.influx_bucket,
|
||||||
org=args.influx_org, \
|
url=args.influx_url,
|
||||||
token=args.influx_token, \
|
org=args.influx_org,
|
||||||
debug=debug, \
|
token=args.influx_token,
|
||||||
daemonize=True)
|
ups=args.ups,
|
||||||
|
debug=debug,
|
||||||
|
daemonize=True,
|
||||||
|
)
|
||||||
|
|
||||||
elif args.report_textfile:
|
elif args.report_textfile:
|
||||||
from bmspy import promethus
|
from bmspy import prometheus
|
||||||
prometheus.prometheus_export(daemonize=False, filename=args.report_textfile, debug=debug)
|
prometheus.prometheus_export(daemonize=False, filename=args.report_textfile, debug=debug)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -77,20 +80,22 @@ def main():
|
|||||||
client.handle_registration(args.socket, 'bmspy', debug)
|
client.handle_registration(args.socket, 'bmspy', debug)
|
||||||
atexit.register(client.handle_registration, args.socket, 'bmspy', debug)
|
atexit.register(client.handle_registration, args.socket, 'bmspy', debug)
|
||||||
|
|
||||||
data = client.read_data(args.socket, 'bmspy')
|
# {ups_name: JBDUPS}
|
||||||
|
data = client.read_data(args.socket, 'bmspy', ups=args.ups, debug=debug)
|
||||||
|
|
||||||
if args.report_json:
|
if args.report_json:
|
||||||
print(json.dumps(data))
|
import json
|
||||||
|
print(json.dumps({name: dict(ups.items()) for name, ups in data.items()}, default=str))
|
||||||
|
|
||||||
elif args.report_print:
|
elif args.report_print:
|
||||||
pp = pprint.PrettyPrinter(indent=4)
|
pp = pprint.PrettyPrinter(indent=4)
|
||||||
pp.pprint(data)
|
for ups_name, ups_data in data.items():
|
||||||
|
print("=== {} ===".format(ups_name))
|
||||||
|
pp.pprint(ups_data)
|
||||||
|
|
||||||
except KeyboardInterrupt as e:
|
except KeyboardInterrupt as e:
|
||||||
bms.cleanup()
|
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
+38
-22
@@ -1,30 +1,35 @@
|
|||||||
#
|
#
|
||||||
# Library with socket client for use by consumers
|
# Library with socket client for use by consumers
|
||||||
#
|
#
|
||||||
import atexit, os, sys
|
import sys
|
||||||
import struct, json
|
import struct
|
||||||
|
import json
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
|
|
||||||
is_registered = False
|
is_registered = False
|
||||||
|
|
||||||
|
|
||||||
def handle_registration(socket_path, client_name, debug=0):
|
def handle_registration(socket_path, client_name, debug=0):
|
||||||
global is_registered
|
global is_registered
|
||||||
data = dict()
|
data = dict()
|
||||||
|
|
||||||
if is_registered:
|
if is_registered:
|
||||||
message = {'command': 'DEREGISTER', 'client': client_name}
|
message = {"command": "DEREGISTER", "client": client_name}
|
||||||
else:
|
else:
|
||||||
# fork server if it's not already running
|
# fork server if it's not already running
|
||||||
message = {'command': 'REGISTER', 'client': client_name}
|
message = {"command": "REGISTER", "client": client_name}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = socket_comms(socket_path, message, debug)
|
data = socket_comms(socket_path, message, debug)
|
||||||
if data['status'] == 'REGISTERED':
|
if data["status"] == "REGISTERED":
|
||||||
is_registered = True
|
is_registered = True
|
||||||
elif data['status'] == 'DEREGISTERED':
|
elif data["status"] == "DEREGISTERED":
|
||||||
is_registered = False
|
is_registered = False
|
||||||
else:
|
else:
|
||||||
raise OSError("{} registration: invalid response: {}".format(client_name, data))
|
raise OSError(
|
||||||
|
"{} registration: invalid response: {}".format(client_name, data)
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if is_registered:
|
if is_registered:
|
||||||
@@ -43,7 +48,7 @@ def socket_comms(socket_path, request_data, debug=0):
|
|||||||
|
|
||||||
# Connect the socket to the port where the server is listening
|
# Connect the socket to the port where the server is listening
|
||||||
if debug > 2:
|
if debug > 2:
|
||||||
print('socket client: connecting to {}'.format(socket_path))
|
print("socket client: connecting to {}".format(socket_path))
|
||||||
try:
|
try:
|
||||||
sock.connect(socket_path)
|
sock.connect(socket_path)
|
||||||
except socket.error as msg:
|
except socket.error as msg:
|
||||||
@@ -54,54 +59,65 @@ def socket_comms(socket_path, request_data, debug=0):
|
|||||||
|
|
||||||
# Send request
|
# Send request
|
||||||
if debug > 2:
|
if debug > 2:
|
||||||
print('socket client: sending {!r}'.format(request_data))
|
print("socket client: sending {!r}".format(request_data))
|
||||||
request = bytes()
|
request = bytes()
|
||||||
try:
|
try:
|
||||||
request = json.dumps(request_data).encode()
|
request = json.dumps(request_data).encode()
|
||||||
# add length to the start of the json string, so we know how much to read on the other end
|
# add length to the start of the json string, so we know how much to read on the other end
|
||||||
length = struct.pack('!I', len(request))
|
length = struct.pack("!I", len(request))
|
||||||
if debug > 3:
|
if debug > 3:
|
||||||
print("socket client: outgoing request length: {}, encoded as {}".format(len(request), length))
|
print(
|
||||||
|
"socket client: outgoing request length: {}, encoded as {}".format(
|
||||||
|
len(request), length
|
||||||
|
)
|
||||||
|
)
|
||||||
request = length + request
|
request = length + request
|
||||||
if debug > 4:
|
if debug > 4:
|
||||||
print("socket client: outgoing request: {}".format(request))
|
print("socket client: outgoing request: {}".format(request))
|
||||||
except:
|
except Exception:
|
||||||
print("socket client ERROR: unable to encode request")
|
print("socket client ERROR: unable to encode request")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
sock.sendall(request)
|
sock.sendall(request)
|
||||||
|
|
||||||
# get length of expected json string
|
# get length of expected json string
|
||||||
response = sock.recv(struct.calcsize('!I'))
|
response = sock.recv(struct.calcsize("!I"))
|
||||||
try:
|
try:
|
||||||
length = struct.unpack('!I', response)[0]
|
length = struct.unpack("!I", response)[0]
|
||||||
if debug > 4:
|
if debug > 4:
|
||||||
print("socket client: incoming length: {}, encoded as {}".format(length, response))
|
print(
|
||||||
|
"socket client: incoming length: {}, encoded as {}".format(
|
||||||
|
length, response
|
||||||
|
)
|
||||||
|
)
|
||||||
# read length bytes
|
# read length bytes
|
||||||
response = sock.recv(length)
|
response = sock.recv(length)
|
||||||
if debug > 3:
|
if debug > 3:
|
||||||
print("socket client: incoming response: {}".format(response))
|
print("socket client: incoming response: {}".format(response))
|
||||||
response_data = json.loads(response)
|
response_data = json.loads(response)
|
||||||
except:
|
except Exception:
|
||||||
print("socket client ERROR: unable to decode response")
|
print("socket client ERROR: unable to decode response")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if debug > 2:
|
if debug > 2:
|
||||||
print('socket client: received {!r}'.format(response_data))
|
print("socket client: received {!r}".format(response_data))
|
||||||
|
|
||||||
sock.close()
|
sock.close()
|
||||||
|
|
||||||
return response_data
|
return response_data
|
||||||
|
|
||||||
|
|
||||||
def read_data(socket_path, client_name, debug=0):
|
def read_data(socket_path, client_name, ups=None, debug=0):
|
||||||
data = dict()
|
"""Return {ups_name: JBDUPS} for all UPSes, or just the named one."""
|
||||||
|
request = {"command": "GET", "client": client_name}
|
||||||
|
if ups is not None:
|
||||||
|
request["ups"] = ups
|
||||||
|
|
||||||
data = socket_comms(socket_path, {'command': 'GET', 'client': client_name}, debug)
|
data = socket_comms(socket_path, request, debug)
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
raise
|
raise RuntimeError("No data received from daemon")
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
+46
-38
@@ -10,7 +10,7 @@ def influx_shutdown(influxclient):
|
|||||||
influxclient.close()
|
influxclient.close()
|
||||||
|
|
||||||
|
|
||||||
def influxdb_export(bucket, url=None, org=None, token=None, socket_path=None, daemonize=True, debug=0):
|
def influxdb_export(bucket, url=None, org=None, token=None, socket_path=None, ups=None, daemonize=True, debug=0):
|
||||||
if not url:
|
if not url:
|
||||||
url = os.environ["INFLUXDB_V2_URL"]
|
url = os.environ["INFLUXDB_V2_URL"]
|
||||||
org = os.environ.get("INFLUXDB_V2_ORG")
|
org = os.environ.get("INFLUXDB_V2_ORG")
|
||||||
@@ -21,21 +21,21 @@ def influxdb_export(bucket, url=None, org=None, token=None, socket_path=None, da
|
|||||||
|
|
||||||
if daemonize:
|
if daemonize:
|
||||||
while True:
|
while True:
|
||||||
data = client.read_data(socket_path, 'influxdb')
|
data = client.read_data(socket_path, 'influxdb', ups=ups)
|
||||||
influxdb_write_snapshot(influxclient, bucket, data, debug)
|
influxdb_write_snapshot(influxclient, bucket, data, debug)
|
||||||
time.sleep(DAEMON_UPDATE_PERIOD)
|
time.sleep(DAEMON_UPDATE_PERIOD)
|
||||||
else:
|
else:
|
||||||
data = client.read_data(socket_path, 'influxdb')
|
data = client.read_data(socket_path, 'influxdb', ups=ups)
|
||||||
influxdb_write_snapshot(influxclient, bucket, data, debug)
|
influxdb_write_snapshot(influxclient, bucket, data, debug)
|
||||||
|
|
||||||
influxclient.close()
|
influxclient.close()
|
||||||
atexit.unregister(influx_shutdown)
|
atexit.unregister(influx_shutdown)
|
||||||
|
|
||||||
|
|
||||||
def influxdb_write_snapshot(influxclient, bucket, data, debug=0):
|
def influxdb_write_snapshot(influxclient, bucket, ups_data, debug=0):
|
||||||
if debug > 1:
|
if debug > 1:
|
||||||
print("influxdb: creating snapshot")
|
print("influxdb: creating snapshot")
|
||||||
points = influxdb_create_snapshot(data, debug)
|
points = influxdb_create_snapshot(ups_data, debug)
|
||||||
if debug > 1:
|
if debug > 1:
|
||||||
print("influxdb: writing snapshot")
|
print("influxdb: writing snapshot")
|
||||||
try:
|
try:
|
||||||
@@ -44,51 +44,56 @@ def influxdb_write_snapshot(influxclient, bucket, data, debug=0):
|
|||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
def influxdb_create_snapshot(data, debug=0):
|
def influxdb_create_snapshot(ups_data, debug=0):
|
||||||
|
"""Build InfluxDB points from {ups_name: JBDUPS}, tagging each point with the UPS name."""
|
||||||
points = []
|
points = []
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
|
||||||
for kind, contains in data.items():
|
for ups_name, data in ups_data.items():
|
||||||
helpmsg = contains.get('help') or ''
|
for kind, contains in data.items():
|
||||||
units = contains.get('units')
|
helpmsg = contains.get('help') or ''
|
||||||
|
units = contains.get('units')
|
||||||
|
|
||||||
if contains.get('raw_value') is not None:
|
if contains.get('raw_value') is not None:
|
||||||
value = contains.get('raw_value')
|
value = contains.get('raw_value')
|
||||||
if debug > 2:
|
|
||||||
print("value: {} : {}".format(kind, value))
|
|
||||||
points.append(
|
|
||||||
Point(kind)
|
|
||||||
.tag("units", units)
|
|
||||||
.tag("help", helpmsg)
|
|
||||||
.field("value", value)
|
|
||||||
.time(now)
|
|
||||||
)
|
|
||||||
|
|
||||||
if contains.get('raw_values') is not None and isinstance(contains.get('raw_values'), dict):
|
|
||||||
label = contains.get('label')
|
|
||||||
for idx, label_value in contains.get('raw_values').items():
|
|
||||||
if debug > 2:
|
if debug > 2:
|
||||||
print("labels: {} [{}] : {}".format(kind, idx, label_value))
|
print("value: {} [{}] : {}".format(kind, ups_name, value))
|
||||||
points.append(
|
points.append(
|
||||||
Point(kind)
|
Point(kind)
|
||||||
.tag(label, idx)
|
.tag("ups", ups_name)
|
||||||
.tag("units", units)
|
.tag("units", units)
|
||||||
.tag("help", helpmsg)
|
.tag("help", helpmsg)
|
||||||
.field("value", label_value)
|
.field("value", value)
|
||||||
.time(now)
|
.time(now)
|
||||||
)
|
)
|
||||||
|
|
||||||
if contains.get('info') is not None:
|
if contains.get('raw_values') is not None and isinstance(contains.get('raw_values'), dict):
|
||||||
value = contains.get('info')
|
label = contains.get('label')
|
||||||
if debug > 2:
|
for idx, label_value in contains.get('raw_values').items():
|
||||||
print("info: {} : {}".format(kind, value))
|
if debug > 2:
|
||||||
points.append(
|
print("labels: {} [{}][{}] : {}".format(kind, ups_name, idx, label_value))
|
||||||
Point(kind)
|
points.append(
|
||||||
.tag("units", units)
|
Point(kind)
|
||||||
.tag("help", helpmsg)
|
.tag("ups", ups_name)
|
||||||
.field("value", value)
|
.tag(label, idx)
|
||||||
.time(now)
|
.tag("units", units)
|
||||||
)
|
.tag("help", helpmsg)
|
||||||
|
.field("value", label_value)
|
||||||
|
.time(now)
|
||||||
|
)
|
||||||
|
|
||||||
|
if contains.get('info') is not None:
|
||||||
|
value = contains.get('info')
|
||||||
|
if debug > 2:
|
||||||
|
print("info: {} [{}] : {}".format(kind, ups_name, value))
|
||||||
|
points.append(
|
||||||
|
Point(kind)
|
||||||
|
.tag("ups", ups_name)
|
||||||
|
.tag("units", units)
|
||||||
|
.tag("help", helpmsg)
|
||||||
|
.field("value", value)
|
||||||
|
.time(now)
|
||||||
|
)
|
||||||
|
|
||||||
return points
|
return points
|
||||||
|
|
||||||
@@ -110,6 +115,8 @@ def main():
|
|||||||
default=False, help='Set the influx token when sending data to influxdb (overrides INFLUXDB environment variables)')
|
default=False, help='Set the influx token when sending data to influxdb (overrides INFLUXDB environment variables)')
|
||||||
parser.add_argument('--socket', '-s', dest='socket', action='store',
|
parser.add_argument('--socket', '-s', dest='socket', action='store',
|
||||||
default='/run/bmspy/bms', help='Socket to communicate with daemon')
|
default='/run/bmspy/bms', help='Socket to communicate with daemon')
|
||||||
|
parser.add_argument('--ups', dest='ups', action='store', default=None,
|
||||||
|
help='Only export data for this UPS name (default: all)')
|
||||||
parser.add_argument('--verbose', '-v', action='count',
|
parser.add_argument('--verbose', '-v', action='count',
|
||||||
default=0, help='Print more verbose information (can be specified multiple times)')
|
default=0, help='Print more verbose information (can be specified multiple times)')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -138,6 +145,7 @@ def main():
|
|||||||
org=args.influx_org or None,
|
org=args.influx_org or None,
|
||||||
token=args.influx_token or None,
|
token=args.influx_token or None,
|
||||||
socket_path=args.socket,
|
socket_path=args.socket,
|
||||||
|
ups=args.ups,
|
||||||
daemonize=True,
|
daemonize=True,
|
||||||
debug=debug,
|
debug=debug,
|
||||||
)
|
)
|
||||||
|
|||||||
+61
-38
@@ -30,7 +30,6 @@ from bmspy.jbd_ups import collect_data, initialise_serial
|
|||||||
# usb 1-1.4: FTDI USB Serial Device converter now attached to ttyUSB0
|
# usb 1-1.4: FTDI USB Serial Device converter now attached to ttyUSB0
|
||||||
|
|
||||||
connected_clients = list()
|
connected_clients = list()
|
||||||
current_data = None
|
|
||||||
|
|
||||||
|
|
||||||
def signalHandler():
|
def signalHandler():
|
||||||
@@ -72,12 +71,7 @@ def read_request(connection, debug=0):
|
|||||||
return request_data
|
return request_data
|
||||||
|
|
||||||
|
|
||||||
def send_response(connection, response_data, debug=0):
|
def send_response(connection, response_data, client, debug=0):
|
||||||
try:
|
|
||||||
client = response_data.client
|
|
||||||
except AttributeError:
|
|
||||||
client = response_data.get("client", "unknown client")
|
|
||||||
|
|
||||||
if debug > 2:
|
if debug > 2:
|
||||||
print("socket: sending {!r}".format(response_data))
|
print("socket: sending {!r}".format(response_data))
|
||||||
try:
|
try:
|
||||||
@@ -100,6 +94,15 @@ def send_response(connection, response_data, debug=0):
|
|||||||
raise OSError("unable to encode response: {}".format(e))
|
raise OSError("unable to encode response: {}".format(e))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_device(device_str):
|
||||||
|
"""Parse 'name:/dev/path' or '/dev/path' into (name, path)."""
|
||||||
|
if not device_str.startswith("/") and ":" in device_str:
|
||||||
|
name, path = device_str.split(":", 1)
|
||||||
|
return name, path
|
||||||
|
name = device_str.split("/")[-1]
|
||||||
|
return name, device_str
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
import argparse
|
import argparse
|
||||||
import socket
|
import socket
|
||||||
@@ -108,9 +111,6 @@ def main():
|
|||||||
|
|
||||||
signal.signal(signal.SIGTERM, signalHandler)
|
signal.signal(signal.SIGTERM, signalHandler)
|
||||||
|
|
||||||
global current_data
|
|
||||||
timestamp = 0
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Query JBD BMS and report status",
|
description="Query JBD BMS and report status",
|
||||||
add_help=True,
|
add_help=True,
|
||||||
@@ -118,10 +118,11 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--device",
|
"--device",
|
||||||
"-d",
|
"-d",
|
||||||
dest="device",
|
dest="devices",
|
||||||
action="store",
|
action="append",
|
||||||
default="/dev/ttyUSB0",
|
default=None,
|
||||||
help="USB device to read",
|
metavar="[NAME:]/dev/PATH",
|
||||||
|
help="USB device to read (may be specified multiple times; optionally prefixed with name:)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--socket",
|
"--socket",
|
||||||
@@ -158,11 +159,24 @@ def main():
|
|||||||
|
|
||||||
debug = args.verbose
|
debug = args.verbose
|
||||||
|
|
||||||
|
device_list = args.devices or ["/dev/ttyUSB0"]
|
||||||
|
ups_devices = {}
|
||||||
|
for device_str in device_list:
|
||||||
|
name, path = parse_device(device_str)
|
||||||
|
if name in ups_devices:
|
||||||
|
print("server: duplicate UPS name '{}', skipping {}".format(name, path))
|
||||||
|
continue
|
||||||
|
ups_devices[name] = {
|
||||||
|
"ser": initialise_serial(path, debug),
|
||||||
|
"data": None,
|
||||||
|
"timestamp": 0,
|
||||||
|
}
|
||||||
|
if debug > 0:
|
||||||
|
print("server: registered UPS '{}' on {}".format(name, path))
|
||||||
|
|
||||||
if debug > 0:
|
if debug > 0:
|
||||||
print("Running BMS query daemon on socket {}".format(args.socket))
|
print("Running BMS query daemon on socket {}".format(args.socket))
|
||||||
|
|
||||||
ser = initialise_serial(args.device)
|
|
||||||
|
|
||||||
socket_dir = os.path.dirname(args.socket)
|
socket_dir = os.path.dirname(args.socket)
|
||||||
socket_dir_created = False
|
socket_dir_created = False
|
||||||
if not os.path.isdir(socket_dir):
|
if not os.path.isdir(socket_dir):
|
||||||
@@ -251,7 +265,10 @@ def main():
|
|||||||
case "REGISTER":
|
case "REGISTER":
|
||||||
connected_clients.append(client)
|
connected_clients.append(client)
|
||||||
send_response(
|
send_response(
|
||||||
connection, {"status": "REGISTERED", "client": client}, debug
|
connection,
|
||||||
|
{"status": "REGISTERED", "client": client},
|
||||||
|
client,
|
||||||
|
debug,
|
||||||
)
|
)
|
||||||
|
|
||||||
case "DEREGISTER":
|
case "DEREGISTER":
|
||||||
@@ -260,32 +277,38 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
send_response(
|
send_response(
|
||||||
connection, {"status": "DEREGISTERED", "client": client}, debug
|
connection,
|
||||||
)
|
{"status": "DEREGISTERED", "client": client},
|
||||||
|
client,
|
||||||
send_response(
|
debug,
|
||||||
connection, {"status": "DEREGISTERED", "client": client}, debug
|
|
||||||
)
|
)
|
||||||
|
|
||||||
case "GET":
|
case "GET":
|
||||||
timestamp = 0
|
ups_filter = request_data.get("ups")
|
||||||
if bool(current_data) is True:
|
targets = (
|
||||||
timestamp = current_data.get("timestamp", 0)
|
{ups_filter: ups_devices[ups_filter]}
|
||||||
print(
|
if ups_filter and ups_filter in ups_devices
|
||||||
"reading data, current timestamp is {}, time is {}".format(
|
else ups_devices
|
||||||
timestamp, time.time()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
# only get new data five seconds after the last read
|
|
||||||
if timestamp <= time.time() - 5:
|
|
||||||
current_data = None
|
|
||||||
while bool(current_data) is False:
|
|
||||||
current_data = collect_data(ser, debug)
|
|
||||||
time.sleep(1)
|
|
||||||
current_data["timestamp"] = time.time()
|
|
||||||
current_data["client"] = client
|
|
||||||
|
|
||||||
send_response(connection, current_data, debug)
|
result = {}
|
||||||
|
for name, device in targets.items():
|
||||||
|
if debug > 0:
|
||||||
|
print(
|
||||||
|
"reading data for '{}', timestamp={}, time={}".format(
|
||||||
|
name, device["timestamp"], time.time()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# only get new data five seconds after the last read
|
||||||
|
if device["timestamp"] <= time.time() - 5:
|
||||||
|
device["data"] = None
|
||||||
|
while not device["data"]:
|
||||||
|
device["data"] = collect_data(device["ser"], debug)
|
||||||
|
time.sleep(1)
|
||||||
|
device["timestamp"] = time.time()
|
||||||
|
result[name] = device["data"]
|
||||||
|
|
||||||
|
send_response(connection, result, client, debug)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
print(
|
print(
|
||||||
|
|||||||
Reference in New Issue
Block a user