LiFePO4バッテリーのSmartBMS(xiaoxiang BMS)とRaspberryPIからbluetooth接続してデータを収集する仕組みを構築します。 以前MPPT充放電コントローラ(Renogy ROVER)とRaspberryPIからシリアル接続して構築している計測システムへの追加構築設定になります。
⇒関連記事:「車庫屋根ソーラー発電システム20(計測システム構築)
⇒前回記事:「車庫屋根ソーラー発電システム24(12Vシステム)

grafana_bms9


 計測システム周りの接続構成。
kousei
 GitHubに上げられていた bms-monitoring-stack を使用します。 Dockerを使用しているようなのですが、初めてさわるので今回試行錯誤でした。 結局現時点ではこれが正解なのではないかという手順を忘れないようにまとめておきたいと思います。

1.【SmartBMSへのBluetooth接続】

2.【Docker環境構築】

3.【bms-monitoring-stack環境構築】

4.【Prometheus環境への追加】

5.【Docker環境のオペレーション(いろいろ)】

6.【grafana https化】

7.【bms-monitoring-stackの改造】


1.【SmartBMSへのBluetooth接続】

 先のページからリンクされていた、同じくGitHubに上げられている FHEM-BluetoothSmartBMS のページを参考にします。
# bluetoothctl
Agent registered
[bluetooth]# power on
[bluetooth]# scan on
[bluetooth]# Discovery started
[bluetooth]# [CHG] Controller XX:XX:XX:XX:XX:XX Discovering: yes
[bluetooth]# [NEW] Device XX:XX:XX:XX:XX:XX xiaoxiang BMS  ⇒xiaoxiang BMSのMACアドレス
[bluetooth]# show
Controller XX:XX:XX:XX:XX:XX (public)
 Name: xxxx
 Alias: xxxx
 Class: 0x00480000
 Powered: yes
 Discoverable: no
 Pairable: yes
 UUID: Headset AG                (00001112-0000-1000-8000-00805f9b34fb)
 UUID: Generic Attribute Profile (00001801-0000-1000-8000-00805f9b34fb)
 UUID: A/V Remote Control        (0000110e-0000-1000-8000-00805f9b34fb)
 UUID: Generic Access Profile    (00001800-0000-1000-8000-00805f9b34fb)
 UUID: PnP Information           (00001200-0000-1000-8000-00805f9b34fb)
 UUID: A/V Remote Control Target (0000110c-0000-1000-8000-00805f9b34fb)
 UUID: Audio Source              (0000110a-0000-1000-8000-00805f9b34fb)
 UUID: Handsfree Audio Gateway   (0000111f-0000-1000-8000-00805f9b34fb)
 Modalias: usb:v1D6Bp0246d0532
 Discovering: no
[bluetooth]# connect XX:XX:XX:XX:XX:XX  ←xiaoxiang BMSのMACアドレスを入力
Attempting to connect to XX:XX:XX:XX:XX:XX

 以上うまくいった流れで、太字部分が入力部分です。 ですが、最初引っ掛かりまして参考で以下リカバリまでの流れを追記しておきます。
--ここから--
# bluetoothctl
[bluetooth]# power on
[bluetooth]# Failed to set power on: org.bluez.Error.Blocked
[bluetooth]# scan on
[bluetooth]# Failed to start discovery: org.bluez.Error.NotReady
[bluetooth]# show
Controller XX:XX:XX:XX:XX:XX (public)
 Name: xxxx
 Alias: xxxx
 Class: 0x00000000
 Powered: no
 Discoverable: no
 Pairable: yes
 UUID: Headset AG                (00001112-0000-1000-8000-00805f9b34fb)
 UUID: Generic Attribute Profile (00001801-0000-1000-8000-00805f9b34fb)
 UUID: A/V Remote Control        (0000110e-0000-1000-8000-00805f9b34fb)
 UUID: Generic Access Profile    (00001800-0000-1000-8000-00805f9b34fb)
 UUID: PnP Information           (00001200-0000-1000-8000-00805f9b34fb)
 UUID: A/V Remote Control Target (0000110c-0000-1000-8000-00805f9b34fb)
 UUID: Audio Source              (0000110a-0000-1000-8000-00805f9b34fb)
 UUID: Handsfree Audio Gateway   (0000111f-0000-1000-8000-00805f9b34fb)
 Modalias: usb:v1D6Bp0246d0532
 Discovering: no
[bluetooth]# exit
# rfkill unblock bluetooth
--ここまで--

 FHEM-BluetoothSmartBMS のページから bmsinfo.py をダウンロードしておきます。
# pip install gattlib
# pip install gatt
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting gatt
  Downloading https://www.piwheels.org/simple/gatt/gatt-0.2.7-py3-none-any.whl
Installing collected packages: gatt
Successfully installed gatt-0.2.7


 テストプログラムを実行してみます。

