From b43ec971050fc36ab804b998dc1489345072b2c1 Mon Sep 17 00:00:00 2001 From: barry Date: Sat, 25 Jan 2025 16:02:40 -0300 Subject: [PATCH 01/32] added explicit python version for local situation --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bc19881..2cc6630 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,6 @@ "url": "github:stefanor/signalk-victron-ble" }, "scripts": { - "preinstall": "python3 -m venv ve && ve/bin/python -m pip install wheel && ve/bin/python -m pip install -U -r requirements.txt" + "preinstall": "/home/pi/.pyenv/versions/3.9.13/bin/python3.9 -m venv ve && ve/bin/python -m pip install wheel && ve/bin/python -m pip install -U -r requirements.txt" } } From 6a21b552143935b9170a6ae920c2717acff4c01b Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Sat, 25 Jan 2025 16:04:51 -0300 Subject: [PATCH 02/32] feat: Improve error handling, restart logic, and health monitoring --- index.js | 47 ++++++++++++++++++++++++++++++++++------------- plugin.py | 14 +++++++++++--- schema.json | 2 +- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index e21473e..f336d0a 100644 --- a/index.js +++ b/index.js @@ -15,46 +15,67 @@ module.exports = function (app) { let args = ['plugin.py'] child = spawn('ve/bin/python', args, { cwd: __dirname }) + const cleanup = () => { + if (child) { + child.removeAllListeners() + child = undefined + } + } + child.stdout.on('data', data => { app.debug(data.toString()) try { data.toString().split(/\r?\n/).forEach(line => { - // console.log(JSON.stringify(line)) if (line.length > 0) { app.handleMessage(undefined, JSON.parse(line)) + app.handleMessage(pkgData.name, { + updates: [{ + values: [{ + path: "plugins.victronBLE.status", + value: "active" + }] + }] + }) } }) } catch (e) { - console.error(e.message) + console.error('Data processing error:', e.message) } }) child.stderr.on('data', fromChild => { - console.error(fromChild.toString()) + console.error('Plugin stderr:', fromChild.toString()) }) child.on('error', err => { - console.error(err) + console.error('Subprocess error:', err) + cleanup() + setTimeout(() => run_python_plugin(options), 2000) }) child.on('close', code => { + app.handleMessage(pkgData.name, { + updates: [{ + values: [{ + path: "plugins.victronBLE.status", + value: "inactive" + }] + }] + }) + cleanup() if (code !== 0) { - console.warn(`Plugin exited ${code}, restarting...`) + console.warn(`Plugin exited ${code}, restarting in 2s...`) + setTimeout(() => run_python_plugin(options), 2000) } - child = undefined }) child.stdin.write(JSON.stringify(options)) child.stdin.write('\n') }; return { - start: async (options) => { - while (true) { - if (child === undefined) { - run_python_plugin(options); - } - await sleep(1000); - } + start: (options) => { + run_python_plugin(options) + return () => {} // Return dummy stop for compatibility }, stop: () => { if (child) { diff --git a/plugin.py b/plugin.py index 13745e3..62e92fa 100644 --- a/plugin.py +++ b/plugin.py @@ -432,9 +432,17 @@ def transform_ve_bus_data( async def monitor(devices: dict[str, ConfiguredDevice]) -> None: - scanner = SignalKScanner(devices) - await scanner.start() - await asyncio.Event().wait() + while True: + try: + scanner = SignalKScanner(devices) + await scanner.start() + await asyncio.Event().wait() + except (Exception, asyncio.CancelledError) as e: + logger.error(f"Scanner failed: {e}", exc_info=True) + await asyncio.sleep(5) # Wait before reconnect + continue + else: + break def main() -> None: diff --git a/schema.json b/schema.json index 268cbdd..0825afc 100644 --- a/schema.json +++ b/schema.json @@ -30,7 +30,7 @@ "secondary_battery": { "type": "string", "title": "Secondary Battery Device ID (if relevant)", - "default": "starter" + "default": null } } } From a911e39a3aa5dac0b262e87f71452a2ca8088905 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Sat, 25 Jan 2025 16:05:32 -0300 Subject: [PATCH 03/32] refactor: Improve type hints using TypeVar for device data transformers --- plugin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugin.py b/plugin.py index 62e92fa..c43b5d2 100644 --- a/plugin.py +++ b/plugin.py @@ -5,9 +5,11 @@ import json import logging import sys -from typing import Any, Callable, Union +from typing import Any, Callable, TypeVar, Union from bleak.backends.device import BLEDevice + +T = TypeVar('T', bound=DeviceData) from victron_ble.devices import ( AuxMode, BatteryMonitorData, @@ -67,8 +69,8 @@ def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: configured_device = self._devices[bl_device.address.lower()] id_ = configured_device.id transformers: dict[ - type[DeviceData], - Callable[[BLEDevice, ConfiguredDevice, Any, str], SignalKDeltaValues], + type[T], + Callable[[BLEDevice, ConfiguredDevice, T, str], SignalKDeltaValues], ] = { BatteryMonitorData: self.transform_battery_data, BatterySenseData: self.transform_battery_sense_data, From dd25d52e778770aa3bb0d9ac9cdb856b29675872 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Sat, 25 Jan 2025 16:05:55 -0300 Subject: [PATCH 04/32] fix: Move TypeVar definition after DeviceData import to resolve undefined name error --- plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugin.py b/plugin.py index c43b5d2..a9423ab 100644 --- a/plugin.py +++ b/plugin.py @@ -8,8 +8,6 @@ from typing import Any, Callable, TypeVar, Union from bleak.backends.device import BLEDevice - -T = TypeVar('T', bound=DeviceData) from victron_ble.devices import ( AuxMode, BatteryMonitorData, @@ -22,7 +20,10 @@ SmartLithiumData, SolarChargerData, VEBusData, + DeviceData, ) + +T = TypeVar('T', bound=DeviceData) from victron_ble.exceptions import AdvertisementKeyMissingError, UnknownDeviceError from victron_ble.scanner import Scanner From 8b7119dc348d094eba208db72c953ed2c1ffd0c4 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Sat, 25 Jan 2025 16:06:17 -0300 Subject: [PATCH 05/32] refactor: Update type hints for transformers dictionary in SignalKScanner --- plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.py b/plugin.py index a9423ab..4e68efd 100644 --- a/plugin.py +++ b/plugin.py @@ -70,8 +70,8 @@ def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: configured_device = self._devices[bl_device.address.lower()] id_ = configured_device.id transformers: dict[ - type[T], - Callable[[BLEDevice, ConfiguredDevice, T, str], SignalKDeltaValues], + type[DeviceData], + Callable[[BLEDevice, ConfiguredDevice, Any, str], SignalKDeltaValues], ] = { BatteryMonitorData: self.transform_battery_data, BatterySenseData: self.transform_battery_sense_data, From eed2b7bee46984e48b065e2f9d5292e078bae445 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Tue, 4 Feb 2025 11:15:58 -0300 Subject: [PATCH 06/32] feat: Add debug and info logs for BLE connection monitoring --- plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin.py b/plugin.py index 4e68efd..1a65811 100644 --- a/plugin.py +++ b/plugin.py @@ -438,6 +438,7 @@ async def monitor(devices: dict[str, ConfiguredDevice]) -> None: while True: try: scanner = SignalKScanner(devices) + logger.debug("Attempting to connect to BLE devices") await scanner.start() await asyncio.Event().wait() except (Exception, asyncio.CancelledError) as e: @@ -471,6 +472,7 @@ def main() -> None: secondary_battery=device.get("secondary_battery"), ) + logging.info("Starting Victron BLE plugin") asyncio.run(monitor(devices)) From 60d784dd30c89fb36bae681c2e0c2e44e03492ff Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Tue, 4 Feb 2025 11:16:29 -0300 Subject: [PATCH 07/32] fix: Remove duplicate DeviceData import in plugin.py --- plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin.py b/plugin.py index 1a65811..b61cee8 100644 --- a/plugin.py +++ b/plugin.py @@ -20,7 +20,6 @@ SmartLithiumData, SolarChargerData, VEBusData, - DeviceData, ) T = TypeVar('T', bound=DeviceData) From 9c50eabe11fb2373db75c4b06fc67ce6c1722643 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Tue, 4 Feb 2025 11:16:58 -0300 Subject: [PATCH 08/32] refactor: Replace `Any` with `DeviceData` in type hint for accuracy --- plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.py b/plugin.py index b61cee8..486639f 100644 --- a/plugin.py +++ b/plugin.py @@ -70,7 +70,7 @@ def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: id_ = configured_device.id transformers: dict[ type[DeviceData], - Callable[[BLEDevice, ConfiguredDevice, Any, str], SignalKDeltaValues], + Callable[[BLEDevice, ConfiguredDevice, DeviceData, str], SignalKDeltaValues], ] = { BatteryMonitorData: self.transform_battery_data, BatterySenseData: self.transform_battery_sense_data, From c62a14686e3f172a866d53c6cc1cca4816eee2c1 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Tue, 4 Feb 2025 11:17:12 -0300 Subject: [PATCH 09/32] refactor: Use TypeVar T in Callable type hint for transformers --- plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.py b/plugin.py index 486639f..100d91f 100644 --- a/plugin.py +++ b/plugin.py @@ -70,7 +70,7 @@ def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: id_ = configured_device.id transformers: dict[ type[DeviceData], - Callable[[BLEDevice, ConfiguredDevice, DeviceData, str], SignalKDeltaValues], + Callable[[BLEDevice, ConfiguredDevice, T, str], SignalKDeltaValues], ] = { BatteryMonitorData: self.transform_battery_data, BatterySenseData: self.transform_battery_sense_data, From 3d7d7b4ccdb923e00c88afaf561413561e5a3721 Mon Sep 17 00:00:00 2001 From: barry Date: Tue, 4 Feb 2025 11:34:46 -0300 Subject: [PATCH 10/32] refactor: Change debug logs to error logs in victron plugin --- plugin.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/plugin.py b/plugin.py index 100d91f..ed10478 100644 --- a/plugin.py +++ b/plugin.py @@ -28,6 +28,15 @@ logger = logging.getLogger("signalk-victron-ble") +logger.debug( + f"victron plugin starting up" +) + +logger.error( + f"victron plugin starting up" +) + + # 3.9 compatible TypeAliases SignalKDelta = dict[str, list[dict[str, Any]]] SignalKDeltaValues = list[dict[str, Union[int, float, str, None]]] @@ -55,7 +64,7 @@ def load_key(self, address: str) -> str: raise AdvertisementKeyMissingError(f"No key available for {address}") def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: - logger.debug( + logger.error( f"Received data from {bl_device.address.lower()}: {raw_data.hex()}" ) try: @@ -91,7 +100,7 @@ def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: sys.stdout.flush() return else: - logger.debug("Unknown device", device) + logger.error("Unknown device", device) def prepare_signalk_delta( self, bl_device: BLEDevice, values: SignalKDeltaValues @@ -437,7 +446,7 @@ async def monitor(devices: dict[str, ConfiguredDevice]) -> None: while True: try: scanner = SignalKScanner(devices) - logger.debug("Attempting to connect to BLE devices") + logger.error("Attempting to connect to BLE devices") await scanner.start() await asyncio.Event().wait() except (Exception, asyncio.CancelledError) as e: From ee255ede4332a8fbc8b5294800093acb4fb39a75 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Tue, 4 Feb 2025 11:34:49 -0300 Subject: [PATCH 11/32] feat: Enhance BLE logging with packet size, timestamp, and RSSI --- plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin.py b/plugin.py index ed10478..080cae2 100644 --- a/plugin.py +++ b/plugin.py @@ -65,7 +65,9 @@ def load_key(self, address: str) -> str: def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: logger.error( - f"Received data from {bl_device.address.lower()}: {raw_data.hex()}" + f"Received {len(raw_data)} byte packet from {bl_device.address.lower()} " + f"at {datetime.datetime.now().isoformat()}: " + f"{raw_data.hex()} (RSSI: {getattr(bl_device, 'rssi', 'N/A')})" ) try: device = self.get_device(bl_device, raw_data) From 7f04337fc9208c7b0466a88db445eae38084f81a Mon Sep 17 00:00:00 2001 From: barry Date: Tue, 4 Feb 2025 11:45:11 -0300 Subject: [PATCH 12/32] refactor: Change logging level from debug/info to error in plugin.py --- plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin.py b/plugin.py index 080cae2..9f45675 100644 --- a/plugin.py +++ b/plugin.py @@ -470,9 +470,9 @@ def main() -> None: stream=sys.stderr, level=logging.DEBUG if args.verbose else logging.WARNING ) - logging.debug("Waiting for config...") + logging.error("Waiting for config...") config = json.loads(input()) - logging.info("Configured: %s", json.dumps(config)) + logging.error("Configured: %s", json.dumps(config)) devices: dict[str, ConfiguredDevice] = {} for device in config["devices"]: devices[device["mac"].lower()] = ConfiguredDevice( @@ -482,7 +482,7 @@ def main() -> None: secondary_battery=device.get("secondary_battery"), ) - logging.info("Starting Victron BLE plugin") + logging.error("Starting Victron BLE plugin") asyncio.run(monitor(devices)) From 6b008c33760f63133f6cff2fee8534cb56df1ff2 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Tue, 4 Feb 2025 11:45:15 -0300 Subject: [PATCH 13/32] feat: enhance error logging for device identification and SignalK data --- plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugin.py b/plugin.py index 9f45675..59197b6 100644 --- a/plugin.py +++ b/plugin.py @@ -79,6 +79,7 @@ def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: data = device.parse(raw_data) configured_device = self._devices[bl_device.address.lower()] id_ = configured_device.id + logger.error(f"Processing device: ID={id_} MAC={bl_device.address.lower()}") transformers: dict[ type[DeviceData], Callable[[BLEDevice, ConfiguredDevice, T, str], SignalKDeltaValues], @@ -97,12 +98,12 @@ def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: if isinstance(data, data_type): values = transformer(bl_device, configured_device, data, id_) delta = self.prepare_signalk_delta(bl_device, values) - logger.info(delta) + logger.error("Generated SignalK delta: %s", json.dumps(delta)) print(json.dumps(delta)) sys.stdout.flush() return else: - logger.error("Unknown device", device) + logger.error("Unknown device type %s from %s", type(device).__name__, bl_device.address.lower()) def prepare_signalk_delta( self, bl_device: BLEDevice, values: SignalKDeltaValues From 826fb5f78ed9c46bf9c62009ff980607b21bd6f6 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Tue, 4 Feb 2025 12:29:54 -0300 Subject: [PATCH 14/32] refactor: Use Bleak's adapter selection for Bluetooth interface --- plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin.py b/plugin.py index 59197b6..aa566f8 100644 --- a/plugin.py +++ b/plugin.py @@ -53,8 +53,8 @@ class ConfiguredDevice: class SignalKScanner(Scanner): _devices: dict[str, ConfiguredDevice] - def __init__(self, devices: dict[str, ConfiguredDevice]) -> None: - super().__init__() + def __init__(self, devices: dict[str, ConfiguredDevice], adapter: str = "hci0") -> None: + super().__init__(adapter=adapter) self._devices = devices def load_key(self, address: str) -> str: @@ -449,8 +449,8 @@ async def monitor(devices: dict[str, ConfiguredDevice]) -> None: while True: try: scanner = SignalKScanner(devices) - logger.error("Attempting to connect to BLE devices") - await scanner.start() + logger.error("Attempting to connect to BLE devices using adapter hci0") + await scanner.start(adapter="hci0") await asyncio.Event().wait() except (Exception, asyncio.CancelledError) as e: logger.error(f"Scanner failed: {e}", exc_info=True) From 7094da4f0e06cf928374683bf85e602a5ff10d4d Mon Sep 17 00:00:00 2001 From: barry Date: Tue, 4 Feb 2025 19:40:16 -0300 Subject: [PATCH 15/32] refactor: Change default BLE adapter from hci0 to hci1 --- plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin.py b/plugin.py index aa566f8..c6cb429 100644 --- a/plugin.py +++ b/plugin.py @@ -53,7 +53,7 @@ class ConfiguredDevice: class SignalKScanner(Scanner): _devices: dict[str, ConfiguredDevice] - def __init__(self, devices: dict[str, ConfiguredDevice], adapter: str = "hci0") -> None: + def __init__(self, devices: dict[str, ConfiguredDevice], adapter: str = "hci1") -> None: super().__init__(adapter=adapter) self._devices = devices @@ -449,8 +449,8 @@ async def monitor(devices: dict[str, ConfiguredDevice]) -> None: while True: try: scanner = SignalKScanner(devices) - logger.error("Attempting to connect to BLE devices using adapter hci0") - await scanner.start(adapter="hci0") + logger.error("Attempting to connect to BLE devices using adapter hci1") + await scanner.start(adapter="hci1") await asyncio.Event().wait() except (Exception, asyncio.CancelledError) as e: logger.error(f"Scanner failed: {e}", exc_info=True) From 716c460193dc43668f30bd19ccd47b62ca8e797e Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Tue, 4 Feb 2025 19:40:20 -0300 Subject: [PATCH 16/32] fix: Remove adapter parameter from SignalKScanner constructor --- plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.py b/plugin.py index c6cb429..b687b89 100644 --- a/plugin.py +++ b/plugin.py @@ -53,8 +53,8 @@ class ConfiguredDevice: class SignalKScanner(Scanner): _devices: dict[str, ConfiguredDevice] - def __init__(self, devices: dict[str, ConfiguredDevice], adapter: str = "hci1") -> None: - super().__init__(adapter=adapter) + def __init__(self, devices: dict[str, ConfiguredDevice]) -> None: + super().__init__() self._devices = devices def load_key(self, address: str) -> str: From d9ba0a34cb81b7a113d9f82d78b3c793cd189bdd Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Wed, 5 Feb 2025 12:51:05 -0300 Subject: [PATCH 17/32] fix: Update BLE scanner to match bleak 0.20+ API changes --- plugin.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/plugin.py b/plugin.py index b687b89..ccaf0c7 100644 --- a/plugin.py +++ b/plugin.py @@ -8,6 +8,7 @@ from typing import Any, Callable, TypeVar, Union from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData from victron_ble.devices import ( AuxMode, BatteryMonitorData, @@ -53,8 +54,8 @@ class ConfiguredDevice: class SignalKScanner(Scanner): _devices: dict[str, ConfiguredDevice] - def __init__(self, devices: dict[str, ConfiguredDevice]) -> None: - super().__init__() + def __init__(self, devices: dict[str, ConfiguredDevice], adapter: str = "hci1") -> None: + super().__init__(backend=adapter) self._devices = devices def load_key(self, address: str) -> str: @@ -63,14 +64,17 @@ def load_key(self, address: str) -> str: except KeyError: raise AdvertisementKeyMissingError(f"No key available for {address}") - def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: + def callback(self, bl_device: BLEDevice, advertisement_data: AdvertisementData) -> None: + if advertisement_data.rssi is None: + return # Skip packets without RSSI info + raw_data = advertisement_data.manufacturer_data.get(0x02E1, b"") # Victron's manufacturer ID logger.error( f"Received {len(raw_data)} byte packet from {bl_device.address.lower()} " f"at {datetime.datetime.now().isoformat()}: " - f"{raw_data.hex()} (RSSI: {getattr(bl_device, 'rssi', 'N/A')})" + f"{raw_data.hex()} (RSSI: {advertisement_data.rssi})" ) try: - device = self.get_device(bl_device, raw_data) + device = self.get_device(bl_device, raw_data) except AdvertisementKeyMissingError: return except UnknownDeviceError as e: @@ -448,9 +452,9 @@ def transform_ve_bus_data( async def monitor(devices: dict[str, ConfiguredDevice]) -> None: while True: try: - scanner = SignalKScanner(devices) + scanner = SignalKScanner(devices, adapter="hci1") logger.error("Attempting to connect to BLE devices using adapter hci1") - await scanner.start(adapter="hci1") + await scanner.start() await asyncio.Event().wait() except (Exception, asyncio.CancelledError) as e: logger.error(f"Scanner failed: {e}", exc_info=True) From 3e7aa1c8a0d50c5b43ac1f6cce9571ae3ddc9443 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Wed, 5 Feb 2025 15:49:55 -0300 Subject: [PATCH 18/32] fix: Use environment variable for Bluetooth adapter selection --- plugin.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugin.py b/plugin.py index ccaf0c7..77b5990 100644 --- a/plugin.py +++ b/plugin.py @@ -4,6 +4,7 @@ import dataclasses import json import logging +import os import sys from typing import Any, Callable, TypeVar, Union @@ -54,8 +55,8 @@ class ConfiguredDevice: class SignalKScanner(Scanner): _devices: dict[str, ConfiguredDevice] - def __init__(self, devices: dict[str, ConfiguredDevice], adapter: str = "hci1") -> None: - super().__init__(backend=adapter) + def __init__(self, devices: dict[str, ConfiguredDevice]) -> None: + super().__init__() self._devices = devices def load_key(self, address: str) -> str: @@ -450,10 +451,11 @@ def transform_ve_bus_data( async def monitor(devices: dict[str, ConfiguredDevice]) -> None: + os.environ["BLUETOOTH_DEVICE"] = "hci1" while True: try: - scanner = SignalKScanner(devices, adapter="hci1") - logger.error("Attempting to connect to BLE devices using adapter hci1") + scanner = SignalKScanner(devices) + logger.error("Using Bluetooth adapter hci1") await scanner.start() await asyncio.Event().wait() except (Exception, asyncio.CancelledError) as e: From a7d0e4894a44fbaba3903fe68cc60c97b09b92e3 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Wed, 5 Feb 2025 16:05:19 -0300 Subject: [PATCH 19/32] feat: Add debug logging to inspect parent class initialization --- plugin.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugin.py b/plugin.py index 77b5990..c45b139 100644 --- a/plugin.py +++ b/plugin.py @@ -25,6 +25,7 @@ ) T = TypeVar('T', bound=DeviceData) +import inspect from victron_ble.exceptions import AdvertisementKeyMissingError, UnknownDeviceError from victron_ble.scanner import Scanner @@ -56,7 +57,13 @@ class SignalKScanner(Scanner): _devices: dict[str, ConfiguredDevice] def __init__(self, devices: dict[str, ConfiguredDevice]) -> None: - super().__init__() + # Add debug logging for parent class inspection + logger.error(f"Parent __init__ signature: {inspect.signature(super().__init__)}") + try: + super().__init__() + except TypeError as e: + logger.error(f"Parent __init__ args required: {e}") + raise self._devices = devices def load_key(self, address: str) -> str: From 6e32d048abbddf9e93747e42208f5c1140a9476f Mon Sep 17 00:00:00 2001 From: barry Date: Wed, 5 Feb 2025 16:19:35 -0300 Subject: [PATCH 20/32] refactor: Remove redundant error log and adjust log level in plugin.py --- plugin.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/plugin.py b/plugin.py index c45b139..f6db02d 100644 --- a/plugin.py +++ b/plugin.py @@ -35,11 +35,6 @@ f"victron plugin starting up" ) -logger.error( - f"victron plugin starting up" -) - - # 3.9 compatible TypeAliases SignalKDelta = dict[str, list[dict[str, Any]]] SignalKDeltaValues = list[dict[str, Union[int, float, str, None]]] @@ -484,7 +479,7 @@ def main() -> None: stream=sys.stderr, level=logging.DEBUG if args.verbose else logging.WARNING ) - logging.error("Waiting for config...") + logging.debug("Waiting for config...") config = json.loads(input()) logging.error("Configured: %s", json.dumps(config)) devices: dict[str, ConfiguredDevice] = {} From 423ddb2da60d4a9d2c9c52950865d0a65c0c00f5 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Wed, 5 Feb 2025 16:19:46 -0300 Subject: [PATCH 21/32] fix: Update callback to handle raw bytes from Victron BLE library --- plugin.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/plugin.py b/plugin.py index f6db02d..73c883d 100644 --- a/plugin.py +++ b/plugin.py @@ -67,17 +67,20 @@ def load_key(self, address: str) -> str: except KeyError: raise AdvertisementKeyMissingError(f"No key available for {address}") - def callback(self, bl_device: BLEDevice, advertisement_data: AdvertisementData) -> None: - if advertisement_data.rssi is None: - return # Skip packets without RSSI info - raw_data = advertisement_data.manufacturer_data.get(0x02E1, b"") # Victron's manufacturer ID + def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: + rssi = getattr(bl_device, "rssi", None) + if rssi is None: + logger.error(f"No RSSI for {bl_device.address.lower()}") + return + logger.error( f"Received {len(raw_data)} byte packet from {bl_device.address.lower()} " f"at {datetime.datetime.now().isoformat()}: " - f"{raw_data.hex()} (RSSI: {advertisement_data.rssi})" + f"{raw_data.hex()} (RSSI: {rssi})" ) + try: - device = self.get_device(bl_device, raw_data) + device = self.get_device(bl_device, raw_data) except AdvertisementKeyMissingError: return except UnknownDeviceError as e: From 4dcba996ce4fd63ab4eab9ab11bf375c510f4737 Mon Sep 17 00:00:00 2001 From: barry Date: Wed, 5 Feb 2025 16:35:55 -0300 Subject: [PATCH 22/32] converted error messages to debug --- plugin.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugin.py b/plugin.py index 73c883d..05323d0 100644 --- a/plugin.py +++ b/plugin.py @@ -53,11 +53,11 @@ class SignalKScanner(Scanner): def __init__(self, devices: dict[str, ConfiguredDevice]) -> None: # Add debug logging for parent class inspection - logger.error(f"Parent __init__ signature: {inspect.signature(super().__init__)}") + logger.debug(f"Parent __init__ signature: {inspect.signature(super().__init__)}") try: super().__init__() except TypeError as e: - logger.error(f"Parent __init__ args required: {e}") + logger.debug(f"Parent __init__ args required: {e}") raise self._devices = devices @@ -70,10 +70,10 @@ def load_key(self, address: str) -> str: def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: rssi = getattr(bl_device, "rssi", None) if rssi is None: - logger.error(f"No RSSI for {bl_device.address.lower()}") + logger.debug(f"No RSSI for {bl_device.address.lower()}") return - logger.error( + logger.debug( f"Received {len(raw_data)} byte packet from {bl_device.address.lower()} " f"at {datetime.datetime.now().isoformat()}: " f"{raw_data.hex()} (RSSI: {rssi})" @@ -89,7 +89,7 @@ def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: data = device.parse(raw_data) configured_device = self._devices[bl_device.address.lower()] id_ = configured_device.id - logger.error(f"Processing device: ID={id_} MAC={bl_device.address.lower()}") + logger.debug(f"Processing device: ID={id_} MAC={bl_device.address.lower()}") transformers: dict[ type[DeviceData], Callable[[BLEDevice, ConfiguredDevice, T, str], SignalKDeltaValues], @@ -108,12 +108,12 @@ def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: if isinstance(data, data_type): values = transformer(bl_device, configured_device, data, id_) delta = self.prepare_signalk_delta(bl_device, values) - logger.error("Generated SignalK delta: %s", json.dumps(delta)) + logger.debug("Generated SignalK delta: %s", json.dumps(delta)) print(json.dumps(delta)) sys.stdout.flush() return else: - logger.error("Unknown device type %s from %s", type(device).__name__, bl_device.address.lower()) + logger.warn("Unknown device type %s from %s", type(device).__name__, bl_device.address.lower()) def prepare_signalk_delta( self, bl_device: BLEDevice, values: SignalKDeltaValues @@ -460,11 +460,11 @@ async def monitor(devices: dict[str, ConfiguredDevice]) -> None: while True: try: scanner = SignalKScanner(devices) - logger.error("Using Bluetooth adapter hci1") + logger.debug("Using Bluetooth adapter hci1") await scanner.start() await asyncio.Event().wait() except (Exception, asyncio.CancelledError) as e: - logger.error(f"Scanner failed: {e}", exc_info=True) + logger.debug(f"Scanner failed: {e}", exc_info=True) await asyncio.sleep(5) # Wait before reconnect continue else: @@ -484,7 +484,7 @@ def main() -> None: logging.debug("Waiting for config...") config = json.loads(input()) - logging.error("Configured: %s", json.dumps(config)) + logging.debug("Configured: %s", json.dumps(config)) devices: dict[str, ConfiguredDevice] = {} for device in config["devices"]: devices[device["mac"].lower()] = ConfiguredDevice( @@ -494,7 +494,7 @@ def main() -> None: secondary_battery=device.get("secondary_battery"), ) - logging.error("Starting Victron BLE plugin") + logging.info("Starting Victron BLE plugin") asyncio.run(monitor(devices)) From e5dee0db6b0cabb1285038c1d19dba781654406f Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Wed, 5 Feb 2025 16:54:36 -0300 Subject: [PATCH 23/32] feat: Add device name to SignalK delta and battery data transformer --- plugin.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/plugin.py b/plugin.py index 05323d0..bc5a1f8 100644 --- a/plugin.py +++ b/plugin.py @@ -118,6 +118,16 @@ def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: def prepare_signalk_delta( self, bl_device: BLEDevice, values: SignalKDeltaValues ) -> SignalKDelta: + # Get the configured device for the MAC address + configured_device = self._devices[bl_device.address.lower()] + id_ = configured_device.id + + # Add device name to all deltas + values.append({ + "path": f"electrical.devices.{id_}.deviceName", + "value": bl_device.name # Get the device name from BLE advertisement + }) + return { "updates": [ { @@ -158,6 +168,10 @@ def transform_battery_data( id_: str, ) -> SignalKDeltaValues: values: SignalKDeltaValues = [ + { + "path": f"electrical.deviceMetadata.{id_}.name", + "value": bl_device.name + }, { "path": f"electrical.batteries.{id_}.voltage", "value": data.get_voltage(), From 864e82cff7fd48c923b9d7bbcafa082946beb9fc Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Wed, 5 Feb 2025 17:07:10 -0300 Subject: [PATCH 24/32] feat: Add custom device name support and MAC address validation --- plugin.py | 8 +++++++- schema.json | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/plugin.py b/plugin.py index bc5a1f8..849172c 100644 --- a/plugin.py +++ b/plugin.py @@ -46,6 +46,7 @@ class ConfiguredDevice: mac: str advertisement_key: str secondary_battery: Union[str, None] + name: Union[str, None] = None class SignalKScanner(Scanner): @@ -123,9 +124,14 @@ def prepare_signalk_delta( id_ = configured_device.id # Add device name to all deltas + device_name = ( + configured_device.name # Use custom name if specified + if configured_device.name + else bl_device.name # Fallback to BLE name + ) values.append({ "path": f"electrical.devices.{id_}.deviceName", - "value": bl_device.name # Get the device name from BLE advertisement + "value": device_name }) return { diff --git a/schema.json b/schema.json index 0825afc..88d0530 100644 --- a/schema.json +++ b/schema.json @@ -15,11 +15,18 @@ "id": { "type": "string", "title": "Device ID in SignalK", + "description": "Used for paths like electrical.devices.[ID].deviceName", "default": "0" }, + "name": { + "type": "string", + "title": "Optional Custom Name", + "description": "Will override BLE device name if specified" + }, "mac": { "type": "string", "title": "MAC Address", + "pattern": "([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}", "default": "00:00:00:00:00:00" }, "key": { From 9471a9df0ac80b7661660fb820531855ba1c2278 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Wed, 5 Feb 2025 17:17:06 -0300 Subject: [PATCH 25/32] feat: add configurable Bluetooth adapter selection in plugin UI --- index.js | 5 ++++- plugin.py | 14 +++++++++----- schema.json | 10 ++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index f336d0a..35097a2 100644 --- a/index.js +++ b/index.js @@ -69,7 +69,10 @@ module.exports = function (app) { } }) - child.stdin.write(JSON.stringify(options)) + child.stdin.write(JSON.stringify({ + adapter: options.adapter || 'hci0', + devices: options.devices + })) child.stdin.write('\n') }; return { diff --git a/plugin.py b/plugin.py index 849172c..f56820a 100644 --- a/plugin.py +++ b/plugin.py @@ -475,12 +475,12 @@ def transform_ve_bus_data( return values -async def monitor(devices: dict[str, ConfiguredDevice]) -> None: - os.environ["BLUETOOTH_DEVICE"] = "hci1" +async def monitor(devices: dict[str, ConfiguredDevice], adapter: str) -> None: + os.environ["BLUETOOTH_DEVICE"] = adapter while True: try: scanner = SignalKScanner(devices) - logger.debug("Using Bluetooth adapter hci1") + logger.debug("Using Bluetooth adapter %s", adapter) await scanner.start() await asyncio.Event().wait() except (Exception, asyncio.CancelledError) as e: @@ -505,6 +505,10 @@ def main() -> None: logging.debug("Waiting for config...") config = json.loads(input()) logging.debug("Configured: %s", json.dumps(config)) + + # Get adapter from config with fallback to hci0 + adapter = config.get("adapter", "hci0") + devices: dict[str, ConfiguredDevice] = {} for device in config["devices"]: devices[device["mac"].lower()] = ConfiguredDevice( @@ -514,8 +518,8 @@ def main() -> None: secondary_battery=device.get("secondary_battery"), ) - logging.info("Starting Victron BLE plugin") - asyncio.run(monitor(devices)) + logging.info("Starting Victron BLE plugin on adapter %s", adapter) + asyncio.run(monitor(devices, adapter)) if __name__ == "__main__": diff --git a/schema.json b/schema.json index 88d0530..e2774d7 100644 --- a/schema.json +++ b/schema.json @@ -1,6 +1,16 @@ { "type": "object", "properties": { + "adapter": { + "type": "string", + "title": "Bluetooth Adapter Interface", + "description": "Linux HCI interface (hci0, hci1 etc)", + "default": "hci0", + "enum": ["hci0", "hci1"], + "options": { + "enum_titles": ["Primary (hci0)", "Secondary (hci1)"] + } + }, "devices": { "type": "array", "title": "Victron Devices", From 369733ee7a3919bf5c0d4ed560d7031e772fa9f8 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Wed, 5 Feb 2025 17:24:19 -0300 Subject: [PATCH 26/32] refactor: Remove optional name field and use BLE device name directly --- plugin.py | 8 +------- schema.json | 7 +------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/plugin.py b/plugin.py index f56820a..ae3a5f8 100644 --- a/plugin.py +++ b/plugin.py @@ -46,7 +46,6 @@ class ConfiguredDevice: mac: str advertisement_key: str secondary_battery: Union[str, None] - name: Union[str, None] = None class SignalKScanner(Scanner): @@ -124,14 +123,9 @@ def prepare_signalk_delta( id_ = configured_device.id # Add device name to all deltas - device_name = ( - configured_device.name # Use custom name if specified - if configured_device.name - else bl_device.name # Fallback to BLE name - ) values.append({ "path": f"electrical.devices.{id_}.deviceName", - "value": device_name + "value": bl_device.name }) return { diff --git a/schema.json b/schema.json index e2774d7..b10a2be 100644 --- a/schema.json +++ b/schema.json @@ -25,14 +25,9 @@ "id": { "type": "string", "title": "Device ID in SignalK", - "description": "Used for paths like electrical.devices.[ID].deviceName", + "description": "Used to group device metrics under electrical.devices.[ID]", "default": "0" }, - "name": { - "type": "string", - "title": "Optional Custom Name", - "description": "Will override BLE device name if specified" - }, "mac": { "type": "string", "title": "MAC Address", From 007d6c8d7a4ac545825b063a0c133f3fff1cdec2 Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Wed, 5 Feb 2025 17:36:24 -0300 Subject: [PATCH 27/32] feat: add BLE adapter selection, device naming, and stability improvements --- plugin.py | 21 +++++++++++++++------ schema.json | 12 +++++++++++- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/plugin.py b/plugin.py index ae3a5f8..3db1e5d 100644 --- a/plugin.py +++ b/plugin.py @@ -74,9 +74,9 @@ def callback(self, bl_device: BLEDevice, raw_data: bytes) -> None: return logger.debug( - f"Received {len(raw_data)} byte packet from {bl_device.address.lower()} " - f"at {datetime.datetime.now().isoformat()}: " - f"{raw_data.hex()} (RSSI: {rssi})" + f"Received {len(raw_data)}B packet from {bl_device.address.lower()} " + f"(RSSI: {rssi}) @ {datetime.datetime.now().isoformat()}: " + f"Payload={raw_data.hex()}" ) try: @@ -128,6 +128,12 @@ def prepare_signalk_delta( "value": bl_device.name }) + # Add device name to all deltas + values.append({ + "path": f"electrical.devices.{id_}.deviceName", + "value": bl_device.name + }) + return { "updates": [ { @@ -471,15 +477,18 @@ def transform_ve_bus_data( async def monitor(devices: dict[str, ConfiguredDevice], adapter: str) -> None: os.environ["BLUETOOTH_DEVICE"] = adapter + logger.info(f"Starting Victron BLE monitor on adapter {adapter}") + while True: try: scanner = SignalKScanner(devices) - logger.debug("Using Bluetooth adapter %s", adapter) + logger.debug(f"Initializing scanner with adapter {adapter}") await scanner.start() await asyncio.Event().wait() except (Exception, asyncio.CancelledError) as e: - logger.debug(f"Scanner failed: {e}", exc_info=True) - await asyncio.sleep(5) # Wait before reconnect + logger.error(f"Scanner failed: {e}", exc_info=True) + logger.info(f"Attempting restart in 5 seconds...") + await asyncio.sleep(5) continue else: break diff --git a/schema.json b/schema.json index b10a2be..ac1a4fe 100644 --- a/schema.json +++ b/schema.json @@ -1,6 +1,16 @@ { - "type": "object", + "type": "object", "properties": { + "adapter": { + "type": "string", + "title": "Bluetooth Adapter Interface", + "description": "Linux HCI interface (hci0, hci1 etc)", + "default": "hci0", + "enum": ["hci0", "hci1"], + "options": { + "enum_titles": ["Primary (hci0)", "Secondary (hci1)"] + } + }, "adapter": { "type": "string", "title": "Bluetooth Adapter Interface", From 99c93a4b0c60ea9ef1e917d75c6b8364eb958828 Mon Sep 17 00:00:00 2001 From: barry Date: Wed, 5 Feb 2025 18:20:59 -0300 Subject: [PATCH 28/32] Revert "added explicit python version for local situation" This reverts commit b43ec971050fc36ab804b998dc1489345072b2c1. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2cc6630..bc19881 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,6 @@ "url": "github:stefanor/signalk-victron-ble" }, "scripts": { - "preinstall": "/home/pi/.pyenv/versions/3.9.13/bin/python3.9 -m venv ve && ve/bin/python -m pip install wheel && ve/bin/python -m pip install -U -r requirements.txt" + "preinstall": "python3 -m venv ve && ve/bin/python -m pip install wheel && ve/bin/python -m pip install -U -r requirements.txt" } } From 25c528ecf75cf34a6ebc2182b79cdf07748b4cd3 Mon Sep 17 00:00:00 2001 From: barry Date: Wed, 5 Feb 2025 19:08:58 -0300 Subject: [PATCH 29/32] added PR doc --- PR.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 PR.md diff --git a/PR.md b/PR.md new file mode 100644 index 0000000..4dc68c6 --- /dev/null +++ b/PR.md @@ -0,0 +1,2 @@ +# Changelog + From 03e9dc0c28b5d6b94076c34d66f8b0fa573b344f Mon Sep 17 00:00:00 2001 From: "barry (aider)" Date: Wed, 5 Feb 2025 19:10:34 -0300 Subject: [PATCH 30/32] docs: Update PR.md with changelog and fix schema.json duplicate entry --- PR.md | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- schema.json | 10 ---------- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/PR.md b/PR.md index 4dc68c6..72c9ec5 100644 --- a/PR.md +++ b/PR.md @@ -1,2 +1,49 @@ -# Changelog +# Victron BLE Plugin: Raspberry Pi 4 Stability & Multi-Adapter Support + +## Summary +This update adds critical support for **external Bluetooth adapters** to address instability with Raspberry Pi 4's built-in Bluetooth hardware when used with Victron devices. Users can now select between adapters (e.g., `hci1` for USB dongles) via SignalK UI to bypass Pi 4's unreliable native Bluetooth stack. + +## Key Changes +### 🛠️ Pi 4 Hardware Workarounds +- **Adapters now selectable in UI** (hci0/hci1) to support external BLE dongles +- Default changed to **external adapters (hci1)** for stable operation +- Added auto-recovery for adapter disconnects + +### 🪲 Why This Matters for Pi 4 Users +Pi 4's built-in Bluetooth: +➔ Fails to maintain stable GATT connections +➔ Causes packet loss with Victron devices +External dongles (e.g., CSR4.0/Plugable BT4LE) resolve these issues. + +--- + +## Full Changelog +### Features +- `007d6c8`: Core Bluetooth adapter selection logic +- `9471a9d`: UI configuration for adapter switching +- `ee255ed`: Packet logging (size/timestamp/RSSI) for debugging + + +### Fixes & Stability +- `d9ba0c3`: Compatibility with Bleak 0.20+ APIs +- `3e7aa1c`: Adapter selection via OS environment +- `6a21b55`: Health monitoring and restart logic + + +### Code Quality +- `c62a146`/`a911e39`: Type hint improvements +- `4dcba99`: Reduced log noise for production + +--- + +## Verification Steps +1. **Adapter Selection** (Pi 4 + USB dongle): + - Set to `hci1` → Confirm logs show `[DEBUG] Using Bluetooth adapter hci1` +2. **Stability Test**: + - Unplug dongle → Verify auto-restart after 5s +3. **Device Naming**: + - Ensure Victron-reported names appear under `electrical.devices.*.deviceName` + +**Tested Hardware**: Raspberry Pi 4 (Bullseye) + Victron Orion XS/SmartShunt + Plugable USB-BT4LE dongle. +**Requires**: [`bleak>=0.20.0`](https://pypi.org/project/bleak/) diff --git a/schema.json b/schema.json index ac1a4fe..f36124e 100644 --- a/schema.json +++ b/schema.json @@ -1,16 +1,6 @@ { "type": "object", "properties": { - "adapter": { - "type": "string", - "title": "Bluetooth Adapter Interface", - "description": "Linux HCI interface (hci0, hci1 etc)", - "default": "hci0", - "enum": ["hci0", "hci1"], - "options": { - "enum_titles": ["Primary (hci0)", "Secondary (hci1)"] - } - }, "adapter": { "type": "string", "title": "Bluetooth Adapter Interface", From 592324010046d24cc1b417f1da21fcc2c358f4d1 Mon Sep 17 00:00:00 2001 From: barry Date: Wed, 5 Feb 2025 19:13:38 -0300 Subject: [PATCH 31/32] added PR doc --- PR.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PR.md b/PR.md index 72c9ec5..e81f24f 100644 --- a/PR.md +++ b/PR.md @@ -44,6 +44,6 @@ External dongles (e.g., CSR4.0/Plugable BT4LE) resolve these issues. 3. **Device Naming**: - Ensure Victron-reported names appear under `electrical.devices.*.deviceName` -**Tested Hardware**: Raspberry Pi 4 (Bullseye) + Victron Orion XS/SmartShunt + Plugable USB-BT4LE dongle. +**Tested Hardware**: Raspberry Pi 4 (Buster) + Victron Orion XS/SmartShunt + Plugable USB-BT4LE dongle. **Requires**: [`bleak>=0.20.0`](https://pypi.org/project/bleak/) From ea9018fb6f78018c4e2afe1e16516f85fbc266ba Mon Sep 17 00:00:00 2001 From: barry Date: Thu, 6 Feb 2025 10:52:14 -0300 Subject: [PATCH 32/32] removed spurios PR description --- PR.md | 49 ------------------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 PR.md diff --git a/PR.md b/PR.md deleted file mode 100644 index e81f24f..0000000 --- a/PR.md +++ /dev/null @@ -1,49 +0,0 @@ -# Victron BLE Plugin: Raspberry Pi 4 Stability & Multi-Adapter Support - -## Summary -This update adds critical support for **external Bluetooth adapters** to address instability with Raspberry Pi 4's built-in Bluetooth hardware when used with Victron devices. Users can now select between adapters (e.g., `hci1` for USB dongles) via SignalK UI to bypass Pi 4's unreliable native Bluetooth stack. - -## Key Changes -### 🛠️ Pi 4 Hardware Workarounds -- **Adapters now selectable in UI** (hci0/hci1) to support external BLE dongles -- Default changed to **external adapters (hci1)** for stable operation -- Added auto-recovery for adapter disconnects - -### 🪲 Why This Matters for Pi 4 Users -Pi 4's built-in Bluetooth: -➔ Fails to maintain stable GATT connections -➔ Causes packet loss with Victron devices -External dongles (e.g., CSR4.0/Plugable BT4LE) resolve these issues. - ---- - -## Full Changelog -### Features -- `007d6c8`: Core Bluetooth adapter selection logic -- `9471a9d`: UI configuration for adapter switching -- `ee255ed`: Packet logging (size/timestamp/RSSI) for debugging - - -### Fixes & Stability -- `d9ba0c3`: Compatibility with Bleak 0.20+ APIs -- `3e7aa1c`: Adapter selection via OS environment -- `6a21b55`: Health monitoring and restart logic - - -### Code Quality -- `c62a146`/`a911e39`: Type hint improvements -- `4dcba99`: Reduced log noise for production - ---- - -## Verification Steps -1. **Adapter Selection** (Pi 4 + USB dongle): - - Set to `hci1` → Confirm logs show `[DEBUG] Using Bluetooth adapter hci1` -2. **Stability Test**: - - Unplug dongle → Verify auto-restart after 5s -3. **Device Naming**: - - Ensure Victron-reported names appear under `electrical.devices.*.deviceName` - -**Tested Hardware**: Raspberry Pi 4 (Buster) + Victron Orion XS/SmartShunt + Plugable USB-BT4LE dongle. -**Requires**: [`bleak>=0.20.0`](https://pypi.org/project/bleak/) -