From e764435ac1d4e56278a110ca6d61efc96b92bd06 Mon Sep 17 00:00:00 2001 From: Tomatensaus Date: Sat, 19 Nov 2022 16:18:30 +0200 Subject: [PATCH 01/12] add executable for bluetooth bms interaction --- bin/daly-bmsBT-cli | 287 ++++++++++++++++++++++++++++++++++ dalybms/daly_bms_bluetooth.py | 7 +- 2 files changed, 291 insertions(+), 3 deletions(-) create mode 100755 bin/daly-bmsBT-cli diff --git a/bin/daly-bmsBT-cli b/bin/daly-bmsBT-cli new file mode 100755 index 0000000..86414bc --- /dev/null +++ b/bin/daly-bmsBT-cli @@ -0,0 +1,287 @@ +#!/usr/bin/python3 +import argparse +import json +import logging +import sys +import asyncio +from dalybms import DalyBMSBluetooth + +class DalyBMSConnection(): + def __init__(self, mac_address,request_retries=3, logger=None): + self.bt_bms = DalyBMSBluetooth(request_retries,logger) + self.mac_address = mac_address + self.connected = False + + async def connect(self): + self.connected = await self.bt_bms.connect(mac_address=self.mac_address) + + async def update_cell_voltages(self): + if not self.connected: + await self.connect() + return self.bt_bms.get_cell_voltages() + + + async def update_cell_voltage_range(self): + if not self.connected: + await self.connect() + return await self.bt_bms.get_cell_voltage_range() + + async def get_status(self): + if not self.connected: + await self.connect() + return await self.bt_bms.get_status() + + async def get_temps2(self): + if not self.connected: + await self.connect() + return await self.bt_bms.get_max_min_temperature() + + async def get_soc(self): + if not self.connected: + await self.connect() + return await self.bt_bms.get_soc() + + async def get_mosfet(self): + if not self.connected: + await self.connect() + return await self.bt_bms.get_mosfet_status() + + async def get_temps(self): + if not self.connected: + await self.connect() + return await self.bt_bms.get_temperatures() + + async def get_bal(self): + if not self.connected: + await self.connect() + return await self.bt_bms.get_balancing_status() + + async def get_errors(self): + if not self.connected: + await self.connect() + return await self.bt_bms.get_errors() + + async def get_all(self): + if not self.connected: + await self.connect() + return await self.bt_bms.get_all() + + async def disconnect(self): + if not self.connected: + return + return await self.bt_bms.disconnect() + + +#async def main(con): +# await con.connect() +# await con.update_status() +# await con.update_soc() +# await con.update_temps2() +# await con.update_cell_voltage_range() +# await con.update_mosfet() +# await con.update_bal() +# await con.update_errors() +# await con.bt_bms.disconnect() +#asyncio.run(main(con)) + +parser = argparse.ArgumentParser() +parser.add_argument("-d", "--device", + help="MAC address, e.g. 88:99:AA:BB:CC", + type=str, required=True) +parser.add_argument("--status", help="show status", action="store_true") +parser.add_argument("--soc", help="show voltage, current, SOC", action="store_true") +parser.add_argument("--mosfet", help="show mosfet status", action="store_true") +parser.add_argument("--cell-voltages", help="show cell voltages", action="store_true") +parser.add_argument("--temperatures", help="show temperature sensor values", action="store_true") +parser.add_argument("--balancing", help="show cell balancing status", action="store_true") +parser.add_argument("--errors", help="show BMS errors", action="store_true") +parser.add_argument("--all", help="show all", action="store_true") +parser.add_argument("--check", help="Nagios style check", action="store_true") +parser.add_argument("--set-discharge-mosfet", help="'on' or 'off'", type=str) +parser.add_argument("--retry", help="retry X times if the request fails, default 5", type=int, default=5) +parser.add_argument("--verbose", help="Verbose output", action="store_true") + +parser.add_argument("--mqtt", help="Write output to MQTT", action="store_true") +parser.add_argument("--mqtt-hass", help="MQTT Home Assistant Mode", action="store_true") +parser.add_argument("--mqtt-topic", + help="MQTT topic to write to. default daly_bms", + type=str, + default="daly_bms") +parser.add_argument("--mqtt-broker", + help="MQTT broker (server). default localhost", + type=str, + default="localhost") +parser.add_argument("--mqtt-port", + help="MQTT port. default 1883", + type=int, + default=1883) +parser.add_argument("--mqtt-user", + help="Username to authenticate MQTT with", + type=str) +parser.add_argument("--mqtt-password", + help="Password to authenticate MQTT with", + type=str) + +args = parser.parse_args() + +log_format = '%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s' +if args.verbose: + level = logging.DEBUG +else: + level = logging.WARNING + +logging.basicConfig(level=level, format=log_format, datefmt='%H:%M:%S') + +logger = logging.getLogger() +#print (logger) + +bms = DalyBMSConnection(mac_address=args.device,request_retries=3, logger=logger ) +#asyncio.run(bms.connect()) + +result = False + +mqtt_client = None +if args.mqtt: + import paho.mqtt.client as paho + + mqtt_client = paho.Client() + mqtt_client.enable_logger(logger) + mqtt_client.username_pw_set(args.mqtt_user, args.mqtt_password) + mqtt_client.connect(args.mqtt_broker, port=args.mqtt_port) + + +def build_mqtt_hass_config_discovery(base): + # Instead of daly_bms should be here added a proper name (unique), like serial or something + # At this point it can be used only one daly_bms system with hass discovery + + hass_config_topic = f'homeassistant/sensor/daly_bms/{base.replace("/", "_")}/config' + hass_config_data = {} + + hass_config_data["unique_id"] = f'daly_bms_{base.replace("/", "_")}' + hass_config_data["name"] = f'Daly BMS {base.replace("/", " ")}' + + if 'soc_percent' in base: + hass_config_data["device_class"] = 'battery' + hass_config_data["unit_of_measurement"] = '%' + elif 'voltage' in base: + hass_config_data["device_class"] = 'voltage' + hass_config_data["unit_of_measurement"] = 'V' + elif 'current' in base: + hass_config_data["device_class"] = 'current' + hass_config_data["unit_of_measurement"] = 'A' + elif 'temperatures' in base: + hass_config_data["device_class"] = 'temperature' + hass_config_data["unit_of_measurement"] = '°C' + else: + pass + + hass_config_data["json_attributes_topic"] = f'{args.mqtt_topic}{base}' + hass_config_data["state_topic"] = f'{args.mqtt_topic}{base}' + + hass_device = { + "identifiers": ['daly_bms'], + "manufacturer": 'Daly', + "model": 'Currently not available', + "name": 'Daly BMS', + "sw_version": 'Currently not available' + } + hass_config_data["device"] = hass_device + + return hass_config_topic, json.dumps(hass_config_data) + + +def mqtt_single_out(topic, data, retain=False): + logger.debug(f'Send data: {data} on topic: {topic}, retain flag: {retain}') + mqtt_client.publish(topic, data, retain=retain) + + +def mqtt_iterator(result, base=''): + for key in result.keys(): + if type(result[key]) == dict: + mqtt_iterator(result[key], f'{base}/{key}') + else: + if args.mqtt_hass: + logger.debug('Sending out hass discovery message') + topic, output = build_mqtt_hass_config_discovery(f'{base}/{key}') + mqtt_single_out(topic, output, retain=True) + + if type(result[key]) == list: + val = json.dumps(result[key]) + else: + val = result[key] + + mqtt_single_out(f'{args.mqtt_topic}{base}/{key}', val) + + +def print_result(result): + if args.mqtt: + mqtt_iterator(result) + else: + print(json.dumps(result, indent=2)) + + +if args.status: + result = asyncio.run(bms.get_status()) + print_result(result) +if args.soc: + result = asyncio.run(bms.get_soc()) + print_result(result) +if args.mosfet: + result = asyncio.run(bms.get_mosfet_status()) + print_result(result) +if args.cell_voltages: + if not args.status: + bms.get_status() + result = asyncio.run(bms.get_cell_voltages()) + print_result(result) +if args.temperatures: + result = asyncio.run(bms.get_temperatures()) + print_result(result) +if args.balancing: + result = asyncio.run(bms.get_balancing_status()) + print_result(result) +if args.errors: + result = asyncio.run(bms.get_errors()) + print_result(result) +if args.all: + result = asyncio.run(bms.get_all()) + print_result(result) + +if args.check: + status = asyncio.run(bms.get_status()) + status_code = 0 # OK + status_codes = ('OK', 'WARNING', 'CRITICAL', 'UNKNOWN') + status_line = '' + + data = asyncio.run(bms.get_soc()) + perfdata = [] + if data: + for key, value in data.items(): + perfdata.append('%s=%s' % (key, value)) + + # todo: read errors + + if status_code == 0: + status_line = '%0.1f volt, %0.1f amper' % (data['total_voltage'], data['current']) + + print("%s - %s | %s" % (status_codes[status_code], status_line, " ".join(perfdata))) + sys.exit(status_code) + +if args.set_discharge_mosfet: + if args.set_discharge_mosfet == 'on': + on = True + elif args.set_discharge_mosfet == 'off': + on = False + else: + print("invalid value '%s', expected 'on' or 'off'" % args.set_discharge_mosfet) + sys.exit(1) + + result = bms.set_discharge_mosfet(on=on) + +if mqtt_client: + mqtt_client.disconnect() + +asyncio.run(bms.disconnect()) + +if not result: + sys.exit(1) diff --git a/dalybms/daly_bms_bluetooth.py b/dalybms/daly_bms_bluetooth.py index 104eddc..9ee754f 100644 --- a/dalybms/daly_bms_bluetooth.py +++ b/dalybms/daly_bms_bluetooth.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 import asyncio import subprocess import logging @@ -29,7 +30,7 @@ async def connect(self, mac_address): """ try: """ - When an earlier execution of the script crashed, the connection to the devices stays open and future + When an earlier execution of the script crashed, the connection to the devices stays open and future connection attempts would fail with this error: bleak.exc.BleakError: Device with address AA:BB:CC:DD:EE:FF was not found. see https://github.com/hbldh/bleak/issues/367 @@ -55,7 +56,7 @@ async def disconnect(self): async def _read_request(self, command, max_responses=1): response_data = None - x = None + x = 0 for x in range(0, self.request_retries): response_data = await self._read( command=command, @@ -179,7 +180,7 @@ async def get_all(self): return { "soc": await self.get_soc(), "cell_voltage_range": await self.get_cell_voltage_range(), - "temperature_range": await self.get_temperature_range(), + "temperature_range": await self.get_max_min_temperature(), "mosfet_status": await self.get_mosfet_status(), "status": await self.get_status(), "cell_voltages": await self.get_cell_voltages(), From efe5d235e137f1db68aa3ad3e45cc411bb70f905 Mon Sep 17 00:00:00 2001 From: Tomatensaus Date: Sat, 19 Nov 2022 16:56:55 +0200 Subject: [PATCH 02/12] some instructions for getting bluetooth going --- README.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f486594..f613077 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This is a Python module for reading data from Daly BMS devices. It supports seri ## Compatibility -There are two different types of devices sold under the Daly Brand, which use different communication protocols. +There are two different types of devices sold under the Daly Brand, which use different communication protocols. This module was initially written for BMS supported by the Windows program BMS monitor. Later (in #1) the support for BMS supported by the BMStool, also known as Sinowealth, was added. They don't support all commands of the first protocol, but still provide valuable information. @@ -163,10 +163,60 @@ Get everything possible: ## Notes + ### Bluetooth +You can fetch the values via bluetooth with the instructions below, this was tested on a raspi zero - It's also recommended to have a recent BlueZ installed (>=5.53). The Bluetooth connection uses `asyncio` for the connection, so the data is received asynchronous. - It seems like the Bluetooth BMS Module goes to sleep after 1 hour of inactivity (no load or charging), while the serial connection responds all the time. Sending a command via the serial interface wakes up the Bluetooth module. + +- Use the daly-bmsBT-cli to interact with your bluetooth, you will need to find the MAC address of your device +``` +pi@pizero:~ $ bluetoothctl +Agent registered +[CHG] Controller B8:27:EB:AC:93:B5 Pairable: yes +[bluetooth]# power on +Failed to set power on: org.bluez.Error.Blocked +[bluetooth]# scan on +Failed to start discovery: org.bluez.Error.NotReady +[bluetooth]# exit +pi@pizero:~ $ rfkill list +0: phy0: Wireless LAN + Soft blocked: no + Hard blocked: no +1: hci0: Bluetooth + Soft blocked: yes + Hard blocked: no +pi@pizero:~ $ rfkill unblock bluetooth +pi@pizero:~ $ rfkill list + 0: phy0: Wireless LAN + Soft blocked: no + Hard blocked: no + 1: hci0: Bluetooth + Soft blocked: no + Hard blocked: no +pi@pizero:~ $ bluetoothctl +Agent registered +[CHG] Controller B8:27:EB:AC:93:B5 Pairable: yes +[bluetooth]# power on +Changing power on succeeded +[bluetooth]# scan on +Discovery started +[CHG] Controller B8:27:EB:AC:93:B5 Discovering: yes +[NEW] Device 02:11:23:34:7D:88 DL-021123347D88 +[NEW] Device A0:9E:1A:61:8B:37 Polar Vantage M 62C +[NEW] Device 58:7E:61:4D:0A:DB 58-7E-61-4D-0A-DB +[bluetooth]# exit +``` +I spot the MAC of my Daly it is this line [NEW] Device 02:11:23:34:7D:88 DL-021123347D88 +Test the output to screen +``` +./daly-bmsBT-cli --all -d 02:11:23:34:7D:88 +``` +This is the command to populate your homeassistant, you should setup a systemd service for this +``` +./daly-bmsBT-cli --all --mqtt --mqtt-hass --mqtt-user yourrmqttuser --mqtt-password yourmqttpassword --mqtt-broker 192.168.2.21 -d 02:11:23:34:7D:88 +``` From db7a23c7145b6d49a15f4148e4802d4f10f858a1 Mon Sep 17 00:00:00 2001 From: Tomatensaus Date: Mon, 21 Nov 2022 11:15:34 +0200 Subject: [PATCH 03/12] added binary to install script --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9f9175d..15022d1 100644 --- a/setup.py +++ b/setup.py @@ -23,5 +23,5 @@ "Programming Language :: Python :: 3.8", ], packages=["dalybms"], - scripts=["bin/daly-bms-cli"], + scripts=["bin/daly-bms-cli" ,"bin/daly-bmsBT-cli"], ) From 4c329a581fc991d6b56777944d5e804a57f8bb7d Mon Sep 17 00:00:00 2001 From: Tomatensaus Date: Mon, 21 Nov 2022 22:38:01 +0200 Subject: [PATCH 04/12] add sample config file and services file for systemd --- bin/daly-bmsBT-cli | 46 +++++++++++++++++++++++++++++++++++++++++- service/dalybt.conf | 14 +++++++++++++ service/dalybt.service | 32 +++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 service/dalybt.conf create mode 100644 service/dalybt.service diff --git a/bin/daly-bmsBT-cli b/bin/daly-bmsBT-cli index 86414bc..7b61eee 100755 --- a/bin/daly-bmsBT-cli +++ b/bin/daly-bmsBT-cli @@ -121,7 +121,13 @@ parser.add_argument("--mqtt-user", parser.add_argument("--mqtt-password", help="Password to authenticate MQTT with", type=str) - +parser.add_argument("-C","--configfile", + nargs="?", + type=str, + help="Full location of config file (default None, /etc/dalybt.conf if -C supplied)", + const="/etc/dalybt.conf", + default=None,) +parser.add_argument("--daemon", action="store_true", help="Run as daemon") args = parser.parse_args() log_format = '%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s' @@ -135,6 +141,44 @@ logging.basicConfig(level=level, format=log_format, datefmt='%H:%M:%S') logger = logging.getLogger() #print (logger) +# Initialize Daemon +if args.daemon: + import time + import systemd.daemon + + # Tell systemd that our service is ready + systemd.daemon.notify("READY=1") + print("Service Initializing ...") + pause = 60 + + # If config file specified, process +if args.configfile: + import configparser + log.debug(f"args.configfile is true: {args.configfile}") + config = configparser.ConfigParser() + try: + config.read(args.configfile) + except configparser.DuplicateSectionError as e: + log.error(f"Config File '{args.configfile}' has duplicate sections") + log.error(e) + exit(1) + sections = config.sections() + # Check setup section exists + if "SETUP" not in config: + log.error(f"Config File '{args.configfile}' is missing the required 'SETUP' section") + exit(1) + # Process setup section + pause = config["SETUP"].getint("pause", fallback=60) + # Overide mqtt_broker settings + mqtt_broker.update(args.mqtt, config["SETUP"].get("mqtt", fallback=False)) + mqtt_broker.update(args.mqtt_broker, config["SETUP"].get("mqtt-broker", fallback=None)) + mqtt_broker.update(args.mqtt_port, config["SETUP"].getint("mqtt-port", fallback=None)) + mqtt_broker.update(args.mqtt_user, config["SETUP"].get("mqtt-user", fallback=None)) + mqtt_broker.update(args.mqtt_password, config["SETUP"].get("mqtt-password", fallback=None)) + sections.remove("SETUP") + + args.device = config["DALYBTBMS"].get("device", fallback=None) + bms = DalyBMSConnection(mac_address=args.device,request_retries=3, logger=logger ) #asyncio.run(bms.connect()) diff --git a/service/dalybt.conf b/service/dalybt.conf new file mode 100644 index 0000000..24030e6 --- /dev/null +++ b/service/dalybt.conf @@ -0,0 +1,14 @@ +[SETUP] +# Number of seconds to pause between loops of processing the sections +# i.e. the pause at the end of an entire run through the config file +# default is 60 +pause=60 +mqtt=True +mqtt-broker=localhost +mqtt-port=1883 +mqtt-user=username +mqtt-pass=password +mqtt-hass=True + +[DALYBTBMS] +device=02:11:23:34:7D:88 diff --git a/service/dalybt.service b/service/dalybt.service new file mode 100644 index 0000000..ad72db6 --- /dev/null +++ b/service/dalybt.service @@ -0,0 +1,32 @@ +# systemd unit file for the DALY Bluetooth Service +# +# needs to go to /etc/systemd/user/* + +[Unit] +# Human readable name of the unit +Description=Daly Bluetooth Service + + +[Service] +# Command to execute when the service is started +ExecStart=/usr/bin/python3 /usr/local/bin/daly-bmsBT-cli -C /etc/dalybt.conf --daemon + +# Disable Python's buffering of STDOUT and STDERR, so that output from the +# service shows up immediately in systemd's logs +Environment=PYTHONUNBUFFERED=1 + +# Automatically restart the service if it crashes +Restart=always +WatchdogSec=300 + +# Our service will notify systemd once it is up and running +Type=notify + +# Use a dedicated user to run our service +User=pi + + +[Install] +# Tell systemd to automatically start this service when the system boots +# (assuming the service is enabled) +WantedBy=default.target From ab0344fa9bc50d79bdf4bf23a4dd447bf6618992 Mon Sep 17 00:00:00 2001 From: Tomatensaus Date: Mon, 21 Nov 2022 23:00:45 +0200 Subject: [PATCH 05/12] config file options to overwrite command line --- bin/daly-bmsBT-cli | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/daly-bmsBT-cli b/bin/daly-bmsBT-cli index 7b61eee..51f6275 100755 --- a/bin/daly-bmsBT-cli +++ b/bin/daly-bmsBT-cli @@ -87,7 +87,7 @@ class DalyBMSConnection(): parser = argparse.ArgumentParser() parser.add_argument("-d", "--device", help="MAC address, e.g. 88:99:AA:BB:CC", - type=str, required=True) + type=str) parser.add_argument("--status", help="show status", action="store_true") parser.add_argument("--soc", help="show voltage, current, SOC", action="store_true") parser.add_argument("--mosfet", help="show mosfet status", action="store_true") @@ -154,7 +154,7 @@ if args.daemon: # If config file specified, process if args.configfile: import configparser - log.debug(f"args.configfile is true: {args.configfile}") + logger.debug(f"args.configfile is true: {args.configfile}") config = configparser.ConfigParser() try: config.read(args.configfile) @@ -170,11 +170,11 @@ if args.configfile: # Process setup section pause = config["SETUP"].getint("pause", fallback=60) # Overide mqtt_broker settings - mqtt_broker.update(args.mqtt, config["SETUP"].get("mqtt", fallback=False)) - mqtt_broker.update(args.mqtt_broker, config["SETUP"].get("mqtt-broker", fallback=None)) - mqtt_broker.update(args.mqtt_port, config["SETUP"].getint("mqtt-port", fallback=None)) - mqtt_broker.update(args.mqtt_user, config["SETUP"].get("mqtt-user", fallback=None)) - mqtt_broker.update(args.mqtt_password, config["SETUP"].get("mqtt-password", fallback=None)) + args.mqtt = config["SETUP"].get("mqtt", fallback=False) + args.mqtt_broker = config["SETUP"].get("mqtt-broker", fallback=None) + args.mqtt_port = config["SETUP"].getint("mqtt-port", fallback=None) + args.mqtt_user = config["SETUP"].get("mqtt-user", fallback=None) + args.mqtt_password = config["SETUP"].get("mqtt-password", fallback=None) sections.remove("SETUP") args.device = config["DALYBTBMS"].get("device", fallback=None) From c4cd3b32de8d73cb09a0e4cd8005077512367148 Mon Sep 17 00:00:00 2001 From: Tomatensaus Date: Tue, 22 Nov 2022 14:16:28 +0200 Subject: [PATCH 06/12] fix the broken pipe on mqtt --- bin/daly-bmsBT-cli | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/daly-bmsBT-cli b/bin/daly-bmsBT-cli index 51f6275..ba0e177 100755 --- a/bin/daly-bmsBT-cli +++ b/bin/daly-bmsBT-cli @@ -170,11 +170,12 @@ if args.configfile: # Process setup section pause = config["SETUP"].getint("pause", fallback=60) # Overide mqtt_broker settings - args.mqtt = config["SETUP"].get("mqtt", fallback=False) - args.mqtt_broker = config["SETUP"].get("mqtt-broker", fallback=None) - args.mqtt_port = config["SETUP"].getint("mqtt-port", fallback=None) - args.mqtt_user = config["SETUP"].get("mqtt-user", fallback=None) - args.mqtt_password = config["SETUP"].get("mqtt-password", fallback=None) + args.mqtt = config["SETUP"].get("mqtt", fallback=args.mqtt) + args.mqtt_hass = config["SETUP"].get("mqtt-hass", fallback=args.mqtt_hass) + args.mqtt_broker = config["SETUP"].get("mqtt-broker", fallback=args.mqtt_broker) + args.mqtt_port = config["SETUP"].getint("mqtt-port", fallback=args.mqtt_port) + args.mqtt_user = config["SETUP"].get("mqtt-user", fallback=args.mqtt_user) + args.mqtt_password = config["SETUP"].get("mqtt-password", fallback=args.mqtt_password) sections.remove("SETUP") args.device = config["DALYBTBMS"].get("device", fallback=None) @@ -192,7 +193,7 @@ if args.mqtt: mqtt_client.enable_logger(logger) mqtt_client.username_pw_set(args.mqtt_user, args.mqtt_password) mqtt_client.connect(args.mqtt_broker, port=args.mqtt_port) - + mqtt_client.loop_start() def build_mqtt_hass_config_discovery(base): # Instead of daly_bms should be here added a proper name (unique), like serial or something @@ -238,7 +239,6 @@ def mqtt_single_out(topic, data, retain=False): logger.debug(f'Send data: {data} on topic: {topic}, retain flag: {retain}') mqtt_client.publish(topic, data, retain=retain) - def mqtt_iterator(result, base=''): for key in result.keys(): if type(result[key]) == dict: From 1984b6b564b3ab778870659ece0d082fe642d5c4 Mon Sep 17 00:00:00 2001 From: Tomatensaus Date: Thu, 24 Nov 2022 14:13:03 +0200 Subject: [PATCH 07/12] fix broken config options --- bin/daly-bmsBT-cli | 140 +++++++++++++++++++++++++-------------------- 1 file changed, 78 insertions(+), 62 deletions(-) diff --git a/bin/daly-bmsBT-cli b/bin/daly-bmsBT-cli index ba0e177..4e6c28d 100755 --- a/bin/daly-bmsBT-cli +++ b/bin/daly-bmsBT-cli @@ -127,7 +127,8 @@ parser.add_argument("-C","--configfile", help="Full location of config file (default None, /etc/dalybt.conf if -C supplied)", const="/etc/dalybt.conf", default=None,) -parser.add_argument("--daemon", action="store_true", help="Run as daemon") +parser.add_argument("--daemon", action="store_true", help="Run as daemon",default=False) +parser.add_argument("--daemon-pause", type=int, help="Sleep time for daemon",default=60) args = parser.parse_args() log_format = '%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s' @@ -141,16 +142,6 @@ logging.basicConfig(level=level, format=log_format, datefmt='%H:%M:%S') logger = logging.getLogger() #print (logger) -# Initialize Daemon -if args.daemon: - import time - import systemd.daemon - - # Tell systemd that our service is ready - systemd.daemon.notify("READY=1") - print("Service Initializing ...") - pause = 60 - # If config file specified, process if args.configfile: import configparser @@ -168,10 +159,10 @@ if args.configfile: log.error(f"Config File '{args.configfile}' is missing the required 'SETUP' section") exit(1) # Process setup section - pause = config["SETUP"].getint("pause", fallback=60) + args.daemon_pause = config["SETUP"].getint("daemon-pause", fallback=60) # Overide mqtt_broker settings - args.mqtt = config["SETUP"].get("mqtt", fallback=args.mqtt) - args.mqtt_hass = config["SETUP"].get("mqtt-hass", fallback=args.mqtt_hass) + args.mqtt = config["SETUP"].getboolean("mqtt", fallback=args.mqtt) + args.mqtt_hass = config["SETUP"].getboolean("mqtt-hass", fallback=args.mqtt_hass) args.mqtt_broker = config["SETUP"].get("mqtt-broker", fallback=args.mqtt_broker) args.mqtt_port = config["SETUP"].getint("mqtt-port", fallback=args.mqtt_port) args.mqtt_user = config["SETUP"].get("mqtt-user", fallback=args.mqtt_user) @@ -180,11 +171,12 @@ if args.configfile: args.device = config["DALYBTBMS"].get("device", fallback=None) +#print(args) + bms = DalyBMSConnection(mac_address=args.device,request_retries=3, logger=logger ) #asyncio.run(bms.connect()) result = False - mqtt_client = None if args.mqtt: import paho.mqtt.client as paho @@ -264,52 +256,72 @@ def print_result(result): print(json.dumps(result, indent=2)) -if args.status: - result = asyncio.run(bms.get_status()) - print_result(result) -if args.soc: - result = asyncio.run(bms.get_soc()) - print_result(result) -if args.mosfet: - result = asyncio.run(bms.get_mosfet_status()) - print_result(result) -if args.cell_voltages: - if not args.status: - bms.get_status() - result = asyncio.run(bms.get_cell_voltages()) - print_result(result) -if args.temperatures: - result = asyncio.run(bms.get_temperatures()) - print_result(result) -if args.balancing: - result = asyncio.run(bms.get_balancing_status()) - print_result(result) -if args.errors: - result = asyncio.run(bms.get_errors()) - print_result(result) -if args.all: - result = asyncio.run(bms.get_all()) - print_result(result) - -if args.check: - status = asyncio.run(bms.get_status()) - status_code = 0 # OK - status_codes = ('OK', 'WARNING', 'CRITICAL', 'UNKNOWN') - status_line = '' - - data = asyncio.run(bms.get_soc()) - perfdata = [] - if data: - for key, value in data.items(): - perfdata.append('%s=%s' % (key, value)) - - # todo: read errors - - if status_code == 0: - status_line = '%0.1f volt, %0.1f amper' % (data['total_voltage'], data['current']) - - print("%s - %s | %s" % (status_codes[status_code], status_line, " ".join(perfdata))) - sys.exit(status_code) +# Initialize Daemon +if args.daemon: + import time + import systemd.daemon + + # Tell systemd that our service is ready + systemd.daemon.notify(systemd.daemon.Notification.READY) + + +while True: + if args.status: + result = asyncio.run(bms.get_status()) + print_result(result) + if args.soc: + result = asyncio.run(bms.get_soc()) + print_result(result) + if args.mosfet: + result = asyncio.run(bms.get_mosfet_status()) + print_result(result) + if args.cell_voltages: + if not args.status: + asyncio.run(bms.get_status()) + result = asyncio.run(bms.get_cell_voltages()) + print_result(result) + if args.temperatures: + result = asyncio.run(bms.get_temperatures()) + print_result(result) + if args.balancing: + result = asyncio.run(bms.get_balancing_status()) + print_result(result) + if args.errors: + result = asyncio.run(bms.get_errors()) + print_result(result) + if args.all: + result = asyncio.run(bms.get_all()) + print_result(result) + + if args.check: + status = asyncio.run(bms.get_status()) + status_code = 0 # OK + status_codes = ('OK', 'WARNING', 'CRITICAL', 'UNKNOWN') + status_line = '' + + data = asyncio.run(bms.get_soc()) + perfdata = [] + if data: + for key, value in data.items(): + perfdata.append('%s=%s' % (key, value)) + + # todo: read errors + + if status_code == 0: + status_line = '%0.1f volt, %0.1f amper' % (data['total_voltage'], data['current']) + + print("%s - %s | %s" % (status_codes[status_code], status_line, " ".join(perfdata))) + sys.exit(status_code) + if args.set_discharge_mosfet: + break #setting mosfets not part of the daemon loop + if args.daemon: + systemd.daemon.notify(systemd.daemon.Notification.WATCHDOG) + print(f"Sleeping for {args.daemon_pause} sec") + time.sleep(args.daemon_pause) + else: + logger.debug("Once off command, not looping") + break + if args.set_discharge_mosfet: if args.set_discharge_mosfet == 'on': @@ -320,12 +332,16 @@ if args.set_discharge_mosfet: print("invalid value '%s', expected 'on' or 'off'" % args.set_discharge_mosfet) sys.exit(1) - result = bms.set_discharge_mosfet(on=on) + result = asyncio.run(bms.set_discharge_mosfet(on=on)) if mqtt_client: mqtt_client.disconnect() + mqtt_client.loop_stop() asyncio.run(bms.disconnect()) if not result: sys.exit(1) + +if args.daemon: + systemd.daemon.notify(systemd.daemon.Notification.STOPPING) From 16041a77ec2c21edbf0003e9efebf1941463b8a5 Mon Sep 17 00:00:00 2001 From: Tomatensaus Date: Thu, 24 Nov 2022 14:13:55 +0200 Subject: [PATCH 08/12] typo on password option --- service/dalybt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/dalybt.conf b/service/dalybt.conf index 24030e6..513c127 100644 --- a/service/dalybt.conf +++ b/service/dalybt.conf @@ -7,7 +7,7 @@ mqtt=True mqtt-broker=localhost mqtt-port=1883 mqtt-user=username -mqtt-pass=password +mqtt-password=password mqtt-hass=True [DALYBTBMS] From 39baa8c130fd4dcb4c3558ac3b14f28bf2a792d0 Mon Sep 17 00:00:00 2001 From: Tomatensaus Date: Thu, 24 Nov 2022 15:15:58 +0200 Subject: [PATCH 09/12] fixed install script that was giving startup errors --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 15022d1..21f049c 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 import pathlib -from setuptools import setup +from setuptools import setup, find_packages HERE = pathlib.Path(__file__).parent README = (HERE / "README.md").read_text() @@ -22,6 +22,6 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ], - packages=["dalybms"], + packages=find_packages(), scripts=["bin/daly-bms-cli" ,"bin/daly-bmsBT-cli"], ) From d11642a52414254aeaa760c4a92b6472020f8074 Mon Sep 17 00:00:00 2001 From: Tomatensaus Date: Thu, 24 Nov 2022 15:20:21 +0200 Subject: [PATCH 10/12] add instructions for installing the service --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index f613077..112c5e5 100644 --- a/README.md +++ b/README.md @@ -220,3 +220,14 @@ This is the command to populate your homeassistant, you should setup a systemd s ``` ./daly-bmsBT-cli --all --mqtt --mqtt-hass --mqtt-user yourrmqttuser --mqtt-password yourmqttpassword --mqtt-broker 192.168.2.21 -d 02:11:23:34:7D:88 ``` +Setting up the bluetooth as a systemd Service +``` +pi@pizero:~/python-daly-bms $ sudo cp service/dalybt.service /etc/systemd/system/ +pi@pizero:~/python-daly-bms $ sudo cp service/dalybt.conf /etc/ +``` +Now edit the options to setup your mqtt server in the /etc/dalybt.conf filename +``` +sudo systemctl start dalybt +sudo systemctl enable dalybt +``` +Your system should now automatically start the service when booting From 081d26ddc6fa70becac41d8794eb7d74cb5180ca Mon Sep 17 00:00:00 2001 From: Tomatensaus Date: Thu, 24 Nov 2022 15:20:48 +0200 Subject: [PATCH 11/12] typo fixed, by default the service will send all values --- service/dalybt.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/dalybt.service b/service/dalybt.service index ad72db6..de5439c 100644 --- a/service/dalybt.service +++ b/service/dalybt.service @@ -9,7 +9,7 @@ Description=Daly Bluetooth Service [Service] # Command to execute when the service is started -ExecStart=/usr/bin/python3 /usr/local/bin/daly-bmsBT-cli -C /etc/dalybt.conf --daemon +ExecStart=/usr/bin/python3 /usr/local/bin/daly-bmsBT-cli -C /etc/dalybt.conf --daemon --all # Disable Python's buffering of STDOUT and STDERR, so that output from the # service shows up immediately in systemd's logs From b4652671ae7db37ae7f12800e6678f3a88273c32 Mon Sep 17 00:00:00 2001 From: Tomatensaus Date: Thu, 24 Nov 2022 18:39:57 +0200 Subject: [PATCH 12/12] added rfkill unblock bluetooth to connect --- dalybms/daly_bms_bluetooth.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dalybms/daly_bms_bluetooth.py b/dalybms/daly_bms_bluetooth.py index 9ee754f..769f9cf 100644 --- a/dalybms/daly_bms_bluetooth.py +++ b/dalybms/daly_bms_bluetooth.py @@ -35,10 +35,13 @@ async def connect(self, mac_address): bleak.exc.BleakError: Device with address AA:BB:CC:DD:EE:FF was not found. see https://github.com/hbldh/bleak/issues/367 """ + out = subprocess.check_output("rfkill unblock bluetooth", shell = True) open_blue = subprocess.Popen(["bluetoothctl"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE) open_blue.communicate(b"disconnect %s\n" % mac_address.encode('utf-8')) open_blue.kill() + + except: pass self.client = BleakClient(mac_address)