# python bmsinfo.py XX:XX:XX:XX:XX:XX  ←xiaoxiang BMSのMACアドレスを入力
BMS found
BMS request generic data
BMS answering
BMS answering
BMS answer: dd03001b0a5bfeff2110271000082a2e00000000000016550308020b5b0b78fb5a77
BMS request voltages
BMS answering
BMS answering
BMS answer: dd0400100cf00cf40cf30cf30cf10cf40cf20cf4f7fb77
BMS chat ended
{
 "Bal": 0,
 "Ibat": -2.57,
 "T1": 17.6,
 "T2": 20.5,
 "V01": 3.312,
 "V02": 3.316,
 "V03": 3.315,
 "V04": 3.315,
 "V05": 3.313,
 "V06": 3.316,
 "V07": 3.314,
 "V08": 3.316,
 "Vbat": 26.516999999999996
}
[XX:XX:XX:XX:XX:XX] Disconnected

 以上、SmartBMSに接続してデータ取得できることを確認しました。

2.【Docker環境構築】

 Dockerをダウンロードし、インストールします。
# curl -sSL https://get.docker.com | sh
# Executing docker install script, commit: 3d8fe77c2c46c5b7571f94b42793905e5b3e42e4
+ sh -c apt-get update -qq >/dev/null
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get install -y -qq apt-transport-https ca-certificates curl >/dev/null
+ sh -c curl -fsSL "https://download.docker.com/linux/raspbian/gpg" | apt-key add -qq - >/dev/null
Warning: apt-key output should not be parsed (stdout is not a terminal)
+ sh -c echo "deb [arch=armhf] https://download.docker.com/linux/raspbian buster stable" > /etc/apt/sources.list.d/docker.list
+ sh -c apt-get update -qq >/dev/null
+ [ -n  ]
+ sh -c apt-get install -y -qq --no-install-recommends docker-ce >/dev/null
+ sh -c docker version
Client: Docker Engine - Community
 Version:           20.10.5
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        55c4c88
 Built:             Tue Mar  2 20:18:46 2021
 OS/Arch:           linux/arm
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.5
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       363e9a8
  Built:            Tue Mar  2 20:16:18 2021
  OS/Arch:          linux/arm
  Experimental:     false
 containerd:
  Version:          1.4.4
  GitCommit:        05f951a3781f4f2c1911b05e61c160e9c30eaa8e
 runc:
  Version:          1.0.0-rc93
  GitCommit:        12644e614e25b05da6fd08a38ffa0cfe1903fdec
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
If you would like to use Docker as a non-root user, you should now consider
adding your user to the "docker" group with something like:

  sudo usermod -aG docker your-user

Remember that you will have to log out and back in for this to take effect!

WARNING: Adding a user to the "docker" group will grant the ability to run
         containers which can be used to obtain root privileges on the
         docker host.
         Refer to https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface
         for more information.


# usermod -aG docker pi


# docker version
Client: Docker Engine - Community
 Version:           20.10.5
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        55c4c88
 Built:             Tue Mar  2 20:18:46 2021
 OS/Arch:           linux/arm
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.5
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       363e9a8
  Built:            Tue Mar  2 20:16:18 2021
  OS/Arch:          linux/arm
  Experimental:     false
 containerd:
  Version:          1.4.4
  GitCommit:        05f951a3781f4f2c1911b05e61c160e9c30eaa8e
 runc:
  Version:          1.0.0-rc93
  GitCommit:        12644e614e25b05da6fd08a38ffa0cfe1903fdec
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

# apt-get install docker-compose
 ※ここらへんが結構な試行錯誤をした結果で、出回っている情報に振りまわされて回り道してしまいました。。。

 次の構築でdocker-compose の実行時に発生していたエラーの対処(バグらしい)を事前に行います。
# wget http://ftp.us.debian.org/debian/pool/main/libs/libseccomp/libseccomp2_2.5.1-1_armhf.deb

# dpkg -i libseccomp2_2.5.1-1_armhf.deb


3.【bms-monitoring-stack環境構築】

 prometheus対応のexporter(BMS監視エージェント)をDocker上で起動する形になります。
 bms-monitoring-stack のページから bms-monitoring-stack-master.zip をダウンロードしておきます。

