From 9eb464790e4706ae3465127335a5fa1471e26039 Mon Sep 17 00:00:00 2001 From: Manuel Date: Sat, 13 May 2023 09:48:58 +0200 Subject: [PATCH 1/4] changed jkbms_ble to dev (#631) * changed jkbms_ble to dev * changed order * test automatic release version change * test automatic release version change * test automatic release version change * added option in bug issue template --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + .github/workflows/release-beta.yml | 10 ++++++++++ .github/workflows/release.yml | 10 ++++++++++ etc/dbus-serialbattery/install.sh | 24 +++++++++++------------- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index df191642..59070773 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -119,6 +119,7 @@ body: - Please select - Serial USB adapter to TTL - Serial USB adapter to RS485 + - Raspberry Pi RS485 HAT - Bluetooth validations: required: true diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index f5a4ed4a..f1634f34 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -17,6 +17,16 @@ jobs: - name: Checkout uses: actions/checkout@v3 + - name: Replace version string + run: | + echo $GITHUB_REF_NAME + VERSION=$(echo $GITHUB_REF_NAME | sed 's/^v//') + echo $VERSION + echo + head -n 39 etc/dbus-serialbattery/utils.py | tail -n 3 + sed -i --regexp-extended --expression="s/[0-9]+\.[0-9]+\.[0-9a-z\_\-]+/$VERSION/" "etc/dbus-serialbattery/utils.py" + head -n 39 etc/dbus-serialbattery/utils.py | tail -n 3 + - name: build release archive run: | find . -type f -name "*.py" -exec chmod +x {} \; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0da358dd..55403a70 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,16 @@ jobs: - name: Checkout uses: actions/checkout@v3 + - name: Replace version string + run: | + echo $GITHUB_REF_NAME + VERSION=$(echo $GITHUB_REF_NAME | sed 's/^v//') + echo $VERSION + echo + head -n 39 etc/dbus-serialbattery/utils.py | tail -n 3 + sed -i --regexp-extended --expression="s/[0-9]+\.[0-9]+\.[0-9a-z\_\-]+/$VERSION/" "etc/dbus-serialbattery/utils.py" + head -n 39 etc/dbus-serialbattery/utils.py | tail -n 3 + - name: build release archive run: | find . -type f -name "*.py" -exec chmod +x {} \; diff --git a/etc/dbus-serialbattery/install.sh b/etc/dbus-serialbattery/install.sh index 57e09e6a..17a12ed3 100755 --- a/etc/dbus-serialbattery/install.sh +++ b/etc/dbus-serialbattery/install.sh @@ -6,7 +6,7 @@ echo "" PS3="Select which version you want to install and enter the corresponding number [1]: " -select version in "latest release (recommended)" "nightly build" "local tar file" "specific version" "quit" +select version in "latest release (recommended)" "specific version" "nightly build" "local tar file" "quit" do case $version in "latest release (recommended)") @@ -14,17 +14,17 @@ do #echo "Selected number: $REPLY" break ;; - "nightly build") + "specific version") echo "Selected: $version" #echo "Selected number: $REPLY" break ;; - "local tar file") + "nightly build") echo "Selected: $version" #echo "Selected number: $REPLY" break ;; - "specific version") + "local tar file") echo "Selected: $version" #echo "Selected number: $REPLY" break @@ -46,11 +46,6 @@ if [ "$version" = "latest release (recommended)" ]; then curl -s https://api.github.com/repos/Louisvdw/dbus-serialbattery/releases/latest | grep "browser_download_url.*gz" | cut -d : -f 2,3 | tr -d \" | wget -O /tmp/venus-data.tar.gz -qi - fi -## local tar file -if [ "$version" = "local tar file" ]; then - echo "Make sure the file is available at \"/var/volatile/tmp/venus-data.tar.gz\"" -fi - ## specific version if [ "$version" = "specific version" ]; then # read the url @@ -62,7 +57,10 @@ if [ "$version" = "specific version" ]; then fi fi - +## local tar file +if [ "$version" = "local tar file" ]; then + echo "Make sure the file is available at \"/var/volatile/tmp/venus-data.tar.gz\"" +fi # backup config.ini if [ -f "/data/etc/dbus-serialbattery/config.ini" ]; then @@ -72,7 +70,7 @@ fi ## extract the tar file -if [ "$version" = "latest release (recommended)" ] || [ "$version" = "local tar file" ] || [ "$version" = "specific version" ]; then +if [ "$version" = "latest release (recommended)" ] || [ "$version" = "specific version" ] || [ "$version" = "local tar file" ]; then # extract driver if [ -f "/tmp/venus-data.tar.gz" ]; then @@ -96,7 +94,7 @@ if [ "$version" = "nightly build" ]; then PS3="Select the branch from wich you want to install the current code (possible bugs included): " - select branch in master jkbms_ble quit + select branch in master dev quit do case $branch in master) @@ -104,7 +102,7 @@ if [ "$version" = "nightly build" ]; then #echo "Selected number: $REPLY" break ;; - jkbms_ble) + dev) echo "Selected branch: $branch" #echo "Selected number: $REPLY" break From 6a1e6b6a18eeed9c9638ca99eca87ad0fc036dbe Mon Sep 17 00:00:00 2001 From: Manuel Date: Fri, 26 May 2023 10:05:17 +0200 Subject: [PATCH 2/4] updated readme and added donation link (#671) --- README.md | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e2989624..063eaaa3 100644 --- a/README.md +++ b/README.md @@ -3,23 +3,38 @@ This is a driver for Venus OS devices (any GX device sold by Victron or a Raspbe The driver will communicate with a Battery Management System (BMS) that support serial communication (RS232, RS485 or TTL UART) and publish this data to the Venus OS system. The main purpose is to act as a Battery Monitor in your GX and supply State Of Charge (SOC) and other values to the inverter. - * [BMS Types supported](https://github.com/Louisvdw/dbus-serialbattery/wiki/BMS-types-supported) - * [FAQ](https://github.com/Louisvdw/dbus-serialbattery/wiki/FAQ) - * [Features](https://github.com/Louisvdw/dbus-serialbattery/wiki/Features) - * [How to install](https://github.com/Louisvdw/dbus-serialbattery/wiki/How-to-install) - * [Troubleshoot](https://github.com/Louisvdw/dbus-serialbattery/wiki/Troubleshoot) + * [BMS Types supported](https://louisvdw.github.io/dbus-serialbattery/general/supported-bms) + * [FAQ](https://louisvdw.github.io/dbus-serialbattery/faq/) + * [Features](https://louisvdw.github.io/dbus-serialbattery/general/features) + * [How to install](https://louisvdw.github.io/dbus-serialbattery/general/install) + * [How to troubleshoot](https://louisvdw.github.io/dbus-serialbattery/troubleshoot/) -### Supporting this project: +### Supporting this project If you find this driver helpful please considder supporting this project. You can buy me a Ko-Fi or get in contact if you would like to donate hardware. +### Support [Louisvdw](https://github.com/Louisvdw) +* Main developer +* Added most of the BMS drivers + [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Z8Z73LCW1) or using [Paypal.me](https://paypal.me/innernet) +### Support [mr-manuel](https://github.com/mr-manuel) +* Added a lot of features, optimizations and improvements with `v1.0.x` +* Added a lot of documentation to the config file and notes that are displayed after installation for better understanding +* Introduced the new documentation page of the driver and reworked a great part of it for easier understanding + +[](https://www.paypal.com/donate/?hosted_button_id=3NEVZBDM5KABW) + ### Developer Remarks To develop this project, install the requirements. This project makes use of velib_python which is pre-installed on Venus-OS Devices under `/opt/victronenergy/dbus-systemcalc-py/ext/velib_python`. To use the python files locally, `git clone` the [velib_python](https://github.com/victronenergy/velib_python) project to velib_python and add velib_python to the `PYTHONPATH` environment variable. +Make sure the GitHub Actions run fine in your repository. In order to make the GitHub Actions run please select in your repository settings under `Actions` -> `General` -> `Actions permissions` the option `Allow all actions and reusable workflows`. Check also in your repository settings under `Actions` -> `General` -> `Workflow permissions` if `Read and write permissions` are selected. This will check your code for Flake8 and Black Lint errors. [Here](https://py-vscode.readthedocs.io/en/latest/files/linting.html) is a short instruction on how to set up Flake8 and Black Lint checks in VS Code. This will save you a lot of time. + +See this checklist, if you want to [add a new BMS](https://louisvdw.github.io/dbus-serialbattery/general/supported-bms#add-by-opening-a-pull-request) + #### How it works * Each supported BMS needs to implement the abstract base class `Battery` from `battery.py`. * `dbus-serialbattery.py` tries to figure out the correct connected BMS by looping through all known implementations of From 0f84f03b32d3c5d58082c73de48bdad91615de43 Mon Sep 17 00:00:00 2001 From: Manuel Date: Wed, 31 May 2023 18:16:30 +0200 Subject: [PATCH 3/4] Merge dev into master (#678) * Changes 2023.05.18 (#649) * Added: Show specific TimeToSoC points in GUI Only if 0%, 10%, 20%, 80%, 90% and/or 100% are selected * Added: Show specific TimeToSoC points in GUI Only if 0%, 10%, 20%, 80%, 90% and/or 100% are selected * fix black lint error * fix black lint error * Improved JBD BMS soc calculation https://github.com/Louisvdw/dbus-serialbattery/pull/439 * Fix for #397 https://github.com/Louisvdw/dbus-serialbattery/pull/484 * small fixes * sort bms imports * Add support for HLPdata BMS4S https://github.com/Louisvdw/dbus-serialbattery/pull/505 * Add support for Seplos BMS https://github.com/Louisvdw/dbus-serialbattery/pull/530 * change flake8 settings * fix black lint errors * removed wildcard imports * fixed black lint errors * change flake8 settings * remove wildcard import and fix black lint errors * removed wildcard import * fixed black lint check * removed wildcard import, fixed black lint errors * config changes * removed wildcard import, fixed black lint errors * remove old log message in handle_changed_setting() * remove old log message in handle_changed_setting() * simplified condition for Time-To-Go/Soc * simplified condition for Time-To-Go/Soc * fix renogy import * fix renogy import * added BMS info and cleanup * MNB * Revov * Sinowealth * added BMS info and cleanup * MNB * Revov * Sinowealth * moved BMS to subfolder * moved BMS to subfolder * moved BMS to subfolder * corrected installble to run correct script * Added self.unique_identifier to the battery class Used to identify a BMS when multiple BMS are connected planned for future use * Added self.unique_identifier to the battery class Used to identify a BMS when multiple BMS are connected planned for future use * changed ble service name from `dbus-blebattery-$1` to `dbus-blebattery.$1` like the non ble service * fix small errors * read installed capacity at startup * disable ANT BMS by default https://github.com/Louisvdw/dbus-serialbattery/issues/479 * fix cell voltage header parser * rework daly receive routine * improve read cell voltages - only work on sufficient data, drop only the bad package on checksum error, not the complete transmission * allow read_soc to also retry serial transmission * add daly cell balance state info. cells are red only if unbalanced now * bump version * typo * moved read_serialport_data() to daly.py * revert read_serialport_data() to the state before my changes * fix connection log startup message. now voltage/current/soc are displayed correctly * black reformatting * added linear voltage recalculation interval In the config file can now be defined how often CVL, CCL and DCL is recalculated * revert Daly adaption * replaced penalty voltage calculation with automatically calculated penalty voltages to simplify config max voltage is kept until batteries are balanced * flake config change * flake config change * added linear voltage recalculation interval In the config file can now be defined how often CVL, CCL and DCL is recalculated * replaced penalty voltage calculation with automatically calculated penalty voltages to simplify config max voltage is kept until batteries are balanced * fix black lint errors * updated changelog * disabled ANT BMS by default https://github.com/Louisvdw/dbus-serialbattery/issues/479 * updated config.default.ini * fix typo * update nightly install script * Removed line * fixed error in HLPdataBMS4S * fixed wrong variable assignment `str` instead of `int` * fixed wrong variable assignment `str` instead of `int` * updated battery template * updated battery template * Fix for #450 https://github.com/Louisvdw/dbus-serialbattery/issues/450 * Read charge/discharge limit JKBMS https://github.com/Louisvdw/dbus-serialbattery/issues/4 * updated battery template * Progress with config limits reason * updated CELL_VOLTAGE_DIFF_TO_RESET_VOLTAGE_LIMIT default value * added SoC round for LLT/JBD * fixed log typo * updated nightly script * Fix for #450 https://github.com/Louisvdw/dbus-serialbattery/issues/450 * Read charge/discharge limit JKBMS https://github.com/Louisvdw/dbus-serialbattery/issues/4 * reworked installation procedure Bluetooth BMS is now also fetched from config.ini * updated release workflow * updated readme * Merge branch 'master' into jkbms_ble * deploy to github pages only on changes in master or docusaurus branch * cleanup * Merge branch 'master' of into jkbms_ble * GitHub pages config change * GitHub pages config change * cleanup * Renamed scripts for better reading #532 * update docusaurus dependencies * Renamed scripts for better reading #532 * update docusaurus dependencies * change sh with bash * limitation reason cleanup * limitation reason cleanup * changed default config settings FLOAT_CELL_VOLTAGE from 3.350V to 3.375V LINEAR_LIMITATION_ENABLE from False to True * changed default config settings FLOAT_CELL_VOLTAGE from 3.350V to 3.375V LINEAR_LIMITATION_ENABLE from False to True * removed testing line * Cleanup duplicated files Files were moved and not deleted * Cleanup * MOSFET temperature was displayed twice after merge * small typo fixes * updated changelog * updated changelog * Small fixes * fix disconnection behaviour: on disconnect, show '---' after 10s and 'not connected' after 60s * fix flake errors * small fix * fix disconnection behaviour & small fixes * on disconnect, show '---' after 10s and 'not connected' after 60s by @transistorgit * small fixes in shell script * added restart driver script * fixed file permission * Added: apply max voltage if CVCM_ENABLE is False before float voltage was applied * fixed type error * Added: BMS disconnect behaviour * Choose to block charge/discharge on disconnect * Trigger Venus OS alarm * Changed: Remove wildcard import from dbushelper.py * small fixes * Added: apply max voltage if CVCM_ENABLE is False before float voltage was applied * Added: BMS disconnect behaviour * Choose to block charge/discharge on disconnect * Trigger Venus OS alarm * Changed: Remove wildcard import from dbushelper.py * flake8 changes * copied lltjbd_ble from idstein:jdb_ble * Added and adapted LltJbd_Ble ATTENTION: Currently it's untested * small changes * read production date and append to hardware version * Set SOC nightly. Button is working, next is send command to bms * Added: Show additional data in device page * show self.unique_identifier as serial number * show self.production as device name * Added: JKBMS unique identifier & fixed data length * Added: JKBMS BLE unique identifier * Added: Jkbms_Ble connection_name() * Added: Daly unique identifier * Added: JKBMS BLE serial number, user defined field * Added: Show additional data in device page * show self.unique_identifier as serial number * show self.production as device name * Added: JKBMS unique identifier & fixed data length * move config.ini before update * read production date by @tranistorgit this adds the battery production date * read out daly battery code and use as unique id * moved production date and added custom field * clean battery code strip whitespace and replace one or multiple spaces with one underline if no battery code generate unique field * Daly read_capacity change Read capacity from config file, if no value provided by BMS * Daly try to fix no reply * Improvements by @transistorgit * changed value * set SOC (and date time) on button press. * fix battery code parser * format fix * format fix * fix extra long serial timeouts by calculating max time instead of loop counts * Changed: Merged all install files into one * updated install docs for nightly build * Small fixes * changed config backup * updated config file * updated changelog * debug daly * changed release workflow * changed release workflow * changed release workflow * changed release workflow * Updated from master * fix blank screen, debug daly * make Reset SoC a spin box * fix possible read_capacity problem * Daly read_balance_state() add missing return * Daly advanced troubleshooting * Changed: Improved Daily stability by a lot * fixes for disable and uninstall service was not removed and if removed, it was recreated by the serial starter * optimized USB install method * updated changelog * added missing qml to restore-gui.sh * optimized USB install method * Daly improvements * Set SoC on button press by @transistorgit * Improved driver stability by @transistorgit & @mr-manuel * moved production date and added custom field * changed jkbms_ble to dev * changed order * final daly 'broken packages handling' * Last changes for daly read problem by @transistorgit * change version in utils based on GitHub tag * test automatic release version change * Added: Configure voltage drop * test automatic release version change * fix Daly alarms * fixes small errors in bash files * fix missing driver name in restart-driver.sh * linear mode, allow max voltage on soc thesshold * Daly added one retry if failed * fixed LLT/JBD cell balancing display * rename tar after USB install fixes https://github.com/Louisvdw/dbus-serialbattery/issues/638 * add force buttons * force buttons working * fixed removing entries * implement force charge/discharge * little bit cleaner soc preset * use existing serial read/write function * add stop balancong switch * use existing serial read/write function * Changed: Get bg colors from MbStyle for dark mode * prevent short circuit evaluation * fix merge errors * added changelog info * Fix #648 --------- Co-authored-by: Bernd Stahlbock * Limit control voltage to max cell voltage * Rework serial parser (#12) much cleaner code, as it is optimised for the daly protocol * updated changelog * fix black lint error * fixed black lint error * added infos to battery template * JDB BLE support (#499) * Implementing JBD BLE support. It is built upon Bleak * Additionally, it provides some handling of for up to 4 temperature probes labels T1 (NTC2), T2 (NTC3), T3 (NTC4) and T4 (NTC5). NTC1 is the BMS module temperature itself * The device page has been extend to provide more details about the actual used hardware (= product name), firmware version and BLE address * updated changelog * Add Supoprt for HeltecSmartBMS (YYBMS) using modbus via RS485 connection (#658) * Changes 2023.05.24 (#667) * Check Venus OS version before installing * fix Jkbms_Ble error * updated changelog * updated readme and added donation link * bugfix: Heltec BMS test_connection breaks on other modbus compliant BMS systems * bugfix: LLTJBD BMS ignore non ASCII letters for hardware version * updated readme * Suppress daly read errors (#13) * updated descriptions * Changed logging level and give better feedback * Changed logging level to debug for unneeded messages * Give the user feedback, if a BMS is found or not. Useful, if only one BMS is tested. * small fixes --------- Co-authored-by: Bernd Stahlbock Co-authored-by: Oleg Gurevich Co-authored-by: Bernd <6627385+transistorgit@users.noreply.github.com> Co-authored-by: Paul Strawder Co-authored-by: Raphael Mack Co-authored-by: Strawder, Paul --- CHANGELOG.md | 16 +- README.md | 18 +- docs/docs/general/features.md | 44 +- docs/docs/general/install.md | 2 +- docs/docs/general/supported-bms.md | 3 + etc/dbus-serialbattery/battery.py | 142 +++-- .../bms/battery_template.py | 8 +- etc/dbus-serialbattery/bms/daly.py | 562 +++++++++--------- etc/dbus-serialbattery/bms/ecs.py | 23 +- etc/dbus-serialbattery/bms/heltecmodbus.py | 445 ++++++++++++++ etc/dbus-serialbattery/bms/hlpdatabms4s.py | 7 + etc/dbus-serialbattery/bms/jkbms_ble.py | 1 + etc/dbus-serialbattery/bms/lifepower.py | 4 +- etc/dbus-serialbattery/bms/lltjbd.py | 333 ++++++++++- etc/dbus-serialbattery/bms/lltjbd_ble.py | 44 +- etc/dbus-serialbattery/bms/mnb.py | 4 +- etc/dbus-serialbattery/bms/seplos.py | 14 +- etc/dbus-serialbattery/config.default.ini | 59 +- etc/dbus-serialbattery/dbus-serialbattery.py | 35 +- etc/dbus-serialbattery/dbushelper.py | 38 +- etc/dbus-serialbattery/disable.sh | 24 +- etc/dbus-serialbattery/install-qml.sh | 2 +- etc/dbus-serialbattery/install.sh | 4 +- .../qml/PageBatteryCellVoltages.qml | 50 +- etc/dbus-serialbattery/qml/PageLynxIonIo.qml | 19 + etc/dbus-serialbattery/reinstall-local.sh | 137 ++++- etc/dbus-serialbattery/restart-driver.sh | 2 +- etc/dbus-serialbattery/uninstall.sh | 52 +- etc/dbus-serialbattery/utils.py | 79 ++- rc/post-hook.sh | 7 + requirements.txt | 1 + 31 files changed, 1612 insertions(+), 567 deletions(-) create mode 100644 etc/dbus-serialbattery/bms/heltecmodbus.py mode change 100755 => 100644 etc/dbus-serialbattery/install.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 30822f96..d3c14b0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,12 @@ * Added: Balancing switch status to the GUI -> SerialBattery -> IO by @mr-manuel * Added: Block charge/discharge when BMS communication is lost. Can be enabled trough the config file by @mr-manuel * Added: Charge Mode display by @mr-manuel +* Added: Check minimum required Venus OS version before installing by @mr-manuel * Added: Choose how battery temperature is assembled (mean temp 1 & 2, only temp 1 or only temp 2) by @mr-manuel * Added: Config file by @ppuetsch * Added: Create empty `config.ini` for easier user usage by @mr-manuel * Added: Cronjob to restart Bluetooth service every 12 hours by @mr-manuel +* Added: Daly BMS - Discharge / Charge Mosfet switching over remote console/GUI https://github.com/Louisvdw/dbus-serialbattery/issues/26 by @transistorgit * Added: Daly BMS - Read capacity https://github.com/Louisvdw/dbus-serialbattery/pull/594 by @transistorgit * Added: Daly BMS - Read production date and build unique identifier by @transistorgit * Added: Daly BMS - Set SoC by @transistorgit @@ -23,7 +25,9 @@ * Added: Device name field (found in the GUI -> SerialBattery -> Device), that show a custom string that can be set in some BMS, if available by @mr-manuel * Added: Driver uninstall script by @mr-manuel * Added: Fix for Venus OS >= v3.00~14 showing unused items https://github.com/Louisvdw/dbus-serialbattery/issues/469 by @mr-manuel +* Added: HeltecSmartBMS driver by @ramack * Added: HighInternalTemperature alarm (MOSFET) for JKBMS by @mr-manuel +* Added: HLPdata BMS driver by @ peterohman * Added: Improved maintainability (flake8, black lint), introduced code checks and automate release build https://github.com/Louisvdw/dbus-serialbattery/pull/386 by @ppuetsch * Added: Install needed Bluetooth components automatically after a Venus OS upgrade by @mr-manuel * Added: JKBMS - MOS temperature https://github.com/Louisvdw/dbus-serialbattery/pull/440 by @baphomett @@ -37,12 +41,16 @@ * Added: JKBMS BLE - Show if balancing is active and which cells are balancing by @mr-manuel * Added: JKBMS BLE - Show serial number and "User Private Data" field that can be set in the JKBMS App to identify the BMS in a multi battery environment by @mr-manuel * Added: JKBMS BLE driver by @baranator +* Added: LLT/JBD BMS BLE driver by @idstein * Added: Possibility to add `config.ini` to the root of a USB flash drive on install via the USB method by @mr-manuel +* Added: Possibility to configure a `VOLTAGE_DROP` voltage, if you are using a SmartShunt as battery monitor as there is a little voltage difference https://github.com/Louisvdw/dbus-serialbattery/discussions/632 by @mr-manuel * Added: Post install notes by @mr-manuel * Added: Read charge/discharge limits from JKBMS by @mr-manuel * Added: Recalculation interval in linear mode for CVL, CCL and DCL by @mr-manuel +* Added: Rename TAR file after USB/SD card install to not overwrite the data on every reboot https://github.com/Louisvdw/dbus-serialbattery/issues/638 by @mr-manuel * Added: Reset values to None, if battery goes offline (not reachable for 10s). Fixes https://github.com/Louisvdw/dbus-serialbattery/issues/193 https://github.com/Louisvdw/dbus-serialbattery/issues/64 by @transistorgit * Added: Script to install directly from repository by @mr-manuel +* Added: Seplos BMS driver by @wollew * Added: Serial number field (found in the GUI -> SerialBattery -> Device), that show the serial number or a unique identifier for the BMS, if available by @mr-manuel * Added: Show charge mode (absorption, bulk, ...) in Parameters page by @mr-manuel * Added: Show charge/discharge limitation reason by @mr-manuel @@ -52,11 +60,15 @@ * Added: Show TimeToGo in GUI only, if enabled by @mr-manuel * Added: Support for HLPdata BMS4S https://github.com/Louisvdw/dbus-serialbattery/pull/505 by @peterohman * Added: Support for Seplos BMS https://github.com/Louisvdw/dbus-serialbattery/pull/530 by @wollew +* Added: Temperature 1-4 are now also available on the dbus and MQTT by @idstein * Added: Temperature name for temperature sensor 1 & 2. This allows to see which sensor is low and high (e.g. battery and cable) by @mr-manuel * Changed: `reinstall-local.sh` to recreate `/data/conf/serial-starter.d`, if deleted by `disable.sh` --> to check if the file `conf/serial-starter.d` could now be removed from the repository by @mr-manuel * Changed: Added QML to `restore-gui.sh` by @mr-manuel * Changed: Bash output by @mr-manuel +* Changed: CVL calculation improvement. Removed cell voltage penalty. Replaced by automatic voltage calculation. Max voltage is kept until cells are balanced and reset when cells are inbalanced or SoC is below threshold by @mr-manuel +* Changed: Daly BMS - Fixed BMS alerts by @mr-manuel * Changed: Daly BMS - Improved driver stability by @transistorgit & @mr-manuel +* Changed: Daly BMS - Reworked serial parser by @transistorgit * Changed: Default config file by @ppuetsch * Added missing descriptions to make it much clearer to understand by @mr-manuel * Changed name from `default_config.ini` to `config.default.ini` https://github.com/Louisvdw/dbus-serialbattery/pull/412#issuecomment-1434287942 by @mr-manuel @@ -67,14 +79,17 @@ * Changed: Default LINEAR_LIMITATION_ENABLE from False to True by @mr-manuel * Changed: Disabled ANT BMS by default https://github.com/Louisvdw/dbus-serialbattery/issues/479 by @mr-manuel * Changed: Driver can now also start without serial adapter attached for Bluetooth BMS by @seidler2547 +* Changed: Feedback from BMS driver to know, if BMS is found or not by @mr-manuel * Changed: Fix for https://github.com/Louisvdw/dbus-serialbattery/issues/239 by @mr-manuel * Changed: Fix for https://github.com/Louisvdw/dbus-serialbattery/issues/311 by @mr-manuel * Changed: Fix for https://github.com/Louisvdw/dbus-serialbattery/issues/351 by @mr-manuel * Changed: Fix for https://github.com/Louisvdw/dbus-serialbattery/issues/397 by @transistorgit * Changed: Fix for https://github.com/Louisvdw/dbus-serialbattery/issues/421 by @mr-manuel * Changed: Fix for https://github.com/Louisvdw/dbus-serialbattery/issues/450 by @mr-manuel +* Changed: Fix for https://github.com/Louisvdw/dbus-serialbattery/issues/648 by @mr-manuel * Changed: Fixed black lint errors by @mr-manuel * Changed: Fixed cell balancing background for cells 17-24 by @mr-manuel +* Changed: Fixed cell balancing display for JBD/LLT BMS https://github.com/Louisvdw/dbus-serialbattery/issues/359 by @mr-manuel * Changed: Fixed Time-To-Go is not working, if `TIME_TO_SOC_VALUE_TYPE` is set to other than `1` https://github.com/Louisvdw/dbus-serialbattery/pull/424#issuecomment-1440511018 by @mr-manuel * Changed: Improved install workflow via USB flash drive by @mr-manuel * Changed: Improved JBD BMS soc calculation https://github.com/Louisvdw/dbus-serialbattery/pull/439 by @aaronreek @@ -83,7 +98,6 @@ * Changed: Moved Bluetooth part to `reinstall-local.sh` by @mr-manuel * Changed: Moved BMS scripts to subfolder by @mr-manuel * Changed: Removed all wildcard imports and fixed black lint errors by @mr-manuel -* Changed: Removed cell voltage penalty. Replaced by automatic voltage calculation. Max voltage is kept until cells are balanced and reset when cells are inbalanced by @mr-manuel * Changed: Renamed scripts for better reading #532 by @mr-manuel * Changed: Reworked and optimized installation scripts by @mr-manuel * Changed: Separate Time-To-Go and Time-To-SoC activation by @mr-manuel diff --git a/README.md b/README.md index 063eaaa3..38f11e44 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,19 @@ # dbus-serialbattery This is a driver for Venus OS devices (any GX device sold by Victron or a Raspberry Pi running the Venus OS image). -The driver will communicate with a Battery Management System (BMS) that support serial communication (RS232, RS485 or TTL UART) and publish this data to the Venus OS system. The main purpose is to act as a Battery Monitor in your GX and supply State Of Charge (SOC) and other values to the inverter. +The driver will communicate with a Battery Management System (BMS) that support serial (RS232, RS485 or TTL UART) and Bluetooth communication (see [BMS feature comparison](https://louisvdw.github.io/dbus-serialbattery/general/features#bms-feature-comparison) for details). The data is then published to the Venus OS system (dbus). The main purpose is to act as a Battery Monitor in your GX and supply State of Charge (SoC) and other values to the inverter/charger. - * [BMS Types supported](https://louisvdw.github.io/dbus-serialbattery/general/supported-bms) - * [FAQ](https://louisvdw.github.io/dbus-serialbattery/faq/) - * [Features](https://louisvdw.github.io/dbus-serialbattery/general/features) - * [How to install](https://louisvdw.github.io/dbus-serialbattery/general/install) - * [How to troubleshoot](https://louisvdw.github.io/dbus-serialbattery/troubleshoot/) +## Documentation + +* [Introduction](https://louisvdw.github.io/dbus-serialbattery/) +* [Features](https://louisvdw.github.io/dbus-serialbattery/general/features) +* [Supported BMS](https://louisvdw.github.io/dbus-serialbattery/general/supported-bms) +* [How to install, update, disable, enable and uninstall](https://louisvdw.github.io/dbus-serialbattery/general/install) +* [How to troubleshoot](https://louisvdw.github.io/dbus-serialbattery/troubleshoot/) +* [FAQ](https://louisvdw.github.io/dbus-serialbattery/faq/) ### Supporting this project -If you find this driver helpful please considder supporting this project. You can buy me a Ko-Fi or get in contact if you would like to donate hardware. +If you find this driver helpful please consider supporting this project. You can buy me a Ko-Fi or get in contact, if you would like to donate hardware for development. ### Support [Louisvdw](https://github.com/Louisvdw) * Main developer @@ -25,6 +28,7 @@ If you find this driver helpful please considder supporting this project. You ca [](https://www.paypal.com/donate/?hosted_button_id=3NEVZBDM5KABW) + ### Developer Remarks To develop this project, install the requirements. This project makes use of velib_python which is pre-installed on Venus-OS Devices under `/opt/victronenergy/dbus-systemcalc-py/ext/velib_python`. To use the python files locally, diff --git a/docs/docs/general/features.md b/docs/docs/general/features.md index a7b2434c..2ed2e9b3 100644 --- a/docs/docs/general/features.md +++ b/docs/docs/general/features.md @@ -83,7 +83,7 @@ CCCM limits the charge/discharge current depending on the highest/lowest cell vo * between `2.8V - 2.9V` → `5A `discharge * below `<= 2.70V` → `0A` discharge -### Temprature +### Temperature * `CCCM_T_ENABLE = True/False` * `DCCM_T_ENABLE = True/False` @@ -121,27 +121,27 @@ If the `MAX_CELL_VOLTAGE` \* `cell count` is reached for `MAX_VOLTAGE_TIME_SEC` ## BMS feature comparison -| Feature | Ant | Daly | ECS | HLPdataBMS4S | JK BMS | Life/Tian Power | LLT/JBD | MNB (1) | Renogy | Seplos | Sinowealth (1) | -| ---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | -| Voltage | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Current | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Power | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| State Of Charge | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Battery temperature | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| MOSFET temperature | No | No | No | No | Yes | No | Yes | No | No | No | No | -| Consumed Ah | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Time-to-go | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | -| Min/max cell voltages | Yes | Yes | No | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | -| Min/max temperature | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Installed capacity | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Available capacity | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Cell details | No | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | ? | -| Balancing status | Yes | No | Yes | No | Yes | Yes | No | No | No | No | ? | -| Raise alarms from the BMS | Yes | Yes | Yes (2) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | ? | -| History of charge cycles | Yes | Yes | No | No | Yes | Yes | Yes | No | Yes | Yes | Yes | -| Get CCL/DCL from the BMS | No | No | No | No | Yes | No | No | No | No | No | No | -| Charge current control management (CCCM) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Set battery parameters (DVCC) | Calc | Calc | Yes | Yes | Calc | Calc | Calc | Yes | Calc | Calc | Calc | +| Feature | Ant | Daly | ECS | Heltec | HLPdataBMS4S | JK BMS | Life/Tian Power | LLT/JBD | MNB (1) | Renogy | Seplos | Sinowealth (1) | +| ---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | +| Voltage | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Current | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Power | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| State Of Charge | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Battery temperature | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| MOSFET temperature | No | No | No | Yes | No | Yes | No | Yes | No | No | No | No | +| Consumed Ah | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Time-to-go | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | +| Min/max cell voltages | Yes | Yes | No | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | +| Min/max temperature | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Installed capacity | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Available capacity | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Cell details | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | ? | +| Balancing status | Yes | No | Yes | Yes | No | Yes | Yes | No | No | No | No | ? | +| Raise alarms from the BMS | Yes | Yes | Yes (2) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | ? | +| History of charge cycles | Yes | Yes | No | No | No | Yes | Yes | Yes | No | Yes | Yes | Yes | +| Get CCL/DCL from the BMS | No | No | No | Yes | No | Yes | No | No | No | No | No | No | +| Charge current control management (CCCM) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Set battery parameters (DVCC) | Calc | Calc | Yes | Calc | Yes | Calc | Calc | Calc | Yes | Calc | Calc | Calc | `Calc` means that the value is calculated by the driver. diff --git a/docs/docs/general/install.md b/docs/docs/general/install.md index 56c891bc..4b30e030 100644 --- a/docs/docs/general/install.md +++ b/docs/docs/general/install.md @@ -122,7 +122,7 @@ Select `2` for `nightly build` and then select the branch you want to install fr ### BMS specific settings * ECS BMS → https://github.com/Louisvdw/dbus-serialbattery/issues/254#issuecomment-1275924313 - +* HeltecModbus → in case the modbus slave address of the BMS was adjusted from the factory default, configure the slave addresses to query in config.ini:HELTEC_MODBUS_ADDR. As always the battery settings shall be configured in the BMS already via app or computer. ## How to change the default limits diff --git a/docs/docs/general/supported-bms.md b/docs/docs/general/supported-bms.md index 416e7362..ff247950 100644 --- a/docs/docs/general/supported-bms.md +++ b/docs/docs/general/supported-bms.md @@ -22,6 +22,9 @@ Disabled by default since driver version `v0.14.0` as it causes other issues. Se ### • ECS GreenMeter with LiPro +### • HeltecModbus SmartBMS (YanYang BMS) +Communication to the Heltec SmartBMS (which is a rebranded YYBMS) via Modbus/RS485. + ### • HLPdataBMS4S ### • [JKBMS](https://www.jkbms.com/products/) / Heltec BMS diff --git a/etc/dbus-serialbattery/battery.py b/etc/dbus-serialbattery/battery.py index 108cf211..eb5b2e2d 100644 --- a/etc/dbus-serialbattery/battery.py +++ b/etc/dbus-serialbattery/battery.py @@ -96,6 +96,8 @@ def init_values(self): self.temp_sensors = None self.temp1 = None self.temp2 = None + self.temp3 = None + self.temp4 = None self.temp_mos = None self.cells: List[Cell] = [] self.control_charging = None @@ -127,6 +129,15 @@ def test_connection(self) -> bool: # return false when failed, true if successful return False + def connection_name(self) -> str: + return "Serial " + self.port + + def custom_name(self) -> str: + return "SerialBattery(" + self.type + ")" + + def product_name(self) -> str: + return "SerialBattery(" + self.type + ")" + @abstractmethod def get_settings(self) -> bool: """ @@ -164,6 +175,10 @@ def to_temp(self, sensor: int, value: float) -> None: self.temp1 = min(max(value, -20), 100) if sensor == 2: self.temp2 = min(max(value, -20), 100) + if sensor == 3: + self.temp3 = min(max(value, -20), 100) + if sensor == 4: + self.temp4 = min(max(value, -20), 100) def manage_charge_voltage(self) -> None: """ @@ -207,18 +222,21 @@ def manage_charge_voltage_linear(self) -> None: voltageDiff = self.get_max_cell_voltage() - self.get_min_cell_voltage() if self.max_voltage_start_time is None: + # start timer, if max voltage is reached and cells are balanced if ( - utils.MAX_CELL_VOLTAGE * self.cell_count <= voltageSum + (utils.MAX_CELL_VOLTAGE * self.cell_count) - utils.VOLTAGE_DROP + <= voltageSum and voltageDiff <= utils.CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_UNTIL and self.allow_max_voltage ): self.max_voltage_start_time = time() + + # allow max voltage again, if cells are unbalanced or SoC threshold is reached elif ( - # utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT > self.soc - voltageDiff >= utils.CELL_VOLTAGE_DIFF_TO_RESET_VOLTAGE_LIMIT - and not self.allow_max_voltage - ): + utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT > self.soc + or voltageDiff >= utils.CELL_VOLTAGE_DIFF_TO_RESET_VOLTAGE_LIMIT + ) and not self.allow_max_voltage: self.allow_max_voltage = True else: tDiff = time() - self.max_voltage_start_time @@ -238,11 +256,14 @@ def manage_charge_voltage_linear(self) -> None: ): self.linear_cvl_last_set = int(time()) - # Keep penalty above min battery voltage + # Keep penalty above min battery voltage and below max battery voltage self.control_voltage = round( - max( - voltageSum - penaltySum, - utils.MIN_CELL_VOLTAGE * self.cell_count, + min( + max( + voltageSum - penaltySum, + utils.MIN_CELL_VOLTAGE * self.cell_count, + ), + utils.MAX_CELL_VOLTAGE * self.cell_count, ), 3, ) @@ -309,9 +330,8 @@ def manage_charge_voltage_step(self) -> None: if self.max_voltage_start_time is None: # check if max voltage is reached and start timer to keep max voltage if ( - utils.MAX_CELL_VOLTAGE * self.cell_count <= voltageSum - and self.allow_max_voltage - ): + utils.MAX_CELL_VOLTAGE * self.cell_count + ) - utils.VOLTAGE_DROP <= voltageSum and self.allow_max_voltage: # example 2 self.max_voltage_start_time = time() @@ -802,14 +822,10 @@ def get_balancing(self) -> int: return 1 return 0 - def extract_from_temp_values(self, extractor) -> Union[float, None]: - if self.temp1 is not None and self.temp2 is not None: - return extractor(self.temp1, self.temp2) - if self.temp1 is not None and self.temp2 is None: - return self.temp1 - if self.temp1 is None and self.temp2 is not None: - return self.temp2 - else: + def get_temperatures(self) -> Union[List[float], None]: + temperatures = [self.temp1, self.temp2, self.temp3, self.temp4] + result = [(t, i) for (t, i) in enumerate(temperatures) if t is not None] + if not result: return None def get_temp(self) -> Union[float, None]: @@ -818,46 +834,93 @@ def get_temp(self) -> Union[float, None]: return self.temp1 elif utils.TEMP_BATTERY == 2: return self.temp2 + elif utils.TEMP_BATTERY == 3: + return self.temp3 + elif utils.TEMP_BATTERY == 4: + return self.temp4 else: - return self.extract_from_temp_values( - extractor=lambda temp1, temp2: round( - (float(temp1) + float(temp2)) / 2, 2 - ) - ) + temps = [ + t + for t in [self.temp1, self.temp2, self.temp3, self.temp4] + if t is not None + ] + n = len(temps) + if not temps or n == 0: + return None + data = sorted(temps) + if n % 2 == 1: + return data[n // 2] + else: + i = n // 2 + return (data[i - 1] + data[i]) / 2 except TypeError: return None def get_min_temp(self) -> Union[float, None]: try: - return self.extract_from_temp_values( - extractor=lambda temp1, temp2: min(temp1, temp2) - ) + temps = [ + t + for t in [self.temp1, self.temp2, self.temp3, self.temp4] + if t is not None + ] + if not temps: + return None + return min(temps) except TypeError: return None def get_min_temp_id(self) -> Union[str, None]: try: - if self.temp1 < self.temp2: + temps = [ + (t, i) + for i, t in enumerate([self.temp1, self.temp2, self.temp3, self.temp4]) + if t is not None + ] + if not temps: + return None + index = min(temps)[1] + if index == 0: return utils.TEMP_1_NAME - else: + if index == 1: return utils.TEMP_2_NAME + if index == 2: + return utils.TEMP_3_NAME + if index == 3: + return utils.TEMP_4_NAME except TypeError: return None def get_max_temp(self) -> Union[float, None]: try: - return self.extract_from_temp_values( - extractor=lambda temp1, temp2: max(temp1, temp2) - ) + temps = [ + t + for t in [self.temp1, self.temp2, self.temp3, self.temp4] + if t is not None + ] + if not temps: + return None + return max(temps) except TypeError: return None def get_max_temp_id(self) -> Union[str, None]: try: - if self.temp1 > self.temp2: + temps = [ + (t, i) + for i, t in enumerate([self.temp1, self.temp2, self.temp3, self.temp4]) + if t is not None + ] + if not temps: + return None + index = max(temps)[1] + if index == 0: return utils.TEMP_1_NAME - else: + if index == 1: return utils.TEMP_2_NAME + if index == 2: + return utils.TEMP_3_NAME + if index == 3: + return utils.TEMP_4_NAME except TypeError: return None @@ -928,3 +991,12 @@ def log_settings(self) -> None: def reset_soc_callback(self, path, value): # callback for handling reset soc request return + + def force_charging_off_callback(self, path, value): + return + + def force_discharging_off_callback(self, path, value): + return + + def turn_balancing_off_callback(self, path, value): + return diff --git a/etc/dbus-serialbattery/bms/battery_template.py b/etc/dbus-serialbattery/bms/battery_template.py index e6148e65..e32e424b 100644 --- a/etc/dbus-serialbattery/bms/battery_template.py +++ b/etc/dbus-serialbattery/bms/battery_template.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # NOTES -# Please also update the feature comparison table, if you are adding a new BMS -# https://louisvdw.github.io/dbus-serialbattery/general/features/#bms-feature-comparison +# Please see "Add/Request a new BMS" https://louisvdw.github.io/dbus-serialbattery/general/supported-bms#add-by-opening-a-pull-request +# in the documentation for a checklist what you have to do, when adding a new BMS +# avoid importing wildcards from battery import Protection, Battery, Cell from utils import is_bit_set, read_serial_data, logger import utils @@ -26,7 +27,7 @@ def test_connection(self): result = False try: result = self.read_status_data() - # get first data to show in startup log + # get first data to show in startup log, only if result is true if result: self.refresh_data() except Exception as err: @@ -104,6 +105,7 @@ def read_serial_data_template(self, command): command, self.port, self.baud_rate, self.LENGTH_POS, self.LENGTH_CHECK ) if data is False: + logger.error(">>> ERROR: No reply - returning") return False start, flag, command_ret, length = unpack_from("BBBB", data) diff --git a/etc/dbus-serialbattery/bms/daly.py b/etc/dbus-serialbattery/bms/daly.py index b358a886..87510c48 100644 --- a/etc/dbus-serialbattery/bms/daly.py +++ b/etc/dbus-serialbattery/bms/daly.py @@ -24,6 +24,9 @@ def __init__(self, port, baud, address): self.reset_soc = 0 self.soc_to_set = None self.runtime = 0 # TROUBLESHOOTING for no reply errors + self.trigger_force_disable_discharge = None + self.trigger_force_disable_charge = None + self.cells_volts_data_lastreadbad = False # command bytes [StartFlag=A5][Address=40][Command=94][DataLength=8][8x zero bytes][checksum] command_base = b"\xA5\x40\x94\x08\x00\x00\x00\x00\x00\x00\x00\x00\x81" @@ -40,6 +43,8 @@ def __init__(self, port, baud, address): command_temp = b"\x96" command_cell_balance = b"\x97" # no reply command_alarm = b"\x98" # no reply + command_disable_discharge_mos = b"\xD9" + command_disable_charge_mos = b"\xDA" BATTERYTYPE = "Daly" LENGTH_CHECK = 1 @@ -55,16 +60,19 @@ def test_connection(self): try: with open_serial_port(self.port, self.baud_rate) as ser: result = self.read_status_data(ser) - self.read_soc_data(ser) - self.read_battery_code(ser) - self.reset_soc = ( - self.soc - ) # set to meaningful value as preset for the GUI + # get first data to show in startup log, only if result is true + if result: + self.read_soc_data(ser) + self.read_battery_code(ser) except Exception as err: logger.error(f"Unexpected {err=}, {type(err)=}") result = False + # give the user a feedback that no BMS was found + if not result: + logger.error(">>> ERROR: No reply - returning") + return result def get_settings(self): @@ -84,32 +92,33 @@ def refresh_data(self): try: with open_serial_port(self.port, self.baud_rate) as ser: result = self.read_soc_data(ser) + self.reset_soc = self.soc if self.soc else 0 if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors logger.info( " |- refresh_data: read_soc_data - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) - result = result and self.read_fed_data(ser) + result = self.read_fed_data(ser) and result if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors logger.info( " |- refresh_data: read_fed_data - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) - result = result and self.read_cell_voltage_range_data(ser) + result = self.read_cell_voltage_range_data(ser) and result if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors logger.info( " |- refresh_data: read_cell_voltage_range_data - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) @@ -119,50 +128,52 @@ def refresh_data(self): " |- refresh_data: write_soc_and_datetime - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) - result = result and self.read_alarm_data(ser) + result = self.read_alarm_data(ser) and result if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors logger.info( " |- refresh_data: read_alarm_data - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) - result = result and self.read_temperature_range_data(ser) + result = self.read_temperature_range_data(ser) and result if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors logger.info( " |- refresh_data: read_temperature_range_data - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) - result = result and self.read_cells_volts(ser) + result = self.read_balance_state(ser) and result if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors logger.info( - " |- refresh_data: read_cells_volts - result: " + " |- refresh_data: read_balance_state - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) - result = result and self.read_balance_state(ser) + result = self.read_cells_volts(ser) and result if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors logger.info( - " |- refresh_data: read_balance_state - result: " + " |- refresh_data: read_cells_volts - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) + self.write_charge_discharge_mos(ser) + except OSError: logger.warning("Couldn't open serial port") @@ -171,10 +182,10 @@ def refresh_data(self): return result def read_status_data(self, ser): - status_data = self.read_serial_data_daly(ser, self.command_status) + status_data = self.request_data(ser, self.command_status) # check if connection success if status_data is False: - logger.warning("No data received in read_status_data()") + logger.debug("No data received in read_status_data()") return False ( @@ -195,7 +206,7 @@ def read_status_data(self, ser): + " cells" + (" (" + self.production + ")" if self.production else "") ) - logger.info(self.hardware_version) + logger.debug(self.hardware_version) return True def read_soc_data(self, ser): @@ -205,7 +216,7 @@ def read_soc_data(self, ser): triesValid = 2 while triesValid > 0: triesValid -= 1 - soc_data = self.read_serial_data_daly(ser, self.command_soc) + soc_data = self.request_data(ser, self.command_soc) # check if connection success if soc_data is False: continue @@ -226,7 +237,7 @@ def read_soc_data(self, ser): return False def read_alarm_data(self, ser): - alarm_data = self.read_serial_data_daly(ser, self.command_alarm) + alarm_data = self.request_data(ser, self.command_alarm) # check if connection success if alarm_data is False: logger.warning("No data received in read_alarm_data()") @@ -245,57 +256,57 @@ def read_alarm_data(self, ser): if al_volt & 48: # High voltage levels - Alarm - self.voltage_high = 2 + self.protection.voltage_high = 2 elif al_volt & 15: # High voltage Warning levels - Pre-alarm - self.voltage_high = 1 + self.protection.voltage_high = 1 else: - self.voltage_high = 0 + self.protection.voltage_high = 0 if al_volt & 128: # Low voltage level - Alarm - self.voltage_low = 2 + self.protection.voltage_low = 2 elif al_volt & 64: # Low voltage Warning level - Pre-alarm - self.voltage_low = 1 + self.protection.voltage_low = 1 else: - self.voltage_low = 0 + self.protection.voltage_low = 0 if al_temp & 2: # High charge temp - Alarm - self.temp_high_charge = 2 + self.protection.temp_high_charge = 2 elif al_temp & 1: # High charge temp - Pre-alarm - self.temp_high_charge = 1 + self.protection.temp_high_charge = 1 else: - self.temp_high_charge = 0 + self.protection.temp_high_charge = 0 if al_temp & 8: # Low charge temp - Alarm - self.temp_low_charge = 2 + self.protection.temp_low_charge = 2 elif al_temp & 4: # Low charge temp - Pre-alarm - self.temp_low_charge = 1 + self.protection.temp_low_charge = 1 else: - self.temp_low_charge = 0 + self.protection.temp_low_charge = 0 if al_temp & 32: # High discharge temp - Alarm - self.temp_high_discharge = 2 + self.protection.temp_high_discharge = 2 elif al_temp & 16: # High discharge temp - Pre-alarm - self.temp_high_discharge = 1 + self.protection.temp_high_discharge = 1 else: - self.temp_high_discharge = 0 + self.protection.temp_high_discharge = 0 if al_temp & 128: # Low discharge temp - Alarm - self.temp_low_discharge = 2 + self.protection.temp_low_discharge = 2 elif al_temp & 64: # Low discharge temp - Pre-alarm - self.temp_low_discharge = 1 + self.protection.temp_low_discharge = 1 else: - self.temp_low_discharge = 0 + self.protection.temp_low_discharge = 0 # if al_crnt_soc & 2: # # High charge current - Alarm @@ -317,98 +328,90 @@ def read_alarm_data(self, ser): if al_crnt_soc & 2 or al_crnt_soc & 8: # High charge/discharge current - Alarm - self.current_over = 2 + self.protection.current_over = 2 elif al_crnt_soc & 1 or al_crnt_soc & 4: # High charge/discharge current - Pre-alarm - self.current_over = 1 + self.protection.current_over = 1 else: - self.current_over = 0 + self.protection.current_over = 0 if al_crnt_soc & 128: # Low SoC - Alarm - self.soc_low = 2 + self.protection.soc_low = 2 elif al_crnt_soc & 64: # Low SoC Warning level - Pre-alarm - self.soc_low = 1 + self.protection.soc_low = 1 else: - self.soc_low = 0 + self.protection.soc_low = 0 return True def read_cells_volts(self, ser): - if self.cell_count is not None: - buffer = bytearray(self.command_base) - buffer[1] = self.command_address[0] # Always serial 40 or 80 - buffer[2] = self.command_cell_volts[0] - buffer[12] = sum(buffer[:12]) & 0xFF - - # logger.info(f"{bytes(buffer).hex()}") - - if (int(self.cell_count) % 3) == 0: - maxFrame = int(self.cell_count / 3) - else: - maxFrame = int(self.cell_count / 3) + 1 - lenFixed = ( - maxFrame * 13 - ) # 0xA5, 0x01, 0x95, 0x08 + 1 byte frame + 6 byte data + 1byte reserved + chksum - - cells_volts_data = self.read_serialport_data( - ser, buffer, self.LENGTH_POS, 0, lenFixed + if self.cell_count is None: + return True + + # calculate how many sentences we will receive + # in each sentence, the bms will send 3 cell voltages + # so for a 4s, we will receive 2 sentences + if (int(self.cell_count) % 3) == 0: + sentences_expected = int(self.cell_count / 3) + else: + sentences_expected = int(self.cell_count / 3) + 1 + + cells_volts_data = self.request_data( + ser, self.command_cell_volts, sentences_to_receive=sentences_expected + ) + + if cells_volts_data is False and self.cells_volts_data_lastreadbad is True: + # if this read out and the last one were bad, report error. + # (we don't report single errors, as current daly firmware sends corrupted cells volts data occassionally) + logger.debug( + "No or invalid data has been received repeatedly in read_cells_volts()" ) - if cells_volts_data is False: - logger.warning("No data received in read_cells_volts()") - return False + return False + elif cells_volts_data is False: + # memorize that this read was bad and bail out, ignoring it + self.cells_volts_data_lastreadbad = True + return True + else: + # this read was good, so reset error flag + self.cells_volts_data_lastreadbad = False - frameCell = [0, 0, 0] - lowMin = utils.MIN_CELL_VOLTAGE / 2 - frame = 0 - bufIdx = 0 - - if len(self.cells) != self.cell_count: - # init the numbers of cells - self.cells = [] - for idx in range(self.cell_count): - self.cells.append(Cell(True)) - - # logger.warning("data " + bytes(cells_volts_data).hex()) - - while bufIdx <= len(cells_volts_data) - ( - 4 + 8 + 1 - ): # we at least need 13 bytes to extract the identifiers + 8 bytes payload + checksum - b1, b2, b3, b4 = unpack_from(">BBBB", cells_volts_data, bufIdx) - if b1 == 0xA5 and b2 == 0x01 and b3 == 0x95 and b4 == 0x08: - ( - frame, - frameCell[0], - frameCell[1], - frameCell[2], - _, - chk, - ) = unpack_from(">BhhhBB", cells_volts_data, bufIdx + 4) - if sum(cells_volts_data[bufIdx : bufIdx + 12]) & 0xFF != chk: - logger.warning("bad cell voltages checksum") - else: - for idx in range(3): - cellnum = ( - (frame - 1) * 3 - ) + idx # daly is 1 based, driver 0 based - if cellnum >= self.cell_count: - break - cellVoltage = frameCell[idx] / 1000 - self.cells[cellnum].voltage = ( - None if cellVoltage < lowMin else cellVoltage - ) - bufIdx += 13 # BBBBBhhhBB -> 13 byte - else: - bufIdx += 1 # step through buffer to find valid start - logger.warning("bad cell voltages header") + frameCell = [0, 0, 0] + lowMin = utils.MIN_CELL_VOLTAGE / 2 + frame = 0 + + if len(self.cells) != self.cell_count: + # init the numbers of cells + self.cells = [] + for idx in range(self.cell_count): + self.cells.append(Cell(True)) + + # logger.warning("data " + bytes(cells_volts_data).hex()) + + # from each of the received sentences, read up to 3 voltages + for i in range(sentences_expected): + ( + frame, + frameCell[0], + frameCell[1], + frameCell[2], + ) = unpack_from(">Bhhh", cells_volts_data, 8 * i) + for idx in range(3): + cellnum = ((frame - 1) * 3) + idx # daly is 1 based, driver 0 based + if cellnum >= self.cell_count: + break # ignore possible unused bytes of last sentence + cellVoltage = frameCell[idx] / 1000 + self.cells[cellnum].voltage = ( + None if cellVoltage < lowMin else cellVoltage + ) return True def read_cell_voltage_range_data(self, ser): - minmax_data = self.read_serial_data_daly(ser, self.command_minmax_cell_volts) + minmax_data = self.request_data(ser, self.command_minmax_cell_volts) # check if connection success if minmax_data is False: - logger.warning("No data received in read_cell_voltage_range_data()") + logger.debug("No data received in read_cell_voltage_range_data()") return False ( @@ -426,7 +429,7 @@ def read_cell_voltage_range_data(self, ser): return True def read_balance_state(self, ser): - balance_data = self.read_serial_data_daly(ser, self.command_cell_balance) + balance_data = self.request_data(ser, self.command_cell_balance) # check if connection success if balance_data is False: logger.debug("No data received in read_balance_state()") @@ -442,7 +445,7 @@ def read_balance_state(self, ser): return True def read_temperature_range_data(self, ser): - minmax_data = self.read_serial_data_daly(ser, self.command_minmax_temp) + minmax_data = self.request_data(ser, self.command_minmax_temp) # check if connection success if minmax_data is False: logger.debug("No data received in read_temperature_range_data()") @@ -454,7 +457,7 @@ def read_temperature_range_data(self, ser): return True def read_fed_data(self, ser): - fed_data = self.read_serial_data_daly(ser, self.command_fet) + fed_data = self.request_data(ser, self.command_fet) # check if connection success if fed_data is False: logger.debug("No data received in read_fed_data()") @@ -472,10 +475,10 @@ def read_fed_data(self, ser): # new def read_capacity(self, ser): - capa_data = self.read_serial_data_daly(ser, self.command_rated_params) + capa_data = self.request_data(ser, self.command_rated_params) # check if connection success if capa_data is False: - logger.warning("No data received in read_capacity()") + logger.debug("No data received in read_capacity()") return False (capacity, cell_volt) = unpack_from(">LL", capa_data) @@ -487,10 +490,10 @@ def read_capacity(self, ser): # new def read_production_date(self, ser): - production = self.read_serial_data_daly(ser, self.command_batt_details) + production = self.request_data(ser, self.command_batt_details) # check if connection success if production is False: - logger.warning("No data received in read_production_date()") + logger.debug("No data received in read_production_date()") return False (_, _, year, month, day) = unpack_from(">BBBBB", production) @@ -499,39 +502,19 @@ def read_production_date(self, ser): # new def read_battery_code(self, ser): - lenFixed = ( - 5 * 13 - ) # batt code field is 35 bytes and we transfer 7 bytes in each telegram - data = self.read_serialport_data( - ser, - self.generate_command(self.command_batt_code), - self.LENGTH_POS, - 0, - lenFixed, - ) + data = self.request_data(ser, self.command_batt_code, sentences_to_receive=5) if data is False: - logger.warning("No data received in read_battery_code()") + logger.debug("No data received in read_battery_code()") return False - bufIdx = 0 battery_code = "" # logger.warning("data " + bytes(cells_volts_data).hex()) - while ( - bufIdx <= len(data) - 13 - ): # we at least need 13 bytes to extract the identifiers + 8 bytes payload + checksum - b1, b2, b3, b4 = unpack_from(">BBBB", data, bufIdx) - if b1 == 0xA5 and b2 == 0x01 and b3 == 0x57 and b4 == 0x08: - _, part, chk = unpack_from(">B7sB", data, bufIdx + 4) - if sum(data[bufIdx : bufIdx + 12]) & 0xFF != chk: - logger.warning( - "bad battery code checksum" - ) # use string anyhow, just warn - battery_code += part.decode("utf-8") - bufIdx += 13 # BBBBB7sB -> 13 byte - else: - bufIdx += 1 # step through buffer to find valid start - logger.warning("bad battery code header") + for i in range(5): + nr, part = unpack_from(">B7s", data, i * 8) + if nr != i + 1: + logger.debug("bad battery code index") # use string anyhow, just warn + battery_code += part.decode("utf-8") if battery_code != "": self.custom_field = sub( @@ -546,136 +529,6 @@ def read_battery_code(self, ser): ) return True - def generate_command(self, command): - buffer = bytearray(self.command_base) - buffer[1] = self.command_address[0] # Always serial 40 or 80 - buffer[2] = command[0] - buffer[12] = sum(buffer[:12]) & 0xFF # checksum calc - return buffer - - def read_serial_data_daly(self, ser, command): - data = self.read_serialport_data( - ser, self.generate_command(command), self.LENGTH_POS, self.LENGTH_CHECK - ) - if data is False: - logger.info("No reply to cmd " + bytes(command).hex()) - return False - - if len(data) <= 12: - logger.debug("Too short reply to cmd " + bytes(command).hex()) - return False - - # search sentence start - try: - idx = data.index(0xA5) - except ValueError: - logger.debug( - "No Sentence Start found for reply to cmd " + bytes(command).hex() - ) - return False - - if len(data[idx:]) <= 12: - logger.debug("Too short reply to cmd " + bytes(command).hex()) - return False - - if data[12 + idx] != sum(data[idx : 12 + idx]) & 0xFF: - logger.debug("Bad checksum in reply to cmd " + bytes(command).hex()) - return False - - _, _, _, length = unpack_from(">BBBB", data, idx) - - if length == 8: - return data[4 + idx : length + 4 + idx] - else: - logger.debug( - ">>> ERROR: Incorrect Reply to CMD " - + bytes(command).hex() - + ": 0x" - + bytes(data).hex() - ) - return False - - # Read data from previously openned serial port - def read_serialport_data( - self, - ser, - command, - length_pos, - length_check, - length_fixed=None, - length_size=None, - ): - try: - # wait shortly, else the Daly is not ready and throws a lot of no reply errors - # if you see a lot of errors, try to increase in steps of 0.005 - sleep(0.020) - - time_start = time() - ser.flushOutput() - ser.flushInput() - ser.write(command) - - length_byte_size = 1 - if length_size is not None: - if length_size.upper() == "H": - length_byte_size = 2 - elif length_size.upper() == "I" or length_size.upper() == "L": - length_byte_size = 4 - - toread = ser.inWaiting() - - while toread < (length_pos + length_byte_size): - sleep(0.005) - toread = ser.inWaiting() - time_run = time() - time_start - if time_run > 0.500: - self.runtime = time_run - logger.error(">>> ERROR: No reply - returning") - return False - - # logger.info('serial data toread ' + str(toread)) - res = ser.read(toread) - if length_fixed is not None: - length = length_fixed - else: - if len(res) < (length_pos + length_byte_size): - logger.error( - ">>> ERROR: No reply - returning [len:" + str(len(res)) + "]" - ) - return False - length_size = length_size if length_size is not None else "B" - length = unpack_from(">" + length_size, res, length_pos)[0] - - data = bytearray(res) - - packetlen = ( - length_fixed - if length_fixed is not None - else length_pos + length_byte_size + length + length_check - ) - while len(data) < packetlen: - res = ser.read(packetlen - len(data)) - data.extend(res) - sleep(0.005) - time_run = time() - time_start - if time_run > 0.500: - self.runtime = time_run - logger.error( - ">>> ERROR: No reply - returning [len:" - + str(len(data)) - + "/" - + str(length + length_check) - + "]" - ) - return False - - self.runtime = time_run - return data - - except Exception as e: - logger.error(e) - return False - def reset_soc_callback(self, path, value): if value is None: return False @@ -715,21 +568,152 @@ def write_soc_and_datetime(self, ser): logger.info(f"write soc {self.soc_to_set}%") self.soc_to_set = None # Reset value, so we will set it only once - time_start = time() ser.flushOutput() ser.flushInput() ser.write(cmd) + reply = self.read_sentence(ser, self.command_set_soc) + if reply[0] != 1: + logger.error("write soc failed") + return True + + def force_charging_off_callback(self, path, value): + if value is None: + return False + + if value == 0: + self.trigger_force_disable_charge = False + return True + + if value == 1: + self.trigger_force_disable_charge = True + return True + + return False + + def force_discharging_off_callback(self, path, value): + if value is None: + return False + + if value == 0: + self.trigger_force_disable_discharge = False + return True + + if value == 1: + self.trigger_force_disable_discharge = True + return True + + return False + + def write_charge_discharge_mos(self, ser): + if ( + self.trigger_force_disable_charge is None + and self.trigger_force_disable_discharge is None + ): + return False + + cmd = bytearray(self.command_base) + + if self.trigger_force_disable_charge is not None: + cmd[2] = self.command_disable_charge_mos[0] + cmd[4] = 0 if self.trigger_force_disable_charge else 1 + cmd[12] = sum(cmd[:12]) & 0xFF + logger.info( + f"write force disable charging: {'true' if self.trigger_force_disable_charge else 'false'}" + ) + self.trigger_force_disable_charge = None + ser.flushOutput() + ser.flushInput() + ser.write(cmd) + + reply = self.read_sentence(ser, self.command_disable_charge_mos) + if reply is False or reply[0] != cmd[4]: + logger.error("write force disable charge/discharge failed") + return False + + if self.trigger_force_disable_discharge is not None: + cmd[2] = self.command_disable_discharge_mos[0] + cmd[4] = 0 if self.trigger_force_disable_discharge else 1 + cmd[12] = sum(cmd[:12]) & 0xFF + logger.info( + f"write force disable discharging: {'true' if self.trigger_force_disable_discharge else 'false'}" + ) + self.trigger_force_disable_discharge = None + ser.flushOutput() + ser.flushInput() + ser.write(cmd) + + reply = self.read_sentence(ser, self.command_disable_discharge_mos) + if reply is False or reply[0] != cmd[4]: + logger.error("write force disable charge/discharge failed") + return False + return True + + def generate_command(self, command): + buffer = bytearray(self.command_base) + buffer[1] = self.command_address[0] # Always serial 40 or 80 + buffer[2] = command[0] + buffer[12] = sum(buffer[:12]) & 0xFF # checksum calc + return buffer + + def request_data(self, ser, command, sentences_to_receive=1): + # wait shortly, else the Daly is not ready and throws a lot of no reply errors + # if you see a lot of errors, try to increase in steps of 0.005 + sleep(0.020) + + self.runtime = 0 + time_start = time() + ser.flushOutput() + ser.flushInput() + ser.write(self.generate_command(command)) + + reply = bytearray() + for i in range(sentences_to_receive): + next = self.read_sentence(ser, command) + if not next: + logger.debug(f"request_data: bad reply no. {i}") + return False + reply += next + self.runtime = time() - time_start + return reply + + def read_sentence(self, ser, expected_reply, timeout=0.5): + """read one 13 byte sentence from daly smart bms. + return false if less than 13 bytes received in timeout secs, or frame errors occured + return received datasection as bytearray else + """ + time_start = time() + + reply = ser.read_until(b"\xA5") + if not reply or b"\xA5" not in reply: + logger.debug( + f"read_sentence {bytes(expected_reply).hex()}: no sentence start received" + ) + return False + + idx = reply.index(b"\xA5") + reply = reply[idx:] toread = ser.inWaiting() - while toread < 13: - sleep(0.005) + while toread < 12: + sleep((12 - toread) * 0.001) toread = ser.inWaiting() time_run = time() - time_start - if time_run > 0.500: - logger.warning("write soc: no reply, probably failed") + if time_run > timeout: + logger.debug(f"read_sentence {bytes(expected_reply).hex()}: timeout") return False - reply = ser.read(toread) - if reply[4] != 1: - logger.error("write soc failed") - return True + reply += ser.read(12) + _, id, cmd, length = unpack_from(">BBBB", reply) + + # logger.info(f"reply: {bytes(reply).hex()}") # debug + + if id != 1 or length != 8 or cmd != expected_reply[0]: + logger.debug(f"read_sentence {bytes(expected_reply).hex()}: wrong header") + return False + + chk = unpack_from(">B", reply, 12)[0] + if sum(reply[:12]) & 0xFF != chk: + logger.debug(f"read_sentence {bytes(expected_reply).hex()}: wrong checksum") + return False + + return reply[4:12] diff --git a/etc/dbus-serialbattery/bms/ecs.py b/etc/dbus-serialbattery/bms/ecs.py index a50e6072..ddfdf901 100644 --- a/etc/dbus-serialbattery/bms/ecs.py +++ b/etc/dbus-serialbattery/bms/ecs.py @@ -32,6 +32,7 @@ def test_connection(self): # Return True if success, False for failure # Trying to find Green Meter ID + result = False try: mbdev = minimalmodbus.Instrument(self.port, utils.GREENMETER_ADDRESS) mbdev.serial.parity = minimalmodbus.serial.PARITY_EVEN @@ -44,14 +45,28 @@ def test_connection(self): if tmpId == self.GREENMETER_ID_125A: self.METER_SIZE = "125A" + # TODO + # has this to be true? + # if yes then self.get_settings() should only be called, if this is true self.find_LiPro_cells() - # get first data to show in startup log - self.refresh_data() + result = self.get_settings() + + # get first data to show in startup log, only if result is true + if result: + self.refresh_data() - return self.get_settings() except IOError: - return False + result = False + except Exception as err: + logger.error(f"Unexpected {err=}, {type(err)=}") + result = False + + # give the user a feedback that no BMS was found + if not result: + logger.error(">>> ERROR: No reply - returning") + + return result def find_LiPro_cells(self): # test for LiPro cell devices diff --git a/etc/dbus-serialbattery/bms/heltecmodbus.py b/etc/dbus-serialbattery/bms/heltecmodbus.py new file mode 100644 index 00000000..e7f866a0 --- /dev/null +++ b/etc/dbus-serialbattery/bms/heltecmodbus.py @@ -0,0 +1,445 @@ +# -*- coding: utf-8 -*- +# known limitations: +# - only BMS variants with 2 cell temperature sensors supported +# - some "interesting" datapoints are not read (e. g. registers 52: switch type, 62: bootloader and firmware version) +# - SOC not yet resettable from Venus (similary to Daly for support of writing SOC), but modbus write to 120 should be +# fairly possible) + + +from battery import Battery, Cell +from utils import logger +import utils +import serial +import time +import minimalmodbus +from typing import Dict +import threading + +# the Heltec BMS is not always as responsive as it should, so let's try it up to (RETRYCNT - 1) times to talk to it +RETRYCNT = 10 + +# the wait time after a communication - normally this should be as defined by modbus RTU and handled in minimalmodbus, +# but yeah, it seems we need it for the Heltec BMS +SLPTIME = 0.03 + +mbdevs: Dict[int, minimalmodbus.Instrument] = {} +locks: Dict[int, any] = {} + + +class HeltecModbus(Battery): + def __init__(self, port, baud, address): + super(HeltecModbus, self).__init__(port, baud, address) + self.type = "Heltec_Smart" + + def test_connection(self): + # call a function that will connect to the battery, send a command and retrieve the result. + # The result or call should be unique to this BMS. Battery name or version, etc. + # Return True if success, False for failure + for self.address in utils.HELTEC_MODBUS_ADDR: + logger.debug("Testing on slave address " + str(self.address)) + found = False + if self.address not in locks: + locks[self.address] = threading.Lock() + + # TODO: We need to lock not only based on the address, but based on the port as soon as multiple BMSs + # are supported on the same serial interface. Then locking on the port will be enough. + + with locks[self.address]: + mbdev = minimalmodbus.Instrument( + self.port, + slaveaddress=self.address, + mode="rtu", + close_port_after_each_call=True, + debug=False, + ) + mbdev.serial.parity = minimalmodbus.serial.PARITY_NONE + mbdev.serial.stopbits = serial.STOPBITS_ONE + mbdev.serial.baudrate = 9600 + # yes, 400ms is long but the BMS is sometimes really slow in responding, so this is a good compromise + mbdev.serial.timeout = 0.4 + mbdevs[self.address] = mbdev + + for n in range(1, RETRYCNT): + try: + string = mbdev.read_string(7, 13) + time.sleep(SLPTIME) + found = True + logger.debug( + "found in try " + + str(n) + + "/" + + str(RETRYCNT) + + " for " + + self.port + + "(" + + str(self.address) + + "): " + + string + ) + except Exception as e: + logger.debug( + "testing failed (" + + str(e) + + ") " + + str(n) + + "/" + + str(RETRYCNT) + + " for " + + self.port + + "(" + + str(self.address) + + ")" + ) + continue + break + if found: + self.type = "#" + str(self.address) + "_Heltec_Smart" + break + + # give the user a feedback that no BMS was found + if not found: + logger.error(">>> ERROR: No reply - returning") + + return ( + found + and self.read_status_data() + and self.get_settings() + and self.refresh_data() + ) + + def get_settings(self): + self.max_battery_voltage = self.max_cell_voltage * self.cell_count + self.min_battery_voltage = self.min_cell_voltage * self.cell_count + + return True + + def refresh_data(self): + # call all functions that will refresh the battery data. + # This will be called for every iteration (1 second) + # Return True if success, False for failure + return self.read_soc_data() and self.read_cell_data() + + def read_status_data(self): + mbdev = mbdevs[self.address] + + with locks[self.address]: + for n in range(1, RETRYCNT + 1): + try: + ccur = mbdev.read_register(191, 0, 3, False) + self.max_battery_charge_current = ( + (int)(((ccur & 0xFF) << 8) | ((ccur >> 8) & 0xFF)) + ) / 100 + time.sleep(SLPTIME) + + dc = mbdev.read_register(194, 0, 3, False) + self.max_battery_discharge_current = ( + ((dc & 0xFF) << 8) | ((dc >> 8) & 0xFF) + ) / 100 + time.sleep(SLPTIME) + + cap = mbdev.read_register(118, 0, 3, False) + self.capacity = (((cap & 0xFF) << 8) | ((cap >> 8) & 0xFF)) / 10 + time.sleep(SLPTIME) + + cap = mbdev.read_register(119, 0, 3, False) + self.actual_capacity = ( + ((cap & 0xFF) << 8) | ((cap >> 8) & 0xFF) + ) / 10 + time.sleep(SLPTIME) + + cap = mbdev.read_register(126, 0, 3, False) + self.learned_capacity = ( + ((cap & 0xFF) << 8) | ((cap >> 8) & 0xFF) + ) / 10 + time.sleep(SLPTIME) + + volt = mbdev.read_register(169, 0, 3, False) + self.max_cell_voltage = ( + ((volt & 0xFF) << 8) | ((volt >> 8) & 0xFF) + ) / 1000 + time.sleep(SLPTIME) + + volt = mbdev.read_register(172, 0, 3, False) + self.min_cell_voltage = ( + ((volt & 0xFF) << 8) | ((volt >> 8) & 0xFF) + ) / 1000 + time.sleep(SLPTIME) + + string = mbdev.read_string(7, 13) + self.hwTypeName = string + time.sleep(SLPTIME) + + string = mbdev.read_string(41, 6) + self.devName = string + time.sleep(SLPTIME) + + serial1 = mbdev.read_registers(2, number_of_registers=4) + self.unique_identifier = "-".join( + "{:04x}".format(x) for x in serial1 + ) + time.sleep(SLPTIME) + + self.pw = mbdev.read_string(47, 2) + time.sleep(SLPTIME) + + tmp = mbdev.read_register(75) + # h: batterytype: 0: Ternery Lithium, 1: Iron Lithium, 2: Lithium Titanat + # l: #of cells + + self.cell_count = (tmp >> 8) & 0xFF + tmp = tmp & 0xFF + if tmp == 0: + self.cellType = "Ternary Lithium" + elif tmp == 1: + self.cellType = "Iron Lithium" + elif tmp == 2: + self.cellType = "Lithium Titatnate" + else: + self.cellType = "unknown" + time.sleep(SLPTIME) + + self.hardware_version = ( + self.devName + + "(" + + str((mbdev.read_register(38) >> 8) & 0xFF) + + ")" + ) + time.sleep(SLPTIME) + + date = mbdev.read_long(39, 3, True, minimalmodbus.BYTEORDER_LITTLE) + self.production_date = ( + str(date & 0xFFFF) + + "-" + + str((date >> 24) & 0xFF) + + "-" + + str((date >> 16) & 0xFF) + ) + time.sleep(SLPTIME) + + # we finished all readings without trouble, so let's break from the retry loop + break + except Exception as e: + logger.warn( + "Error reading settings from BMS, retry (" + + str(n) + + "/" + + str(RETRYCNT) + + "): " + + str(e) + ) + if n == RETRYCNT: + return False + continue + + logger.info(self.hardware_version) + logger.info("Heltec-" + self.hwTypeName) + logger.info(" Dev name: " + self.devName) + logger.info(" Serial: " + self.unique_identifier) + logger.info(" Made on: " + self.production_date) + logger.info(" Cell count: " + str(self.cell_count)) + logger.info(" Cell type: " + self.cellType) + logger.info(" BT password: " + self.pw) + logger.info(" rated capacity: " + str(self.capacity)) + logger.info(" actual capacity: " + str(self.actual_capacity)) + logger.info(" learned capacity: " + str(self.learned_capacity)) + + return True + + def read_soc_data(self): + mbdev = mbdevs[self.address] + + with locks[self.address]: + for n in range(1, RETRYCNT): + try: + self.voltage = ( + mbdev.read_long(76, 3, True, minimalmodbus.BYTEORDER_LITTLE) + / 1000 + ) + time.sleep(SLPTIME) + + self.current = -( + mbdev.read_long(78, 3, True, minimalmodbus.BYTEORDER_LITTLE) + / 100 + ) + time.sleep(SLPTIME) + + runState1 = mbdev.read_long( + 152, 3, True, minimalmodbus.BYTEORDER_LITTLE + ) + time.sleep(SLPTIME) + + # bit 29 is discharge protection + if (runState1 & 0x20000000) == 0: + self.discharge_fet = True + else: + self.discharge_fet = False + + # bit 28 is charge protection + if (runState1 & 0x10000000) == 0: + self.charge_fet = True + else: + self.charge_fet = False + + warnings = mbdev.read_long( + 156, 3, True, minimalmodbus.BYTEORDER_LITTLE + ) + if (warnings & (1 << 3)) or ( + warnings & (1 << 15) + ): # 15 is full protection, 3 is total overvoltage + self.voltage_high = 2 + else: + self.voltage_high = 0 + + if warnings & (1 << 0): + self.protection.voltage_cell_high = 2 + # we handle a single cell OV as total OV, as long as cell_high is not explicitly handled + self.protection.voltage_high = 1 + else: + self.protection.voltage_cell_high = 0 + + if warnings & (1 << 1): + self.protection.voltage_cell_low = 2 + else: + self.protection.voltage_cell_low = 0 + + if warnings & (1 << 4): + self.protection.voltage_low = 2 + else: + self.protection.voltage_low = 0 + + if warnings & (1 << 5): + self.protection.current_over = 2 + else: + self.protection.current_over = 0 + + if warnings & (1 << 7): + self.protection.current_under = 2 + elif warnings & (1 << 6): + self.protection.current_under = 1 + else: + self.protection.current_under = 0 + + if warnings & (1 << 8): # this is a short circuit + self.protection.current_over = 2 + + if warnings & (1 << 9): + self.protection.temp_high_charge = 2 + else: + self.protection.temp_high_charge = 0 + + if warnings & (1 << 10): + self.protection.temp_low_charge = 2 + else: + self.protection.temp_low_charge = 0 + + if warnings & (1 << 11): + self.protection.temp_high_discharge = 2 + else: + self.protection.temp_high_discharge = 0 + + if warnings & (1 << 12): + self.protection.temp_low_discharge = 2 + else: + self.protection.temp_low_discharge = 0 + + if warnings & (1 << 13): # MOS overtemp + self.protection.temp_high_internal = 2 + else: + self.protection.temp_high_internal = 0 + + if warnings & (1 << 14): # SOC low + self.protection.soc_low = 2 + else: + self.protection.soc_low = 0 + + if warnings & (0xFFFF0000): # any other fault + self.protection.internal_failure = 2 + else: + self.protection.internal_failure = 0 + + socsoh = mbdev.read_register(120, 0, 3, False) + self.soh = socsoh & 0xFF + self.soc = (socsoh >> 8) & 0xFF + time.sleep(SLPTIME) + + # we could read min and max temperature, here, but I have a BMS with only 2 sensors, + # so I couldn't test the logic and read therefore only the first two temperatures + # tminmax = mbdev.read_register(117, 0, 3, False) + # nmin = (tminmax & 0xFF) + # nmax = ((tminmax >> 8) & 0xFF) + + temps = mbdev.read_register(113, 0, 3, False) + self.temp1 = (temps & 0xFF) - 40 + self.temp2 = ((temps >> 8) & 0xFF) - 40 + time.sleep(SLPTIME) + + temps = mbdev.read_register(112, 0, 3, False) + most = (temps & 0xFF) - 40 + balt = ((temps >> 8) & 0xFF) - 40 + # balancer temperature is not handled separately in dbus-serialbattery, + # so let's display the max of both temperatures inside the BMS as mos temperature + self.temp_mos = max(most, balt) + time.sleep(SLPTIME) + + return True + + except Exception as e: + logger.warn( + "Error reading SOC, retry (" + + str(n) + + "/" + + str(RETRYCNT) + + ") " + + str(e) + ) + continue + break + logger.warn("Error reading SOC, failed") + return False + + def read_cell_data(self): + result = False + mbdev = mbdevs[self.address] + + with locks[self.address]: + for n in range(1, RETRYCNT): + try: + cells = mbdev.read_registers( + 81, number_of_registers=self.cell_count + ) + time.sleep(SLPTIME) + + balancing = mbdev.read_long( + 139, 3, signed=False, byteorder=minimalmodbus.BYTEORDER_LITTLE + ) + time.sleep(SLPTIME) + + result = True + except Exception as e: + logger.warn( + "read_cell_data() failed (" + + str(e) + + ") " + + str(n) + + "/" + + str(RETRYCNT) + ) + continue + break + if result is False: + return False + + if len(self.cells) != self.cell_count: + self.cells = [] + for idx in range(self.cell_count): + self.cells.append(Cell(False)) + + i = 0 + for cell in cells: + cellV = ((cell & 0xFF) << 8) | ((cell >> 8) & 0xFF) + self.cells[i].voltage = cellV / 1000 + self.cells[i].balance = balancing & (1 << i) != 0 + + i = i + 1 + + return True diff --git a/etc/dbus-serialbattery/bms/hlpdatabms4s.py b/etc/dbus-serialbattery/bms/hlpdatabms4s.py index d4a350b1..7faf8b2c 100644 --- a/etc/dbus-serialbattery/bms/hlpdatabms4s.py +++ b/etc/dbus-serialbattery/bms/hlpdatabms4s.py @@ -24,6 +24,10 @@ def test_connection(self): logger.error(f"Unexpected {err=}, {type(err)=}") result = False + # give the user a feedback that no BMS was found + if not result: + logger.error(">>> ERROR: No reply - returning") + return result def get_settings(self): @@ -210,6 +214,9 @@ def read_serial_data2(command, port, baud, time, min_len): logger.error(e) return False + except Exception: + return False + def read_serialport_data2(ser, command, time, min_len): try: diff --git a/etc/dbus-serialbattery/bms/jkbms_ble.py b/etc/dbus-serialbattery/bms/jkbms_ble.py index 03a6e47e..276103c4 100644 --- a/etc/dbus-serialbattery/bms/jkbms_ble.py +++ b/etc/dbus-serialbattery/bms/jkbms_ble.py @@ -14,6 +14,7 @@ class Jkbms_Ble(Battery): def __init__(self, port, baud, address): super(Jkbms_Ble, self).__init__(address.replace(":", "").lower(), baud, address) + self.address = address self.type = self.BATTERYTYPE self.jk = Jkbms_Brn(address) diff --git a/etc/dbus-serialbattery/bms/lifepower.py b/etc/dbus-serialbattery/bms/lifepower.py index 84c93a37..b46421fb 100644 --- a/etc/dbus-serialbattery/bms/lifepower.py +++ b/etc/dbus-serialbattery/bms/lifepower.py @@ -38,7 +38,7 @@ def get_settings(self): # After successful connection get_settings will be call to set up the battery. # Set the current limits, populate cell count, etc # Return True if success, False for failure - self.max_battery_current = utils.MAX_BATTERY_CURRENT + self.max_battery_charge_current = utils.MAX_BATTERY_CHARGE_CURRENT self.max_battery_discharge_current = utils.MAX_BATTERY_DISCHARGE_CURRENT hardware_version = self.read_serial_data_eg4(self.command_hardware_version) if hardware_version: @@ -159,7 +159,7 @@ def read_serial_data_eg4(self, command): self.LENGTH_FIXED, ) if data is False: - logger.error(">>> ERROR: Incorrect Data") + logger.debug(">>> ERROR: Incorrect Data") return False # 0x0D always terminates the response diff --git a/etc/dbus-serialbattery/bms/lltjbd.py b/etc/dbus-serialbattery/bms/lltjbd.py index a74697cc..63de584b 100644 --- a/etc/dbus-serialbattery/bms/lltjbd.py +++ b/etc/dbus-serialbattery/bms/lltjbd.py @@ -5,6 +5,166 @@ from struct import unpack_from import struct +# Protocol registers +REG_ENTER_FACTORY = 0x00 +REG_EXIT_FACTORY = 0x01 +# REG_UNKNOWN = 0x02 +REG_GENERAL = 0x03 +REG_CELL = 0x04 +REG_HARDWARE = 0x05 +# Firmware 0x16+ +REG_USE_PASSWORD = 0x06 +REG_SET_PASSWORD = 0x07 +# REG_UNKNOWN2 = 0x08 - Maybe define master password? +REG_CLEAR_PASSWORD = 0x09 + +REG_FRESET = 0x0A + +REG_DESIGN_CAP = 0x10 +REG_CYCLE_CAP = 0x11 +REG_CAP_100 = 0x12 +REG_CAP_0 = 0x13 +REG_SELF_DSG_RATE = 0x14 +REG_MFG_DATE = 0x15 +REG_SERIAL_NUM = 0x16 +REG_CYCLE_CNT = 0x17 +REG_CHGOT = 0x18 +REG_CHGOT_REL = 0x19 +REG_CHGUT = 0x1A +REG_CHGUT_REL = 0x1B +REG_DSGOT = 0x1C +REG_DSGOT_REL = 0x1D +REG_DSGUT = 0x1E +REG_DSGUT_REL = 0x1F +REG_POVP = 0x20 +REG_POVP_REL = 0x21 +REG_PUVP = 0x22 +REG_PUVP_REL = 0x23 +REG_COVP = 0x24 +REG_COVP_REL = 0x25 +REG_CUVP = 0x26 +REG_CUVP_REL = 0x27 +REG_CHGOC = 0x28 +REG_DSGOC = 0x29 +REG_BAL_START = 0x2A +REG_BAL_WINDOW = 0x2B +REG_SHUNT_RES = 0x2C +REG_FUNC_CONFIG = 0x2D +REG_NTC_CONFIG = 0x2E +REG_CELL_CNT = 0x2F +REG_FET_TIME = 0x30 +REG_LED_TIME = 0x31 +REG_CAP_80 = 0x32 +REG_CAP_60 = 0x33 +REG_CAP_40 = 0x34 +REG_CAP_20 = 0x35 +REG_COVP_HIGH = 0x36 +REG_CUVP_HIGH = 0x37 +REG_SC_DSGOC2 = 0x38 +REG_CXVP_HIGH_DELAY_SC_REL = 0x39 +REG_CHG_T_DELAYS = 0x3A +REG_DSG_T_DELAYS = 0x3B +REG_PACK_V_DELAYS = 0x3C +REG_CELL_V_DELAYS = 0x3D +REG_CHGOC_DELAYS = 0x3E +REG_DSGOC_DELAYS = 0x3F +REG_GPSOFF = 0x40 +REG_GPSOFF_TIME = 0x41 +REG_CAP_90 = 0x42 +REG_CAP_70 = 0x43 +REG_CAP_50 = 0x44 +REG_CAP_30 = 0x45 +REG_CAP_10 = 0x46 +# REG_CAP2_100 = 0x47 + +# [0x48, 0x9F] - 87 registers + +REG_MFGNAME = 0xA0 +REG_MODEL = 0xA1 +REG_BARCODE = 0xA2 +REG_ERROR = 0xAA +# 0xAB +# 0xAC +REG_CAL_CUR_IDLE = 0xAD +REG_CAL_CUR_CHG = 0xAE +REG_CAL_CUR_DSG = 0xAF + +REG_CAL_V_CELL_01 = 0xB0 +REG_CAL_V_CELL_02 = 0xB1 +REG_CAL_V_CELL_03 = 0xB2 +REG_CAL_V_CELL_04 = 0xB3 +REG_CAL_V_CELL_05 = 0xB4 +REG_CAL_V_CELL_06 = 0xB5 +REG_CAL_V_CELL_07 = 0xB6 +REG_CAL_V_CELL_08 = 0xB7 +REG_CAL_V_CELL_09 = 0xB8 +REG_CAL_V_CELL_10 = 0xB9 +REG_CAL_V_CELL_11 = 0xBA +REG_CAL_V_CELL_12 = 0xBB +REG_CAL_V_CELL_13 = 0xBC +REG_CAL_V_CELL_14 = 0xBD +REG_CAL_V_CELL_15 = 0xBE +REG_CAL_V_CELL_16 = 0xBF +REG_CAL_V_CELL_17 = 0xC0 +REG_CAL_V_CELL_18 = 0xC1 +REG_CAL_V_CELL_19 = 0xC2 +REG_CAL_V_CELL_20 = 0xC3 +REG_CAL_V_CELL_21 = 0xC4 +REG_CAL_V_CELL_22 = 0xC5 +REG_CAL_V_CELL_23 = 0xC6 +REG_CAL_V_CELL_24 = 0xC7 +REG_CAL_V_CELL_25 = 0xC8 +REG_CAL_V_CELL_26 = 0xC9 +REG_CAL_V_CELL_27 = 0xCA +REG_CAL_V_CELL_28 = 0xCB +REG_CAL_V_CELL_29 = 0xCC +REG_CAL_V_CELL_30 = 0xCD +REG_CAL_V_CELL_31 = 0xCE +REG_CAL_V_CELL_32 = 0xCF + +REG_CAL_T_NTC_0 = 0xD0 +REG_CAL_T_NTC_1 = 0xD1 +REG_CAL_T_NTC_2 = 0xD2 +REG_CAL_T_NTC_3 = 0xD3 +REG_CAL_T_NTC_4 = 0xD4 +REG_CAL_T_NTC_5 = 0xD5 +REG_CAL_T_NTC_6 = 0xD6 +REG_CAL_T_NTC_7 = 0xD7 + +REG_CAP_REMAINING = 0xE0 +REG_CTRL_MOSFET = 0xE1 +REG_CTRL_BALANCE = 0xE2 +REG_RESET = 0xE3 + +# Protocol commands +CMD_ENTER_FACTORY_MODE = b"\x56\x78" +CMD_EXIT_FACTORY_MODE = b"\x00\x00" +CMD_EXIT_AND_SAVE_FACTORY_MODE = b"\x28\x28" + + +def checksum(payload): + return (0x10000 - sum(payload)) % 0x10000 + + +def cmd(op, reg, data): + payload = [reg, len(data)] + list(data) + chksum = checksum(payload) + data = [0xDD, op] + payload + [chksum, 0x77] + format = f">BB{len(payload)}BHB" + return struct.pack(format, *data) + + +def readCmd(reg, data=None): + if data is None: + data = [] + return cmd(0xA5, reg, data) + + +def writeCmd(reg, data=None): + if data is None: + data = [] + return cmd(0x5A, reg, data) + class LltJbdProtection(Protection): def __init__(self): @@ -51,24 +211,32 @@ def __init__(self, port, baud, address): super(LltJbd, self).__init__(port, baud, address) self.protection = LltJbdProtection() self.type = self.BATTERYTYPE + self._product_name: str = "" + self.has_settings = 0 + self.reset_soc = 100 + self.soc_to_set = None + self.factory_mode = False + self.writable = False # degree_sign = u'\N{DEGREE SIGN}' - command_general = b"\xDD\xA5\x03\x00\xFF\xFD\x77" - command_cell = b"\xDD\xA5\x04\x00\xFF\xFC\x77" - command_hardware = b"\xDD\xA5\x05\x00\xFF\xFB\x77" BATTERYTYPE = "LLT/JBD" LENGTH_CHECK = 6 LENGTH_POS = 3 + command_general = readCmd(REG_GENERAL) # b"\xDD\xA5\x03\x00\xFF\xFD\x77" + command_cell = readCmd(REG_CELL) # b"\xDD\xA5\x04\x00\xFF\xFC\x77" + command_hardware = readCmd(REG_HARDWARE) # b"\xDD\xA5\x05\x00\xFF\xFB\x77" + def test_connection(self): # call a function that will connect to the battery, send a command and retrieve the result. # The result or call should be unique to this BMS. Battery name or version, etc. # Return True if success, False for failure result = False try: - result = self.read_hardware_data() - # get first data to show in startup log + result = self.get_settings() + # get first data to show in startup log, only if result is true if result: + self.read_hardware_data() self.refresh_data() except Exception as err: logger.error(f"Unexpected {err=}, {type(err)=}") @@ -76,12 +244,50 @@ def test_connection(self): return result + def product_name(self) -> str: + return self._product_name + def get_settings(self): - self.read_gen_data() + if not self.read_gen_data(): + return False self.max_battery_charge_current = utils.MAX_BATTERY_CHARGE_CURRENT self.max_battery_discharge_current = utils.MAX_BATTERY_DISCHARGE_CURRENT + with self.eeprom(writable=False): + charge_over_current = self.read_serial_data_llt(readCmd(REG_CHGOC)) + if charge_over_current: + self.max_battery_charge_current = float( + unpack_from(">h", charge_over_current)[0] / 100.0 + ) + discharge_over_current = self.read_serial_data_llt(readCmd(REG_DSGOC)) + if discharge_over_current: + self.max_battery_discharge_current = float( + unpack_from(">h", discharge_over_current)[0] / -100.0 + ) + return True + def reset_soc_callback(self, path, value): + if value is None: + return False + + if value < 0 or value > 100: + return False + + self.reset_soc = value + self.soc_to_set = value + return True + + def write_soc(self): + if self.soc_to_set is None or self.soc_to_set != 100 or not self.voltage: + return False + logger.info(f"write soc {self.soc_to_set}%") + self.soc_to_set = None # Reset value, so we will set it only once + # TODO implement logic to map current pack readings into + # REG_CAP_100, REG_CAP_90, REG_CAP_80, REG_CAP_70, REG_CAP_60, ... + with self.eeprom(writable=True): + pack_voltage = struct.pack(">H", int(self.voltage * 10)) + self.read_serial_data_llt(writeCmd(REG_CAP_100, pack_voltage)) + def refresh_data(self): result = self.read_gen_data() result = result and self.read_cell_data() @@ -116,6 +322,43 @@ def to_protection_bits(self, byte_data): self.protection.set_short = is_bit_set(tmp[2]) def to_cell_bits(self, byte_data, byte_data_high): + # init the cell array once + if len(self.cells) == 0: + for _ in range(self.cell_count): + print("#" + str(_)) + self.cells.append(Cell(False)) + + # get up to the first 16 cells + tmp = bin(byte_data)[2:].rjust(min(self.cell_count, 16), utils.zero_char) + # 4 cells + # tmp = 0101 + # 16 cells + # tmp = 0101010101010101 + + tmp_reversed = list(reversed(tmp)) + # print(tmp_reversed) --> ['1', '0', '1', '0', '1', '0', '1', '0', '1', '0', '1', '0', '1', '0', '1', '0'] + # [cell1, cell2, cell3, ...] + + if self.cell_count > 16: + tmp2 = bin(byte_data_high)[2:].rjust(self.cell_count - 16, utils.zero_char) + # tmp = 1100110011001100 + tmp_reversed = tmp_reversed + list(reversed(tmp2)) + # print(tmp_reversed) --> [ + # '1', '0', '1', '0', '1', '0', '1', '0', '1', '0', '1', '0', '1', '0', '1', '0', + # '0', '0', '1', '1', '0', '0', '1', '1', '0', '0', '1', '1', '0', '0', '1', '1' + # ] + # [ + # cell1, cell2, ..., cell16, + # cell17, cell18, ..., cell32 + # ] + + for c in range(self.cell_count): + if is_bit_set(tmp_reversed[c]): + self.cells[c].balance = True + else: + self.cells[c].balance = False + + """ # clear the list for c in self.cells: self.cells.remove(c) @@ -128,6 +371,7 @@ def to_cell_bits(self, byte_data, byte_data_high): tmp = bin(byte_data_high)[2:].rjust(self.cell_count - 16, utils.zero_char) for bit in reversed(tmp): self.cells.append(Cell(is_bit_set(bit))) + """ def to_fet_bits(self, byte_data): tmp = bin(byte_data)[2:].rjust(2, utils.zero_char) @@ -162,7 +406,9 @@ def read_gen_data(self): self.capacity_remain = capacity_remain / 100 self.capacity = capacity / 100 self.to_cell_bits(balance, balance2) - self.version = float(str(version >> 4 & 0x0F) + "." + str(version & 0x0F)) + self.hardware_version = float( + str(version >> 4 & 0x0F) + "." + str(version & 0x0F) + ) self.to_fet_bits(fet) self.to_protection_bits(protection) self.max_battery_voltage = utils.MAX_CELL_VOLTAGE * self.cell_count @@ -196,24 +442,73 @@ def read_hardware_data(self): if hardware_data is False: return False - self.hardware_version = unpack_from( + self._product_name = unpack_from( ">" + str(len(hardware_data)) + "s", hardware_data - )[0].decode() - logger.debug(self.hardware_version) + )[0].decode("ascii", errors="ignore") + logger.debug(self._product_name) return True + @staticmethod + def validate_packet(data): + if not data: + return False + + if data is False: + return False + + start, op, status, payload_length = unpack_from("BBBB", data) + if start != 0xDD: + logger.error( + ">>> ERROR: Invalid response packet. Expected begin packet character 0xDD" + ) + if status != 0x0: + logger.warn(">>> WARN: BMS rejected request. Status " + status) + return False + if len(data) != payload_length + 7: + logger.error( + ">>> ERROR: BMS send insufficient data. Received " + + str(len(data)) + + " expected " + + str(payload_length + 7) + ) + return False + chk_sum, end = unpack_from(">HB", data, payload_length + 4) + if end != 0x77: + logger.error( + ">>> ERROR: Incorrect Reply. Expected end packet character 0x77" + ) + return False + if chk_sum != checksum(data[2:-3]): + logger.error(">>> ERROR: Invalid checksum.") + return False + + payload = data[4 : payload_length + 4] + + return payload + def read_serial_data_llt(self, command): data = read_serial_data( command, self.port, self.baud_rate, self.LENGTH_POS, self.LENGTH_CHECK ) - if data is False: - return False + return self.validate_packet(data) - start, flag, command_ret, length = unpack_from("BBBB", data) - checksum, end = unpack_from("HB", data, length + 4) + def __enter__(self): + if self.read_serial_data_llt( + writeCmd(REG_ENTER_FACTORY, CMD_ENTER_FACTORY_MODE) + ): + self.factory_mode = True - if end == 119: - return data[4 : length + 4] - else: - logger.error(">>> ERROR: Incorrect Reply") - return False + def __exit__(self, type, value, traceback): + cmd_value = ( + CMD_EXIT_AND_SAVE_FACTORY_MODE if self.writable else CMD_EXIT_FACTORY_MODE + ) + if self.factory_mode: + if not self.read_serial_data_llt(writeCmd(REG_EXIT_FACTORY, cmd_value)): + logger.error(">>> ERROR: Unable to exit factory mode.") + else: + self.factory_mode = False + self.writable = False + + def eeprom(self, writable=False): + self.writable = writable + return self diff --git a/etc/dbus-serialbattery/bms/lltjbd_ble.py b/etc/dbus-serialbattery/bms/lltjbd_ble.py index fa4b38da..de995492 100644 --- a/etc/dbus-serialbattery/bms/lltjbd_ble.py +++ b/etc/dbus-serialbattery/bms/lltjbd_ble.py @@ -3,13 +3,12 @@ import atexit import functools import threading +from asyncio import CancelledError from typing import Union, Optional from utils import logger -from struct import unpack_from from bleak import BleakClient, BleakScanner, BLEDevice from bms.lltjbd import LltJbdProtection, LltJbd - BLE_SERVICE_UUID = "0000ff00-0000-1000-8000-00805f9b34fb" BLE_CHARACTERISTICS_TX_UUID = "0000ff02-0000-1000-8000-00805f9b34fb" BLE_CHARACTERISTICS_RX_UUID = "0000ff01-0000-1000-8000-00805f9b34fb" @@ -21,7 +20,9 @@ class LltJbd_Ble(LltJbd): BATTERYTYPE = "LltJbd_Ble" def __init__(self, port: Optional[str], baud: Optional[int], address: str): - super(LltJbd_Ble, self).__init__(address.replace(":", "").lower(), -1, address) + super(LltJbd_Ble, self).__init__( + "ble" + address.replace(":", "").lower(), -1, address + ) self.address = address self.protection = LltJbdProtection() @@ -50,9 +51,14 @@ def on_disconnect(self, client): logger.info("BLE client disconnected") async def bt_main_loop(self): - self.device = await BleakScanner.find_device_by_address( - self.address, cb=dict(use_bdaddr=True) - ) + try: + self.device = await BleakScanner.find_device_by_address( + self.address, cb=dict(use_bdaddr=True) + ) + except Exception as e: + logger.error(">>> ERROR: Bluetooth stack failed.", e) + self.device = None + await asyncio.sleep(0.5) if not self.device: self.run = False @@ -155,22 +161,18 @@ async def async_read_serial_data_llt(self, command): def read_serial_data_llt(self, command): if not self.bt_loop: return False - data = asyncio.run(self.async_read_serial_data_llt(command)) - if not data: + try: + data = asyncio.run(self.async_read_serial_data_llt(command)) + return self.validate_packet(data) + except CancelledError as e: + logger.error(">>> ERROR: No reply - canceled - returning", e) return False - - start, flag, command_ret, length = unpack_from("BBBB", data) - checksum, end = unpack_from("HB", data, length + 4) - - if end == 119: - return data[4 : length + 4] - else: - logger.error(">>> ERROR: Incorrect Reply") + except Exception as e: + logger.error(">>> ERROR: No reply - returning", e) return False -""" -async def test_LltJbd_Ble(): +if __name__ == "__main__": import sys bat = LltJbd_Ble("Foo", -1, sys.argv[1]) @@ -178,8 +180,4 @@ async def test_LltJbd_Ble(): logger.error(">>> ERROR: Unable to connect") else: bat.refresh_data() - - -if __name__ == "__main__": - test_LltJbd_Ble() -""" + bat.get_settings() diff --git a/etc/dbus-serialbattery/bms/mnb.py b/etc/dbus-serialbattery/bms/mnb.py index 1ad1126e..84365866 100644 --- a/etc/dbus-serialbattery/bms/mnb.py +++ b/etc/dbus-serialbattery/bms/mnb.py @@ -169,9 +169,9 @@ def manage_charge_current(self): # Change depending on the cell_min_voltage values if self.cell_min_voltage < self.V_C_min + 0.05: - self.control_allow_dicharge = False + self.control_allow_discharge = False else: - self.control_allow_dicharge = True + self.control_allow_discharge = True if self.cell_min_voltage < self.V_C_min + 0.15: b = 10 * (self.cell_min_voltage - self.V_C_min - 0.05) diff --git a/etc/dbus-serialbattery/bms/seplos.py b/etc/dbus-serialbattery/bms/seplos.py index 083d6bc4..b7c9a2e1 100644 --- a/etc/dbus-serialbattery/bms/seplos.py +++ b/etc/dbus-serialbattery/bms/seplos.py @@ -79,12 +79,18 @@ def test_connection(self): # call a function that will connect to the battery, send a command and retrieve the result. # The result or call should be unique to this BMS. Battery name or version, etc. # Return True if success, False for failure - + result = False try: - return self.read_status_data() + result = self.read_status_data() except Exception as err: logger.error(f"Unexpected {err=}, {type(err)=}") - return False + result = False + + # give the user a feedback that no BMS was found + if not result: + logger.error(">>> ERROR: No reply - returning") + + return result def get_settings(self): # After successful connection get_settings will be called to set up the battery. @@ -254,7 +260,7 @@ def is_valid_frame(data: bytes) -> bool: * not checked: lchksum """ if len(data) < 18: - logger.warning("short read, data={}".format(data)) + logger.debug("short read, data={}".format(data)) return False chksum = Seplos.get_checksum(data[1:-5]) diff --git a/etc/dbus-serialbattery/config.default.ini b/etc/dbus-serialbattery/config.default.ini index 8018e3bd..e7c967ff 100644 --- a/etc/dbus-serialbattery/config.default.ini +++ b/etc/dbus-serialbattery/config.default.ini @@ -14,8 +14,11 @@ FLOAT_CELL_VOLTAGE = 3.375 ; --------- Bluetooth BMS --------- ; Description: List the Bluetooth BMS here that you want to install -; Example with 1 BMS: Jkbms_Ble C8:47:8C:00:00:00 -; Example with 3 BMS: Jkbms_Ble C8:47:8C:00:00:00, Jkbms_Ble C8:47:8C:00:00:11, Jkbms_Ble C8:47:8C:00:00:22 +; -- Available Bluetooth BMS: +; Jkbms_Ble, LltJbd_Ble +; Example: +; 1 BMS: Jkbms_Ble C8:47:8C:00:00:00 +; 3 BMS: Jkbms_Ble C8:47:8C:00:00:00, Jkbms_Ble C8:47:8C:00:00:11, Jkbms_Ble C8:47:8C:00:00:22 BLUETOOTH_BMS = ; --------- BMS disconnect behaviour --------- @@ -50,15 +53,18 @@ LINEAR_RECALCULATION_ON_PERC_CHANGE = 5 ; SoC is below SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT it switches back to max voltage. ; Linear mode: After max voltage is reachend and cell voltage difference is smaller or equal to ; CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_UNTIL it switches to float voltage after 300 (fixed) -; additional seconds. After cell voltage difference is greater or equal to -; CELL_VOLTAGE_DIFF_TO_RESET_VOLTAGE_LIMIT it switches back to max voltage. +; additional seconds. +; After cell voltage difference is greater or equal to CELL_VOLTAGE_DIFF_TO_RESET_VOLTAGE_LIMIT +; OR +; SoC is below SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT +; it switches back to max voltage. ; Example: The battery reached max voltage of 55.2V and hold it for 900 seconds, the the CVL is switched to ; float voltage of 53.6V to don't stress the batteries. Allow max voltage of 55.2V again, if SoC is ; once below 90% ; OR ; The battery reached max voltage of 55.2V and the max cell difference is 0.010V, then switch to float ; voltage of 53.6V after 300 additional seconds to don't stress the batteries. Allow max voltage of -; 55.2V again if max cell difference is above 0.050V +; 55.2V again if max cell difference is above 0.080V or SoC below 90%. ; Charge voltage control management enable (True/False). CVCM_ENABLE = True @@ -177,8 +183,12 @@ TIME_TO_SOC_INC_FROM = False ; --------- Additional settings --------- -; Specify only one BMS type to load else leave empty to try to load all availabe -; LltJbd, Ant, Daly, Daly, Jkbms, Lifepower, Renogy, Renogy, Ecs +; Specify only one BMS type to load else leave empty to try to load all available +; -- Available BMS: +; Daly, Ecs, HeltecModbus, HLPdataBMS4S, Jkbms, Lifepower, LltJbd, Renogy, Seplos +; -- Available BMS, but disabled by default: +; https://louisvdw.github.io/dbus-serialbattery/general/install#how-to-enable-a-disabled-bms +; Ant, MNB, Sinowealth BMS_TYPE = ; Publish the config settings to the dbus path "/Info/Config/" @@ -196,10 +206,12 @@ MIDPOINT_ENABLE = False ; Battery temperature -; Specifiy how the battery temperature is assembled -; 0 Get mean of temperature sensor 1 and temperature sensor 2 +; Specify how the battery temperature is assembled +; 0 Get mean of temperature sensor 1 to sensor 4 ; 1 Get only temperature from temperature sensor 1 ; 2 Get only temperature from temperature sensor 2 +; 3 Get only temperature from temperature sensor 3 +; 4 Get only temperature from temperature sensor 4 TEMP_BATTERY = 0 ; Temperature sensor 1 name @@ -208,6 +220,12 @@ TEMP_1_NAME = Temp 1 ; Temperature sensor 2 name TEMP_2_NAME = Temp 2 +; Temperature sensor 2 name +TEMP_3_NAME = Temp 3 + +; Temperature sensor 2 name +TEMP_4_NAME = Temp 4 + ; --------- BMS specific settings --------- @@ -218,7 +236,7 @@ SOC_LOW_WARNING = 20 SOC_LOW_ALARM = 10 ; -- Daly settings -; Battery capacity (amps) if the BMS does not support reading it +; Battery capacity (amps), if the BMS does not support reading it BATTERY_CAPACITY = 50 ; Invert Battery Current. Default non-inverted. Set to -1 to invert INVERT_CURRENT_MEASUREMENT = 1 @@ -228,3 +246,24 @@ GREENMETER_ADDRESS = 1 LIPRO_START_ADDRESS = 2 LIPRO_END_ADDRESS = 4 LIPRO_CELL_COUNT = 15 + +; -- HeltecModbus (Heltec SmartBMS/YYBMS) settings +; Set the Modbus addresses from the adapters +; Separate each address to check by a comma like: 1, 2, 3, ... +; factory default address will be 1 +HELTEC_MODBUS_ADDR = 1 + + +; --------- Battery monitor specific settings --------- +; If you are using a SmartShunt or something else as a battery monitor, the battery voltage reported +; from the BMS and SmartShunt could differ. This causes, that the driver never goapplies the float voltage, +; since max voltage is never reached. +; Example: +; cell count: 16 +; MAX_CELL_VOLTAGE = 3.45 +; max voltage calculated = 16 * 3.45 = 55.20 +; CVL is set to 55.20 and the battery is now charged until the SmartShunt measures 55.20 V. The BMS +; now measures 55.05 V since there is a voltage drop of 0.15 V. Since the dbus-serialbattery measures +; 55.05 V the max voltage is never reached for the driver and max voltage is kept forever. +; Set VOLTAGE_DROP to 0.15 +VOLTAGE_DROP = 0.00 diff --git a/etc/dbus-serialbattery/dbus-serialbattery.py b/etc/dbus-serialbattery/dbus-serialbattery.py index 68265a07..4bca9b35 100644 --- a/etc/dbus-serialbattery/dbus-serialbattery.py +++ b/etc/dbus-serialbattery/dbus-serialbattery.py @@ -24,6 +24,7 @@ # import battery classes from bms.daly import Daly from bms.ecs import Ecs +from bms.heltecmodbus import HeltecModbus from bms.hlpdatabms4s import HLPdataBMS4S from bms.jkbms import Jkbms from bms.lifepower import Lifepower @@ -39,6 +40,7 @@ {"bms": Daly, "baud": 9600, "address": b"\x40"}, {"bms": Daly, "baud": 9600, "address": b"\x80"}, {"bms": Ecs, "baud": 19200}, + {"bms": HeltecModbus, "baud": 9600}, {"bms": HLPdataBMS4S, "baud": 9600}, {"bms": Jkbms, "baud": 115200}, {"bms": Lifepower, "baud": 9600}, @@ -56,7 +58,7 @@ if battery_type["bms"].__name__ == utils.BMS_TYPE or utils.BMS_TYPE == "" ] -logger.info("") +print("") logger.info("Starting dbus-serialbattery") @@ -72,17 +74,24 @@ def get_battery(_port) -> Union[Battery, None]: while count > 0: # create a new battery object that can read the battery and run connection test for test in expected_bms_types: - logger.info("Testing " + test["bms"].__name__) - batteryClass = test["bms"] - baud = test["baud"] - battery: Battery = batteryClass( - port=_port, baud=baud, address=test.get("address") - ) - if battery.test_connection(): - logger.info( - "Connection established to " + battery.__class__.__name__ + # noinspection PyBroadException + try: + logger.info("Testing " + test["bms"].__name__) + batteryClass = test["bms"] + baud = test["baud"] + battery: Battery = batteryClass( + port=_port, baud=baud, address=test.get("address") ) - return battery + if battery.test_connection(): + logger.info( + "Connection established to " + battery.__class__.__name__ + ) + return battery + except KeyboardInterrupt: + return None + except Exception: + # Ignore any malfunction test_function() + pass count -= 1 sleep(0.5) @@ -97,9 +106,7 @@ def get_port() -> str: logger.info("No Port needed") return "/dev/tty/USB9" - logger.info( - "dbus-serialbattery v" + str(utils.DRIVER_VERSION) + utils.DRIVER_SUBVERSION - ) + logger.info("dbus-serialbattery v" + str(utils.DRIVER_VERSION)) port = get_port() battery = None diff --git a/etc/dbus-serialbattery/dbushelper.py b/etc/dbus-serialbattery/dbushelper.py index 555cb1ba..0cca3187 100644 --- a/etc/dbus-serialbattery/dbushelper.py +++ b/etc/dbus-serialbattery/dbushelper.py @@ -113,21 +113,17 @@ def setup_vedbus(self): self._dbusservice.add_path( "/Mgmt/ProcessVersion", "Python " + platform.python_version() ) - self._dbusservice.add_path("/Mgmt/Connection", "Serial " + self.battery.port) + self._dbusservice.add_path("/Mgmt/Connection", self.battery.connection_name()) # Create the mandatory objects self._dbusservice.add_path("/DeviceInstance", self.instance) self._dbusservice.add_path("/ProductId", 0x0) - self._dbusservice.add_path( - "/ProductName", "SerialBattery(" + self.battery.type + ")" - ) - self._dbusservice.add_path( - "/FirmwareVersion", str(utils.DRIVER_VERSION) + utils.DRIVER_SUBVERSION - ) + self._dbusservice.add_path("/ProductName", self.battery.product_name()) + self._dbusservice.add_path("/FirmwareVersion", str(utils.DRIVER_VERSION)) self._dbusservice.add_path("/HardwareVersion", self.battery.hardware_version) self._dbusservice.add_path("/Connected", 1) self._dbusservice.add_path( - "/CustomName", "SerialBattery(" + self.battery.type + ")", writeable=True + "/CustomName", self.battery.custom_name(), writeable=True ) self._dbusservice.add_path( "/Serial", self.battery.unique_identifier, writeable=True @@ -233,6 +229,10 @@ def setup_vedbus(self): self._dbusservice.add_path("/System/MaxCellTemperature", None, writeable=True) self._dbusservice.add_path("/System/MaxTemperatureCellId", None, writeable=True) self._dbusservice.add_path("/System/MOSTemperature", None, writeable=True) + self._dbusservice.add_path("/System/Temperature1", None, writeable=True) + self._dbusservice.add_path("/System/Temperature2", None, writeable=True) + self._dbusservice.add_path("/System/Temperature3", None, writeable=True) + self._dbusservice.add_path("/System/Temperature4", None, writeable=True) self._dbusservice.add_path( "/System/MaxCellVoltage", None, @@ -253,6 +253,24 @@ def setup_vedbus(self): self._dbusservice.add_path("/Io/AllowToCharge", 0, writeable=True) self._dbusservice.add_path("/Io/AllowToDischarge", 0, writeable=True) self._dbusservice.add_path("/Io/AllowToBalance", 0, writeable=True) + self._dbusservice.add_path( + "/Io/ForceChargingOff", + 0, + writeable=True, + onchangecallback=self.battery.force_charging_off_callback, + ) + self._dbusservice.add_path( + "/Io/ForceDischargingOff", + 0, + writeable=True, + onchangecallback=self.battery.force_discharging_off_callback, + ) + self._dbusservice.add_path( + "/Io/TurnBalancingOff", + 0, + writeable=True, + onchangecallback=self.battery.turn_balancing_off_callback, + ) # self._dbusservice.add_path('/SystemSwitch', 1, writeable=True) # Create the alarms @@ -454,6 +472,10 @@ def publish_dbus(self): "/System/MaxTemperatureCellId" ] = self.battery.get_max_temp_id() self._dbusservice["/System/MOSTemperature"] = self.battery.get_mos_temp() + self._dbusservice["/System/Temperature1"] = self.battery.temp1 + self._dbusservice["/System/Temperature2"] = self.battery.temp2 + self._dbusservice["/System/Temperature3"] = self.battery.temp3 + self._dbusservice["/System/Temperature4"] = self.battery.temp4 # Voltage control self._dbusservice["/Info/MaxChargeVoltage"] = self.battery.control_voltage diff --git a/etc/dbus-serialbattery/disable.sh b/etc/dbus-serialbattery/disable.sh index 34304cd5..f1902881 100755 --- a/etc/dbus-serialbattery/disable.sh +++ b/etc/dbus-serialbattery/disable.sh @@ -4,16 +4,27 @@ #set -x # handle read only mounts -sh /opt/victronenergy/swupdate-scripts/remount-rw.sh +bash /opt/victronenergy/swupdate-scripts/remount-rw.sh -# remove files, don't use variables here, since on an error the whole /opt/victronenergy gets deleted +# remove driver from serial starter rm -f /data/conf/serial-starter.d/dbus-serialbattery.conf +# kill serial starter, to reload changes +pkill -f "/opt/victronenergy/serial-starter/serial-starter.sh" + +# remove services rm -rf /service/dbus-serialbattery.* rm -rf /service/dbus-blebattery.* +# kill driver, if running +pkill -f "python .*/dbus-serialbattery.py" +pkill -f "blebattery" + # remove install script from rc.local sed -i "/bash \/data\/etc\/dbus-serialbattery\/reinstall-local.sh/d" /data/rc.local +# remove cronjob +sed -i "/5 0,12 \* \* \* \/etc\/init.d\/bluetooth restart/d" /var/spool/cron/root + ### needed for upgrading from older versions | start ### # remove old drivers before changing from dbus-blebattery-$1 to dbus-blebattery.$1 @@ -25,10 +36,5 @@ sed -i "/sh \/data\/etc\/dbus-serialbattery\/reinstall-local.sh/d" /data/rc.loca sed -i "/sh \/data\/etc\/dbus-serialbattery\/installble.sh/d" /data/rc.local ### needed for upgrading from older versions | end ### - -# kill serial starter, to reload changes -pkill -f "/opt/victronenergy/serial-starter/serial-starter.sh" - -# kill driver, if running -pkill -f "serialbattery" -pkill -f "blebattery" +echo "The dbus-serialbattery driver was disabled". +echo diff --git a/etc/dbus-serialbattery/install-qml.sh b/etc/dbus-serialbattery/install-qml.sh index 6c155b51..287aac83 100755 --- a/etc/dbus-serialbattery/install-qml.sh +++ b/etc/dbus-serialbattery/install-qml.sh @@ -7,7 +7,7 @@ # https://github.com/kwindrem/SetupHelper/blob/ebaa65fcf23e2bea6797f99c1c41174143c1153c/updateFileSets#L56-L81 function versionStringToNumber () { - local local p4="" ; local p5="" ; local p5="" + local p4="" ; local p5="" ; local p5="" local major=""; local minor="" # first character should be 'v' so first awk parameter will be empty and is not prited into the read command diff --git a/etc/dbus-serialbattery/install.sh b/etc/dbus-serialbattery/install.sh old mode 100755 new mode 100644 index 17a12ed3..c54c92c6 --- a/etc/dbus-serialbattery/install.sh +++ b/etc/dbus-serialbattery/install.sh @@ -49,8 +49,8 @@ fi ## specific version if [ "$version" = "specific version" ]; then # read the url - read -p "Enter the url of the \"venus-data.tar.gz\" you want to install: " tar_url - wget -O /tmp/venus-data.tar.gz $tar_url + read -r -p "Enter the url of the \"venus-data.tar.gz\" you want to install: " tar_url + wget -O /tmp/venus-data.tar.gz "$tar_url" if [ $? -ne 0 ]; then echo "Error during downloading the TAR file. Please check, if the URL is correct." exit diff --git a/etc/dbus-serialbattery/qml/PageBatteryCellVoltages.qml b/etc/dbus-serialbattery/qml/PageBatteryCellVoltages.qml index 112564df..ea723a89 100644 --- a/etc/dbus-serialbattery/qml/PageBatteryCellVoltages.qml +++ b/etc/dbus-serialbattery/qml/PageBatteryCellVoltages.qml @@ -4,6 +4,8 @@ import com.victron.velib 1.0 MbPage { id: root property string bindPrefix + property MbStyle style: MbStyle{} + property VBusItem _b1: VBusItem { bind: service.path("/Balances/Cell1") } property VBusItem _b2: VBusItem { bind: service.path("/Balances/Cell2") } property VBusItem _b3: VBusItem { bind: service.path("/Balances/Cell3") } @@ -52,30 +54,30 @@ MbPage { property VBusItem volt22: VBusItem { bind: service.path("/Voltages/Cell22") } property VBusItem volt23: VBusItem { bind: service.path("/Voltages/Cell23") } property VBusItem volt24: VBusItem { bind: service.path("/Voltages/Cell24") } - property string c1: _b1.valid && _b1.text == "1" ? "#ff0000" : "#ddd" - property string c2: _b2.valid && _b2.text == "1" ? "#ff0000" : "#ddd" - property string c3: _b3.valid && _b3.text == "1" ? "#ff0000" : "#ddd" - property string c4: _b4.valid && _b4.text == "1" ? "#ff0000" : "#ddd" - property string c5: _b5.valid && _b5.text == "1" ? "#ff0000" : "#ddd" - property string c6: _b6.valid && _b6.text == "1" ? "#ff0000" : "#ddd" - property string c7: _b7.valid && _b7.text == "1" ? "#ff0000" : "#ddd" - property string c8: _b8.valid && _b8.text == "1" ? "#ff0000" : "#ddd" - property string c9: _b9.valid && _b9.text == "1" ? "#ff0000" : "#ddd" - property string c10: _b10.valid && _b10.text == "1" ? "#ff0000" : "#ddd" - property string c11: _b11.valid && _b11.text == "1" ? "#ff0000" : "#ddd" - property string c12: _b12.valid && _b12.text == "1" ? "#ff0000" : "#ddd" - property string c13: _b13.valid && _b13.text == "1" ? "#ff0000" : "#ddd" - property string c14: _b14.valid && _b14.text == "1" ? "#ff0000" : "#ddd" - property string c15: _b15.valid && _b15.text == "1" ? "#ff0000" : "#ddd" - property string c16: _b16.valid && _b16.text == "1" ? "#ff0000" : "#ddd" - property string c17: _b17.valid && _b17.text == "1" ? "#ff0000" : "#ddd" - property string c18: _b18.valid && _b18.text == "1" ? "#ff0000" : "#ddd" - property string c19: _b19.valid && _b19.text == "1" ? "#ff0000" : "#ddd" - property string c20: _b20.valid && _b20.text == "1" ? "#ff0000" : "#ddd" - property string c21: _b21.valid && _b21.text == "1" ? "#ff0000" : "#ddd" - property string c22: _b22.valid && _b22.text == "1" ? "#ff0000" : "#ddd" - property string c23: _b23.valid && _b23.text == "1" ? "#ff0000" : "#ddd" - property string c24: _b24.valid && _b24.text == "1" ? "#ff0000" : "#ddd" + property string c1: _b1.valid && _b1.text == "1" ? "#ff0000" : style.borderColor + property string c2: _b2.valid && _b2.text == "1" ? "#ff0000" : style.borderColor + property string c3: _b3.valid && _b3.text == "1" ? "#ff0000" : style.borderColor + property string c4: _b4.valid && _b4.text == "1" ? "#ff0000" : style.borderColor + property string c5: _b5.valid && _b5.text == "1" ? "#ff0000" : style.borderColor + property string c6: _b6.valid && _b6.text == "1" ? "#ff0000" : style.borderColor + property string c7: _b7.valid && _b7.text == "1" ? "#ff0000" : style.borderColor + property string c8: _b8.valid && _b8.text == "1" ? "#ff0000" : style.borderColor + property string c9: _b9.valid && _b9.text == "1" ? "#ff0000" : style.borderColor + property string c10: _b10.valid && _b10.text == "1" ? "#ff0000" : style.borderColor + property string c11: _b11.valid && _b11.text == "1" ? "#ff0000" : style.borderColor + property string c12: _b12.valid && _b12.text == "1" ? "#ff0000" : style.borderColor + property string c13: _b13.valid && _b13.text == "1" ? "#ff0000" : style.borderColor + property string c14: _b14.valid && _b14.text == "1" ? "#ff0000" : style.borderColor + property string c15: _b15.valid && _b15.text == "1" ? "#ff0000" : style.borderColor + property string c16: _b16.valid && _b16.text == "1" ? "#ff0000" : style.borderColor + property string c17: _b17.valid && _b17.text == "1" ? "#ff0000" : style.borderColor + property string c18: _b18.valid && _b18.text == "1" ? "#ff0000" : style.borderColor + property string c19: _b19.valid && _b19.text == "1" ? "#ff0000" : style.borderColor + property string c20: _b20.valid && _b20.text == "1" ? "#ff0000" : style.borderColor + property string c21: _b21.valid && _b21.text == "1" ? "#ff0000" : style.borderColor + property string c22: _b22.valid && _b22.text == "1" ? "#ff0000" : style.borderColor + property string c23: _b23.valid && _b23.text == "1" ? "#ff0000" : style.borderColor + property string c24: _b24.valid && _b24.text == "1" ? "#ff0000" : style.borderColor title: service.description + " | Cell Voltages" model: VisibleItemModel { diff --git a/etc/dbus-serialbattery/qml/PageLynxIonIo.qml b/etc/dbus-serialbattery/qml/PageLynxIonIo.qml index e6ad7106..9d0ff4c4 100644 --- a/etc/dbus-serialbattery/qml/PageLynxIonIo.qml +++ b/etc/dbus-serialbattery/qml/PageLynxIonIo.qml @@ -49,6 +49,24 @@ MbPage { ] } + MbSwitch { + name: qsTr("Force charging off") + bind: Utils.path(bindPrefix, "/Io/ForceChargingOff") + show: item.valid + } + + MbSwitch { + name: qsTr("Force discharging off") + bind: Utils.path(bindPrefix, "/Io/ForceDischargingOff") + show: item.valid + } + + MbSwitch { + name: qsTr("Turn balancing off") + bind: Utils.path(bindPrefix, "/Io/TurnBalancingOff") + show: item.valid + } + MbItemOptions { description: qsTr("External relay") bind: Utils.path(bindPrefix, "/Io/ExternalRelay") @@ -70,5 +88,6 @@ MbPage { MbOption{description: qsTr("Active"); value: 1} ] } + } } diff --git a/etc/dbus-serialbattery/reinstall-local.sh b/etc/dbus-serialbattery/reinstall-local.sh index f822e026..a518a100 100755 --- a/etc/dbus-serialbattery/reinstall-local.sh +++ b/etc/dbus-serialbattery/reinstall-local.sh @@ -5,6 +5,63 @@ DRIVERNAME=dbus-serialbattery + +# check if minimum required Venus OS is installed | start +versionRequired="v2.90" + +# elaborate version string for better comparing +# https://github.com/kwindrem/SetupHelper/blob/ebaa65fcf23e2bea6797f99c1c41174143c1153c/updateFileSets#L56-L81 +function versionStringToNumber () +{ + local p4="" ; local p5="" ; local p5="" + local major=""; local minor="" + + # first character should be 'v' so first awk parameter will be empty and is not prited into the read command + # + # version number formats: v2.40, v2.40~6, v2.40-large-7, v2.40~6-large-7 + # so we must adjust how we use paramters read from the version string + # and parsed by awk + # if no beta make sure release is greater than any beta (i.e., a beta portion of 999) + + read major minor p4 p5 p6 <<< $(echo $1 | awk -v FS='[v.~-]' '{print $2, $3, $4, $5, $6}') + ((versionNumber = major * 1000000000 + minor * 1000000)) + if [ -z $p4 ] || [ $p4 = "large" ]; then + ((versionNumber += 999)) + else + ((versionNumber += p4)) + fi + if [ ! -z $p4 ] && [ $p4 = "large" ]; then + ((versionNumber += p5 * 1000)) + large=$p5 + elif [ ! -z $p6 ]; then + ((versionNumber += p6 * 1000)) + fi +} + +# get current Venus OS version +versionStringToNumber "$(head -n 1 /opt/victronenergy/version)" +venusVersionNumber="$versionNumber" + +# minimum required version to install the driver +versionStringToNumber "$versionRequired" + +if (( $venusVersionNumber < $versionNumber )); then + echo + echo + echo "Minimum required Venus OS version \"$versionRequired\" not met. Currently version \"$(head -n 1 /opt/victronenergy/version)\" is installed." + echo + echo "Please update via \"Remote Console/GUI -> Settings -> Firmware -> Online Update\"" + echo "OR" + echo "by executing \"/opt/victronenergy/swupdate-scripts/check-updates.sh -update -force\"" + echo + echo "Install the driver again after Venus OS was updated." + echo + echo + exit 1 +fi +# check if minimum required Venus OS is installed | end + + # handle read only mounts bash /opt/victronenergy/swupdate-scripts/remount-rw.sh @@ -24,38 +81,47 @@ serialstarter_path="/data/conf/serial-starter.d" serialstarter_file="$serialstarter_path/dbus-serialbattery.conf" # check if folder is a file (older versions of this driver < v1.0.0) -if [ -f $serialstarter_path ]; then - rm -f $serialstarter_path +if [ -f "$serialstarter_path" ]; then + rm -f "$serialstarter_path" fi # check if folder exists -if [ ! -d $serialstarter_path ]; then - mkdir $serialstarter_path +if [ ! -d "$serialstarter_path" ]; then + mkdir "$serialstarter_path" fi # check if file exists -if [ ! -f $serialstarter_file ]; then - echo "service sbattery dbus-serialbattery" >> $serialstarter_file - echo "alias default gps:vedirect:sbattery" >> $serialstarter_file - echo "alias rs485 cgwacs:fzsonick:imt:modbus:sbattery" >> $serialstarter_file +if [ ! -f "$serialstarter_file" ]; then + { + echo "service sbattery dbus-serialbattery" + echo "alias default gps:vedirect:sbattery" + echo "alias rs485 cgwacs:fzsonick:imt:modbus:sbattery" + } > "$serialstarter_file" fi # add install-script to rc.local to be ready for firmware update filename=/data/rc.local -if [ ! -f $filename ]; then - echo "#!/bin/bash" >> $filename - chmod 755 $filename +if [ ! -f "$filename" ]; then + echo "#!/bin/bash" > "$filename" + chmod 755 "$filename" fi grep -qxF "bash /data/etc/$DRIVERNAME/reinstall-local.sh" $filename || echo "bash /data/etc/$DRIVERNAME/reinstall-local.sh" >> $filename # add empty config.ini, if it does not exist to make it easier for users to add custom settings -filename=/data/etc/$DRIVERNAME/config.ini -if [ ! -f $filename ]; then - echo "[DEFAULT]" > $filename - echo "" >> $filename - echo "; If you want to add custom settings, then check the settings you want to change in \"config.default.ini\"" >> $filename - echo "; and add them below to persist future driver updates." >> $filename - echo "" >> $filename +filename="/data/etc/$DRIVERNAME/config.ini" +if [ ! -f "$filename" ]; then + { + echo "[DEFAULT]" + echo + echo "; If you want to add custom values/settings, then check the values/settings you want to change in \"config.default.ini\"" + echo "; and insert them below to persist future driver updates." + echo + echo "; Example (remove the semicolon \";\" to uncomment and activate the value/setting):" + echo "; MAX_BATTERY_CURRENT = 50.0" + echo "; MAX_BATTERY_DISCHARGE_CURRENT = 60.0" + echo + echo + } > $filename fi @@ -84,7 +150,7 @@ rm -rf /service/dbus-blebattery.* # kill all blebattery processes pkill -f "blebattery" -if [ $length -gt 0 ]; then +if [ "$length" -gt 0 ]; then echo "Found $length Bluetooth BMS in the config file!" echo "" @@ -101,16 +167,20 @@ if [ $length -gt 0 ]; then # function to install ble battery install_blebattery_service() { - mkdir -p /service/dbus-blebattery.$1/log - echo "#!/bin/sh" > /service/dbus-blebattery.$1/log/run - echo "exec multilog t s25000 n4 /var/log/dbus-blebattery.$1" >> /service/dbus-blebattery.$1/log/run - chmod 755 /service/dbus-blebattery.$1/log/run - - echo "#!/bin/sh" > /service/dbus-blebattery.$1/run - echo "exec 2>&1" >> /service/dbus-blebattery.$1/run - echo "bluetoothctl disconnect $3" >> /service/dbus-blebattery.$1/run - echo "python /opt/victronenergy/dbus-serialbattery/dbus-serialbattery.py $2 $3" >> /service/dbus-blebattery.$1/run - chmod 755 /service/dbus-blebattery.$1/run + mkdir -p "/service/dbus-blebattery.$1/log" + { + echo "#!/bin/sh" + echo "exec multilog t s25000 n4 /var/log/dbus-blebattery.$1" + } > "/service/dbus-blebattery.$1/log/run" + chmod 755 "/service/dbus-blebattery.$1/log/run" + + { + echo "#!/bin/sh" + echo "exec 2>&1" + echo "bluetoothctl disconnect $3" + echo "python /opt/victronenergy/dbus-serialbattery/dbus-serialbattery.py $2 $3" + } > "/service/dbus-blebattery.$1/run" + chmod 755 "/service/dbus-blebattery.$1/run" } echo "Packages installed." @@ -119,7 +189,7 @@ if [ $length -gt 0 ]; then # install_blebattery_service 0 Jkbms_Ble C8:47:8C:00:00:00 # install_blebattery_service 1 Jkbms_Ble C8:47:8C:00:00:11 - for (( i=0; i<${length}; i++ )); + for (( i=0; i Date: Wed, 31 May 2023 18:50:06 +0200 Subject: [PATCH 4/4] Release build 1.0 --- etc/dbus-serialbattery/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/dbus-serialbattery/utils.py b/etc/dbus-serialbattery/utils.py index 98ea5dca..e5f22fa5 100644 --- a/etc/dbus-serialbattery/utils.py +++ b/etc/dbus-serialbattery/utils.py @@ -35,7 +35,7 @@ def _get_list_from_config( # if not specified: baud = 9600 # Constants - Need to dynamically get them in future -DRIVER_VERSION = "1.0.20230526dev" +DRIVER_VERSION = "1.0.20230531" zero_char = chr(48) degree_sign = "\N{DEGREE SIGN}"