- Introdotta l'implementazione JavaScript per la comunicazione BMS in bmscore.js, inclusi i metodi per il recupero dati e la gestione degli errori. - Creato errors.js per mappare i codici di errore dal formato Python a quello JavaScript.
181 lines
7.3 KiB
Python
181 lines
7.3 KiB
Python
import asyncio
|
|
import subprocess
|
|
import logging
|
|
from bleak import BleakClient
|
|
|
|
from .daly_bms import DalyBMS
|
|
|
|
|
|
class DalyBMSBluetooth(DalyBMS):
|
|
def __init__(self, request_retries=3, logger=None):
|
|
"""
|
|
|
|
:param request_retries: How often read requests should get repeated in case that they fail (Default: 3).
|
|
:param logger: Python Logger object for output (Default: None)
|
|
"""
|
|
if logger:
|
|
self.logger = logger
|
|
else:
|
|
self.logger = logging.getLogger(__name__)
|
|
DalyBMS.__init__(self, request_retries=request_retries, address=8, logger=logger)
|
|
self.client = None
|
|
self.response_cache = {}
|
|
|
|
async def connect(self, mac_address):
|
|
"""
|
|
Open the connection to the Bluetooth device.
|
|
|
|
:param mac_address: MAC address of the Bluetooth device
|
|
"""
|
|
try:
|
|
"""
|
|
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
|
|
"""
|
|
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)
|
|
await self.client.connect()
|
|
await self.client.start_notify(17, self._notification_callback)
|
|
await self.client.write_gatt_char(48, bytearray(b""))
|
|
|
|
async def disconnect(self):
|
|
"""
|
|
Disconnect from the Bluetooth device
|
|
"""
|
|
self.logger.info("Bluetooth Disconnecting")
|
|
await self.client.disconnect()
|
|
self.logger.info("Bluetooth Disconnected")
|
|
|
|
async def _read_request(self, command, max_responses=1):
|
|
response_data = None
|
|
x = None
|
|
for x in range(0, self.request_retries):
|
|
response_data = await self._read(
|
|
command=command,
|
|
max_responses=max_responses)
|
|
if not response_data:
|
|
self.logger.debug("%x. try failed, retrying..." % (x + 1))
|
|
await asyncio.sleep(0.2)
|
|
else:
|
|
break
|
|
if not response_data:
|
|
self.logger.error('%s failed after %s tries' % (command, x + 1))
|
|
return False
|
|
return response_data
|
|
|
|
async def _read(self, command, max_responses=1):
|
|
self.logger.debug("-- %s ------------------------" % command)
|
|
self.response_cache[command] = {"queue": [],
|
|
"future": asyncio.Future(),
|
|
"max_responses": max_responses,
|
|
"done": False}
|
|
|
|
message_bytes = self._format_message(command)
|
|
result = await self._async_char_write(command, message_bytes)
|
|
self.logger.debug("got %s" % result)
|
|
if not result:
|
|
return False
|
|
if max_responses == 1:
|
|
return result[0]
|
|
else:
|
|
return result
|
|
|
|
def _notification_callback(self, handle, data):
|
|
self.logger.debug("%s %s %s" % (handle, repr(data), len(data)))
|
|
responses = []
|
|
if len(data) == 13:
|
|
responses.append(data)
|
|
elif len(data) == 26:
|
|
responses.append(data[0:13])
|
|
responses.append(data[13:])
|
|
else:
|
|
self.logger.error(len(data), "bytes received, not 13 or 26, not implemented")
|
|
|
|
for response_bytes in responses:
|
|
command = response_bytes[2:3].hex()
|
|
if self.response_cache[command]["done"] is True:
|
|
self.logger.debug("skipping response for %s, done" % command)
|
|
return
|
|
self.response_cache[command]["queue"].append(response_bytes[4:-1])
|
|
if len(self.response_cache[command]["queue"]) == self.response_cache[command]["max_responses"]:
|
|
self.response_cache[command]["done"] = True
|
|
self.response_cache[command]["future"].set_result(self.response_cache[command]["queue"])
|
|
|
|
async def _async_char_write(self, command, value):
|
|
if not self.client.is_connected:
|
|
self.logger.info("Connecting...")
|
|
await self.client.connect()
|
|
|
|
await self.client.write_gatt_char(15, value)
|
|
self.logger.debug("Waiting...")
|
|
try:
|
|
result = await asyncio.wait_for(self.response_cache[command]["future"], 5)
|
|
except asyncio.TimeoutError:
|
|
self.logger.warning("Timeout while waiting for %s response" % command)
|
|
return False
|
|
self.logger.debug("got %s" % result)
|
|
return result
|
|
|
|
# wrap all sync functions so that they can be awaited
|
|
async def get_soc(self, response_data=None):
|
|
response_data = await self._read_request("90")
|
|
return super().get_soc(response_data=response_data)
|
|
|
|
async def get_cell_voltage_range(self, response_data=None):
|
|
response_data = await self._read_request("91")
|
|
return super().get_cell_voltage_range(response_data=response_data)
|
|
|
|
async def get_max_min_temperature(self, response_data=None):
|
|
response_data = await self._read_request("92")
|
|
return super().get_max_min_temperature(response_data=response_data)
|
|
|
|
async def get_mosfet_status(self, response_data=None):
|
|
response_data = await self._read_request("93")
|
|
return super().get_mosfet_status(response_data=response_data)
|
|
|
|
async def get_status(self, response_data=None):
|
|
response_data = await self._read_request("94")
|
|
return super().get_status(response_data=response_data)
|
|
|
|
async def get_cell_voltages(self, response_data=None):
|
|
if not self.status:
|
|
await self.get_status()
|
|
max_responses = self._calc_cell_voltage_responses()
|
|
if not max_responses:
|
|
return
|
|
response_data = await self._read_request("95", max_responses=max_responses)
|
|
|
|
return super().get_cell_voltages(response_data=response_data)
|
|
|
|
async def get_temperatures(self, response_data=None):
|
|
response_data = await self._read_request("95")
|
|
return super().get_temperatures(response_data=response_data)
|
|
|
|
async def get_balancing_status(self, response_data=None):
|
|
response_data = await self._read_request("96")
|
|
return super().get_balancing_status(response_data=response_data)
|
|
|
|
async def get_errors(self, response_data=None):
|
|
response_data = await self._read_request("97")
|
|
return super().get_errors(response_data=response_data)
|
|
|
|
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(),
|
|
"mosfet_status": await self.get_mosfet_status(),
|
|
"status": await self.get_status(),
|
|
"cell_voltages": await self.get_cell_voltages(),
|
|
"temperatures": await self.get_temperatures(),
|
|
"balancing_status": await self.get_balancing_status(),
|
|
"errors": await self.get_errors()
|
|
}
|