# unzip bms-monitoring-stack-master.zip
Archive:  bms-monitoring-stack-master.zip
3f63a4b78e93fd36328304bb96ad0c8027de7e0f
   creating: bms-monitoring-stack-master/
  inflating: bms-monitoring-stack-master/.gitignore 
  inflating: bms-monitoring-stack-master/README.md 
   creating: bms-monitoring-stack-master/bms-db/
  inflating: bms-monitoring-stack-master/bms-db/Dockerfile 
 extracting: bms-monitoring-stack-master/bms-db/Makefile 
  inflating: bms-monitoring-stack-master/bms-db/bms-db 
   creating: bms-monitoring-stack-master/bms-exporter/
  inflating: bms-monitoring-stack-master/bms-exporter/Dockerfile 
 extracting: bms-monitoring-stack-master/bms-exporter/Makefile 
   creating: bms-monitoring-stack-master/bms-exporter/bms_exporter/
 extracting: bms-monitoring-stack-master/bms-exporter/bms_exporter/__init__.py 
  inflating: bms-monitoring-stack-master/bms-exporter/bms_exporter/main.py 
  inflating: bms-monitoring-stack-master/bms-exporter/setup.py 
   creating: bms-monitoring-stack-master/dht-exporter/
  inflating: bms-monitoring-stack-master/dht-exporter/Dockerfile 
 extracting: bms-monitoring-stack-master/dht-exporter/Makefile 
  inflating: bms-monitoring-stack-master/dht-exporter/dht_exporter.py 
  inflating: bms-monitoring-stack-master/docker-compose.yaml 
   creating: bms-monitoring-stack-master/grafana/
 extracting: bms-monitoring-stack-master/grafana/config.monitoring 
  inflating: bms-monitoring-stack-master/grafana/grafana.ini 
   creating: bms-monitoring-stack-master/grafana/provisioning/
   creating: bms-monitoring-stack-master/grafana/provisioning/dashboards/
  inflating: bms-monitoring-stack-master/grafana/provisioning/dashboards/Docker Prometheus Monitoring-1571332751387.json 
  inflating: bms-monitoring-stack-master/grafana/provisioning/dashboards/bms.json 
  inflating: bms-monitoring-stack-master/grafana/provisioning/dashboards/dashboard.yml 
  inflating: bms-monitoring-stack-master/grafana/provisioning/dashboards/node-exporter-full_rev19.json 
   creating: bms-monitoring-stack-master/grafana/provisioning/datasources/
  inflating: bms-monitoring-stack-master/grafana/provisioning/datasources/datasource.yml 
   creating: bms-monitoring-stack-master/prometheus/
  inflating: bms-monitoring-stack-master/prometheus/prometheus.yml 
   creating: bms-monitoring-stack-master/screenshots/
  inflating: bms-monitoring-stack-master/screenshots/grafana-dashboard.png 


# cd bms-monitoring-stack-master

 今回は、bms-db、bms-exporterのみ使用しますので docker-compose.yaml を編集して余計な部分を削除します。
# vi docker-compose.yaml
--
networks:
  back-tier:
version: '3'
services:
  # docker run -p 8000:8000 -v $PWD/bms-exporter:/app -v $PWD/bms-db:/bms-db -e DATABASE=/bms-db/bms.db -it --entrypoint /bin/sh bms-exporter
  bms-exporter:
    build: ./bms-exporter/
    container_name: bms-exporter
    image: bms-exporter
    restart: always
    volumes:
      - ./bms-exporter/:/app
      - ./bms-db:/bms-db
    ports:
      - 8000:8000
    environment:
      - DATABASE=/bms-db/bms.db
    networks:
      - back-tier
    depends_on:
     - bms-db
    cap_add:
      - SYS_ADMIN
  # docker run -it -v $PWD/bms-db:/app -v /run/dbus/system_bus_socket:/run/dbus/system_bus_socket --privileged bms-db
  bms-db:
    build: ./bms-db/
    container_name: bms-db
    image: bms-db
    restart: always
    privileged: true
    volumes:
      - ./bms-db:/app
      - /run/dbus/system_bus_socket:/run/dbus/system_bus_socket
    network_mode: host
    command: /app/bms-db XX:XX:XX:XX:XX:XX  ←xiaoxiang BMSのMACアドレスを入力
--

 ビルドします。
# docker-compose up --build
Creating network "bms-monitoring-stack-master_back-tier" with the default driver
Creating bms-db ...
Creating bms-db ... done
Creating bms-exporter ...
Creating bms-exporter ... done
Attaching to bms-db, bms-exporter
・・・
 ※この初回実行段階で必要なコンポーネントをダウンロード、インストールするようですので、結構時間がかかります。さっき書いたバグの関連ですんなりはいきませんでしたが。。。

 エラーメッセージなく正常に起動したら、別のターミナルログインで一度終了させてから、
# docker-compose down

 バックグラウンド実行します。
# docker-compose up -d

 面白いと思うのですが、ファイルシステムを確認すると、以下のようなものが追加されていました。
# df -k
・・・
overlay                       120015044  21621920 92273600   19% /var/lib/docker/overlay2/59fa1465b317a22362ac3b55571edad8f6a5fb9cff3f7db03d6807ae99ac244e/merged
shm                               65536         0    65536    0% /var/lib/docker/containers/a67428de1d4803fc7689b80548171c3797a780e9c95543bda25d13e3fc77f305/mounts/shm
overlay                       120015044  21621920 92273600   19% /var/lib/docker/overlay2/cec9cc9ae4c28371adbae28e90af7bb34463700087988ed8e445910f66609487/merged
shm                               65536         0    65536    0% /var/lib/docker/containers/28357737fef6b4fa4271a9969542dee62b0b194db8b93f565b200cb9f62c2b0f/mounts/shm

 curlかブラウザを使い、8000ポートにhttpアクセスしてみます。 以下のようにprometheusフォーマットで出力されました。
# curl http://localhost:8000


・・・
# HELP bms_last_update Metrics of last update
# TYPE bms_last_update gauge
bms_last_update 1.617415275e+09
# HELP bms_ah_percent Metrics of percent of capacity
# TYPE bms_ah_percent gauge
bms_ah_percent 99.99
# HELP bms_ah_remaining Metrics of remaining capcity
# TYPE bms_ah_remaining gauge
bms_ah_remaining 99.99
# HELP bms_ah_full Metrics of full capacity
# TYPE bms_ah_full gauge
bms_ah_full 100.0
# HELP bms_power Metrics of power
# TYPE bms_power gauge
bms_power 0.0
# HELP bms_voltage Metrics of voltage
# TYPE bms_voltage gauge
bms_voltage 27.891000000000002
# HELP bms_current Metrics of current
# TYPE bms_current gauge
bms_current 0.0
# HELP bms_temperature Metrics of temperature
# TYPE bms_temperature gauge
bms_temperature 25.8
# HELP bms_cycles Metrics of cycles
# TYPE bms_cycles gauge
bms_cycles 8.0
# HELP bms_mode Metrics of mode
# TYPE bms_mode gauge
bms_mode{bms_mode="discharging"} 0.0
bms_mode{bms_mode="charging"} 1.0
# HELP bms_time_left Metrics of time to charge/discharge
# TYPE bms_time_left gauge
bms_time_left{mode="smart"} 0.08196721311479603
bms_time_left{mode="to100"} 0.08196721311479603
bms_time_left{mode="to40"} 491.7213114754098
bms_time_left{mode="to0"} 819.5901639344261


4.【Prometheus環境への追加】

 最初の関連記事リンク先で構築したPrometheus監視サーバにBMS監視エージェントのエントリを追加して再起動します。

# cd /etc/prometheus
# vi prometheus.yml
--
  - job_name: solarshed
    static_configs:- targets: ['localhost:5000']

  - job_name: bms
    static_configs:- targets: ['localhost:8000']

--

# systemctl restart prometheus

 最初の関連記事リンク先のGrafana構築においてPrometheusサーバ連携設定をしているため、今回はGrafanaダッシュボードに必要なパネルを追加設定するだけです。
grafana_bms3


5.【Docker環境のオペレーション(いろいろ)】

 以下運用操作についてメモします。

サービスを停止(すでにコンテナがある前提)
# docker-compose stop
Stopping bms-exporter ... done
Stopping bms-db       ... done

サービスを開始(すでにコンテナがある前提)
# docker-compose start
Starting bms-db       ... done
Starting bms-exporter ... done

対象のイメージの情報を表示
# docker-compose images
 Container      Repository     Tag       Image Id      Size
-------------------------------------------------------------
bms-db         bms-db         latest   2e7b0717dbb9   337 MB
bms-exporter   bms-exporter   latest   0b62a24b9b77   41.1 MB

コンテナの一覧を表示
# docker-compose ps
    Name                  Command              State           Ports
-----------------------------------------------------------------------------
bms-db         /app/bms-db XX:XX:XX:XX:XX:XX   Up
bms-exporter   bms-exporter                    Up      0.0.0.0:8000->8000/tcp

コンテナの一覧を表示(先のコマンドと同じ)
# docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED       STATUS              PORTS                    NAMES
6e9a3b09f421   bms-exporter   "bms-exporter"           8 hours ago   Up About a minute   0.0.0.0:8000->8000/tcp   bms-exporter
7973695012d1   bms-db         "/app/bms-db XX:XX:X…"   8 hours ago   Up About a minute

各コンテナのプロセス情報を表示
# docker-compose top
bms-db
UID     PID    PPID    C   STIME   TTY     TIME                      CMD
-----------------------------------------------------------------------------------------
root   18196   18177   1   18:29   ?     00:00:01   python3 /app/bms-db XX:XX:XX:XX:XX:XX

bms-exporter
UID     PID    PPID    C   STIME   TTY     TIME                            CMD
-----------------------------------------------------------------------------------------------------
root   18285   18264   1   18:29   ?     00:00:00   /usr/local/bin/python /usr/local/bin/bms-exporter

sqlite3データベースの確認方法
# cd bms-monitoring-stack-master/bms-db
# sqlite3 bms.db
> select * from readings;
・・・
 ※コントロール-d で終了。


6.【grafana https化】

 外出先から確認できるように外部公開も考えてhttps化することにしました。

cd /etc/grafana

 鍵作成します。
# openssl genrsa -aes256 -out /etc/grafana/cakey.pem 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
..................................................+++++
....+++++
e is 65537 (0x010001)
Enter pass phrase for /etc/grafana/cakey.pem:xxxxxxxx
Verifying - Enter pass phrase for /etc/grafana/cakey.pem:xxxxxxxx

# openssl req -new -key /etc/grafana/cakey.pem -out /etc/grafana/cacert.csr
Enter pass phrase for /etc/grafana/cakey.pem:xxxxxxxx
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:XX
State or Province Name (full name) [Some-State]:xxxxxxxx
Locality Name (eg, city) []:xxx
Organization Name (eg, company) [Internet Widgits Pty Ltd]:xxx
Organizational Unit Name (eg, section) []:xxx
Common Name (e.g. server FQDN or YOUR name) []:xxx
Email Address []:xxxxxxxx

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:xxxxxxxx
An optional company name []:xxx

 証明書を作成します。
# openssl x509 -days 9999 -in /etc/grafana/cacert.csr -req -signkey /etc/grafana/cakey.pem -out /etc/grafana/cacert.pem
Signature ok
subject=C = XX, ST = xxxxxxxx, L = xxx, O = xxx, OU = xxx, CN = xxx, emailAddress = xxxxxxxx
Getting Private key
Enter pass phrase for /etc/grafana/cakey.pem:xxxxxxxx

# chgrp grafana cakey.pem cacert.pem cacert.csr

# chmod 640 cakey.pem cacert.pem cacert.csr

# ls -l
合計 56
-rw-r----- 1 root grafana  1086  4月  4 01:31 cacert.csr
-rw-r----- 1 root grafana  1257  4月  4 01:32 cacert.pem
-rw-r----- 1 root grafana  1766  4月  4 01:28 cakey.pem
-rw-r----- 1 root grafana 31544  4月  4 02:09 grafana.ini
-rw-r----- 1 root grafana  2289 11月 30 22:54 ldap.toml
drwxr-xr-x 6 root grafana  4096 11月 30 22:54 provisioning

# vi /etc/grafana/grafana.ini
・・・
[server]
・・・
protocol = https
・・・
cert_file = /etc/grafana/cacert.pem
cert_key = /etc/grafana/cakey.pem

・・・

 鍵からパスワードを削除します。 xxxxxxxxには、鍵作成時に入れたパスワードを入れます。
# openssl rsa -in cakey.pem -out canokey.pem -passin pass:xxxxxxxx
# mv cakey.pem cakey_mv.pem
# mv canokey.pem cakey.pem

# systemctl restart grafana-server

 ブラウザからhttps、3000ポートでgrafanaに接続し、最初に出る警告を無視すればアクセスできると思います。

※うまくいかないときは、ログを確認して切り分けます。
# vi /var/log/grafana/grafana.log


7.【bms-monitoring-stackの改造】

 後日、プログラムを変えて、8セル毎の電圧等も見れるようにしました。 元々の作り手の意図としては異なるセルのBMSとの互換性を重視したと理解し8セルBMSに特化してプログラムを改造しました。赤文字が追加部分です。

# vi bms-monitoring-stack-master/bms-db/bms-db
--
#!/usr/bin/env python3
"""
based on
https://github.com/sw-home/FHEM-BluetoothSmartBMS
"""

"""
test data:
generic:    data = b"\x053\xff\xb2I\xb8TS\x00\x01\'Y\x00\x00\x00\x00\x00\x00\x19W\x03\x04\x01\x0b\x06\xfbLw"
"""
import gatt
import json
import sys
import time

import sqlite3

from time import gmtime, strftime

manager = gatt.DeviceManager(adapter_name='hci0')

class AnyDevice(gatt.Device):
    def  __init__(self, c, **kwargs):
        super().__init__(**kwargs)
        self.c=c

    def connect_succeeded(self):
        super().connect_succeeded()

    def connect_failed(self, error):
        super().connect_failed(error)
        print("[%s] Connection failed: %s" % (self.mac_address, str(error)))
        exit(1)

    def disconnect_succeeded(self):
        super().disconnect_succeeded()

    def services_resolved(self):
        super().services_resolved()

        device_information_service = next(
            s for s in self.services
            if s.uuid == '0000ff00-0000-1000-8000-00805f9b34fb')

        self.bms_read_characteristic = next(
            c for c in device_information_service.characteristics
            if c.uuid == '0000ff01-0000-1000-8000-00805f9b34fb')

        self.bms_write_characteristic = next(
            c for c in device_information_service.characteristics
            if c.uuid == '0000ff02-0000-1000-8000-00805f9b34fb')

        self.bms_read_characteristic.enable_notifications()

    def characteristic_enable_notifications_succeeded(self, characteristic):
        super().characteristic_enable_notifications_succeeded(characteristic)
        self.response=bytearray()
        self.rawdat={}
        self.get_voltages=False
        self.bms_write_characteristic.write_value(bytes([0xDD,0xA5,0x03,0x00,0xFF,0xFD,0x77]));

    def characteristic_enable_notifications_failed(self, characteristic, error):
        super.characteristic_enable_notifications_failed(characteristic, error)

    def characteristic_value_updated(self, characteristic, value):
        self.response+=value
        if (self.response.endswith(b'w')):
            self.response=self.response[4:]
            if (self.get_voltages):
                packVolts=0
                for i in range(int(len(self.response)/2)-1):
                    cell=int.from_bytes(self.response[i*2:i*2+2], byteorder = 'big')/1000
                    self.rawdat['V{0:0=2}'.format(i+1)]=cell
                    packVolts+=cell
                self.rawdat['Vbat']=packVolts
                self.rawdat['P']=round(self.rawdat['Vbat']*self.rawdat['Ibat'], 1)
                self.c.execute("INSERT INTO readings values(datetime('now'), (?), (?), (?), (?), (?), (?), (?), (?), (?), (?), (?), (?), (?), (?), (?), (?), (?), (?), (?))", (
                    self.rawdat['Ah_percent'],
                    self.rawdat['Ah_remaining'],
                    self.rawdat['Ah_full'],
                    self.rawdat['P'],
                    self.rawdat['Vbat'],
                    self.rawdat['Ibat'],
                    self.rawdat['T1'],
                    self.rawdat['Cycles'],
                    self.rawdat['T2'],
                    self.rawdat['V01'],
                    self.rawdat['V02'],
                    self.rawdat['V03'],
                    self.rawdat['V04'],
                    self.rawdat['V05'],
                    self.rawdat['V06'],
                    self.rawdat['V07'],
                    self.rawdat['V08'],
                    self.rawdat['Bal'],
                    self.rawdat['State'],

                    ))
                conn.commit()

                self.manager.stop()
            else:
                self.rawdat['packV']=int.from_bytes(self.response[0:2], byteorder = 'big',signed=True)/100.0
                self.rawdat['Ibat']=int.from_bytes(self.response[2:4], byteorder = 'big',signed=True)/100.0
                self.rawdat['Ah_remaining']=int.from_bytes(self.response[4:6], byteorder='big', signed=True)/100
                self.rawdat['Ah_full']=int.from_bytes(self.response[6:8], byteorder='big', signed=True)/100
                self.rawdat['Cycles']=int.from_bytes(self.response[8:10], byteorder='big', signed=True)
                self.rawdat['Bal']=int.from_bytes(self.response[12:14],byteorder = 'big',signed=False)
                self.rawdat['State']=int.from_bytes(self.response[16:18], byteorder = 'big',signed=False)
                self.rawdat['Ah_percent']=round(self.rawdat['Ah_remaining'] / self.rawdat['Ah_full'] * 100, 2)

                for i in range(int.from_bytes(self.response[22:23],'big')): # read temperatures
                    self.rawdat['T{0:0=1}'.format(i+1)]=(int.from_bytes(self.response[23+i*2:i*2+25],'big')-2731)/10

                self.get_voltages=True
                self.response=bytearray()
                self.bms_write_characteristic.write_value(bytes([0xDD,0xA5,0x04,0x00,0xFF,0xFC,0x77]));

    def characteristic_write_value_failed(self, characteristic, error):
        pass

if (len(sys.argv)<2):
    print("Usage: bmsinfo.py <device_uuid>")
else:
    conn = sqlite3.connect('/app/bms.db')
    c = conn.cursor()
    try:
        c.execute('''CREATE TABLE readings
                  (date time, ah_percent real, ah_remaining real, ah_full real, power real, voltage real, current real, temperature real, cycles numeric, temperature2 real, v01 real, v02 real, v03 real, v04 real, v05 real, v06 real, v07 real, v08 real, bal blob, state blob)''')
    except:
       pass

    while True:
        print(strftime("%Y-%m-%d %H:%M:%S", gmtime()))
        device = AnyDevice(mac_address=sys.argv[1], manager=manager, c=c)
        device.connect()
        manager.run()
        time.sleep(30)

    conn.close()
--

# vi bms-monitoring-stack-master/bms-exporter/bms_exporter/main.py
--
import datetime
import sys
import time

import sqlite3

import click

from prometheus_client import Gauge, start_http_server, Enum

class Metrics:
    def __init__(self, database):
        """
        (date time, ah_percent real, ah_remaining real, ah_full real, power real, voltage real, current real, temperature real, cycles numeric, temperature2 real, v01 real, v02 real, v03 real, v04 real, v05 real, v06 real, v07 real, v08 real, bal blob, state blob);
        """
        self.database = ''
        conn = sqlite3.connect(database)
        self.c = conn.cursor()
        self.last_update_gauge = Gauge('bms_last_update', 'Metrics of last update')
        self.ah_percent_gauge = Gauge('bms_ah_percent', 'Metrics of percent of capacity')
        self.ah_remaining_gauge = Gauge('bms_ah_remaining', 'Metrics of remaining capcity')
        self.ah_full_gauge = Gauge('bms_ah_full', 'Metrics of full capacity')
        self.power_gauge = Gauge('bms_power', 'Metrics of power')
        self.voltage_gauge = Gauge('bms_voltage', 'Metrics of voltage')
        self.current_gauge = Gauge('bms_current', 'Metrics of current')
        self.temperature_gauge = Gauge('bms_temperature', 'Metrics of temperature')
        self.cycles_gauge = Gauge('bms_cycles', 'Metrics of cycles')
        self.temperature2_gauge = Gauge('bms_temperature2', 'Metrics of temperature2')
        self.voltage01_gauge = Gauge('bms_voltage01', 'Metrics of voltage01')
        self.voltage02_gauge = Gauge('bms_voltage02', 'Metrics of voltage02')
        self.voltage03_gauge = Gauge('bms_voltage03', 'Metrics of voltage03')
        self.voltage04_gauge = Gauge('bms_voltage04', 'Metrics of voltage04')
        self.voltage05_gauge = Gauge('bms_voltage05', 'Metrics of voltage05')
        self.voltage06_gauge = Gauge('bms_voltage06', 'Metrics of voltage06')
        self.voltage07_gauge = Gauge('bms_voltage07', 'Metrics of voltage07')
        self.voltage08_gauge = Gauge('bms_voltage08', 'Metrics of voltage08')
        self.bal_status = Gauge('bms_balance_status', 'Metrics of balance status', ['cell'])
        self.bms_state = Gauge('bms_state', 'Metrics of bms state', ['state'])

    def build_metrics(self):
        data = self.c.execute('select * from readings  ORDER BY date DESC LIMIT 1').fetchone()
        last_update = data[0]
        last_update = datetime.datetime.strptime(last_update, '%Y-%m-%d %H:%M:%S')
        last_update = last_update.timestamp()
        self.last_update_gauge.set(last_update)
        self.ah_percent_gauge.set(data[1])
        ah_remaining_gauge = data[2]
        self.ah_remaining_gauge.set(ah_remaining_gauge)
        ah_full_gauge = data[3]
        self.ah_full_gauge.set(ah_full_gauge)
        self.power_gauge.set(data[4])
        self.voltage_gauge.set(data[5])
        self.current_gauge.set(data[6])
        self.temperature_gauge.set(data[7])
        self.cycles_gauge.set(data[8])
        self.temperature2_gauge.set(data[9])
        self.voltage01_gauge.set(data[10])
        self.voltage02_gauge.set(data[11])
        self.voltage03_gauge.set(data[12])
        self.voltage04_gauge.set(data[13])
        self.voltage05_gauge.set(data[14])
        self.voltage06_gauge.set(data[15])
        self.voltage07_gauge.set(data[16])
        self.voltage08_gauge.set(data[17])

        celbals = [0b0,0b0,0b0,0b0,0b0,0b0,0b0,0b0]
        for num in range(0,8):
            celbals[num] = ( data[18]>>num ) & 0b1
        self.bal_status.labels(cell='celbals[1]').set(celbals[0])
        self.bal_status.labels(cell='celbals[2]').set(celbals[1])
        self.bal_status.labels(cell='celbals[3]').set(celbals[2])
        self.bal_status.labels(cell='celbals[4]').set(celbals[3])
        self.bal_status.labels(cell='celbals[5]').set(celbals[4])
        self.bal_status.labels(cell='celbals[6]').set(celbals[5])
        self.bal_status.labels(cell='celbals[7]').set(celbals[6])
        self.bal_status.labels(cell='celbals[8]').set(celbals[7])

        bms_state = [0b0,0b0,0b0,0b0,0b0,0b0,0b0,0b0,0b0,0b0,0b0,0b0,0b0]
        for num in range(0,13):
            bms_state[num] = ( data[19]>>num ) & 0b1
        self.bms_state.labels(state='Cell_Block_Over_Vol').set(bms_state[0])
        self.bms_state.labels(state='Cell_Block_Under_Vol').set(bms_state[1])
        self.bms_state.labels(state='Battery_Over_Vol').set(bms_state[2])
        self.bms_state.labels(state='Battery_Under_Vol').set(bms_state[3])
        self.bms_state.labels(state='Charging_Over_Temp').set(bms_state[4])
        self.bms_state.labels(state='Charging_Under_Temp').set(bms_state[5])
        self.bms_state.labels(state='Discharging_Over_Temp').set(bms_state[6])
        self.bms_state.labels(state='Discharging_Under_Temp').set(bms_state[7])
        self.bms_state.labels(state='Charging_Over_Current').set(bms_state[8])
        self.bms_state.labels(state='Discharging_Over_Current').set(bms_state[9])
        self.bms_state.labels(state='Short_Circuit').set(bms_state[10])
        self.bms_state.labels(state='Fore-end_IC_Error').set(bms_state[11])
        self.bms_state.labels(state='MOS_Software_Lock-in').set(bms_state[12])

@click.command()
@click.option('--database', '-f', envvar='DATABASE', help='Path to sqlite db')
@click.option('--port', '-p', envvar='PORT', help='Exporter port', default=8000)
@click.option('--interval', '-i', envvar='INTERVAL', help='interval', default=5)
def main(database, port, interval):
    """
    """
    start_http_server(port)
    metrics = Metrics(database)

    while True:
        metrics.build_metrics()
        time.sleep(interval)

--

 以上の変更を行った上で
# cd bms-monitoring-stack-master

# docker-compose down

# cd bms-db

# rm -f bms.db

# cd ..

# docker-compose up -d --build

 bms-exporterの動作確認
# curl http://localhost:8000

・・・
# HELP bms_last_update Metrics of last update
# TYPE bms_last_update gauge
bms_last_update 1.617990282e+09
# HELP bms_ah_percent Metrics of percent of capacity
# TYPE bms_ah_percent gauge
bms_ah_percent 75.23
# HELP bms_ah_remaining Metrics of remaining capcity
# TYPE bms_ah_remaining gauge
bms_ah_remaining 75.23
# HELP bms_ah_full Metrics of full capacity
# TYPE bms_ah_full gauge
bms_ah_full 100.0
# HELP bms_power Metrics of power
# TYPE bms_power gauge
bms_power -70.9
# HELP bms_voltage Metrics of voltage
# TYPE bms_voltage gauge
bms_voltage 26.452
# HELP bms_current Metrics of current
# TYPE bms_current gauge
bms_current -2.68
# HELP bms_temperature Metrics of temperature
# TYPE bms_temperature gauge
bms_temperature 11.1
# HELP bms_cycles Metrics of cycles
# TYPE bms_cycles gauge
bms_cycles 11.0
# HELP bms_temperature2 Metrics of temperature2
# TYPE bms_temperature2 gauge
bms_temperature2 15.0
# HELP bms_voltage01 Metrics of voltage01
# TYPE bms_voltage01 gauge
bms_voltage01 3.305
# HELP bms_voltage02 Metrics of voltage02
# TYPE bms_voltage02 gauge
bms_voltage02 3.308
# HELP bms_voltage03 Metrics of voltage03
# TYPE bms_voltage03 gauge
bms_voltage03 3.308
# HELP bms_voltage04 Metrics of voltage04
# TYPE bms_voltage04 gauge
bms_voltage04 3.307
# HELP bms_voltage05 Metrics of voltage05
# TYPE bms_voltage05 gauge
bms_voltage05 3.305
# HELP bms_voltage06 Metrics of voltage06
# TYPE bms_voltage06 gauge
bms_voltage06 3.306
# HELP bms_voltage07 Metrics of voltage07
# TYPE bms_voltage07 gauge
bms_voltage07 3.306
# HELP bms_voltage08 Metrics of voltage08
# TYPE bms_voltage08 gauge
bms_voltage08 3.307
# HELP bms_balance_status Metrics of balance status
# TYPE bms_balance_status gauge
bms_balance_status{cell="celbals[1]"} 0.0
bms_balance_status{cell="celbals[2]"} 0.0
bms_balance_status{cell="celbals[3]"} 0.0
bms_balance_status{cell="celbals[4]"} 0.0
bms_balance_status{cell="celbals[5]"} 0.0
bms_balance_status{cell="celbals[6]"} 0.0
bms_balance_status{cell="celbals[7]"} 0.0
bms_balance_status{cell="celbals[8]"} 0.0
# HELP bms_state Metrics of bms state
# TYPE bms_state gauge
bms_state{state="Cell_Block_Over_Vol"} 0.0
bms_state{state="Cell_Block_Under_Vol"} 0.0
bms_state{state="Battery_Over_Vol"} 0.0
bms_state{state="Battery_Under_Vol"} 0.0
bms_state{state="Charging_Over_Temp"} 0.0
bms_state{state="Charging_Under_Temp"} 0.0
bms_state{state="Discharging_Over_Temp"} 0.0
bms_state{state="Discharging_Under_Temp"} 0.0
bms_state{state="Charging_Over_Current"} 0.0
bms_state{state="Discharging_Over_Current"} 0.0
bms_state{state="Short_Circuit"} 0.0
bms_state{state="Fore-end_IC_Error"} 0.0
bms_state{state="MOS_Software_Lock-in"} 0.0


 あとはGrafanaダッシュボードに、今回追加したメトリックスのパネルを追加設定するだけです。
grafana_bms9
 ※何度も止めたりしているので、途中のデータが欠落しています。