BACnetプロトコルの装置からデータ収集

## BACnetとは
BACnet（バックネット）とは、ASHRAE（アメリカ暖房冷凍空調学会;アシュレ）などで標準化されたビルオートメーション用通信プロトコルです。
空調や衛生（給水・排水）、エレベータ、電気、防犯、防災（消火設備）などの装置間の通信において、広く利用されています。

もし、これらビル用装置からデータを取得したり、制御したりする場合、BACnetの知識が役に立ちます。
この投稿では、PythonライブラリであるBAC0を使って、BACnet対応装置と通信してみます。

## Pythonライブラリ BAC0
BACnetプロトコルを実装したPythonライブラリとして、BACpypesが有名です。
しかし、データを取得するのには複雑なプログラムの記述が必要です。
そこで、もっとお手軽にデータ取得できるように、BACpypesをラッピングしたライブラリがBAC0です。


## BAC0のインストール
Pythonが実行可能な環境で、以下のコマンドでインストールできます。

```
pip install BAC0
```

インストールし、以下のプログラムを書いて実行してみましょう。

```python
import BAC0
print('BAC0', BAC0.version)
```

実行結果は以下の通りです。
```
BAC0 23.07.03
```

インストールしたBA0のバージョンが出力されました。
以上で、無事BAC0のインストールが完了したことを確認できました。

## BACnetのデバイスを列挙

プログラムを実行する前に、あらかじめBACnetデバイスをネットワークに接続しておいてください。
ネットワークに接続する方法は様々ありますが、今回は簡単にBACnet/IPで接続しています。
つまり、スイッチングハブでPCとBACnetデバイスをイーサネット接続しています。

自身の端末をBACnetデバイスとして初期化し、ネットワークに参加します。

```python
bacnet = BAC0.lite()
```

次に、同じネットワークに接続されたBACnetデバイスを探します。

```python
bacnet.discover()
print(bacnet.devices)
```

私の環境における、出力結果は以下です。

```:出力結果
[('my-bacnet-device', 'my-vender-name', '192.168.0.248', 6367)]
```

出力された内容を表に整理します。

| # | 項目 | 値 |
|---|----|---|
| 1 | デバイス名 |  my-bacnet-device|
| 2 | ベンダ名 | my-vender-name |
| 3 | アドレス | 192.168.0.248 |
| 4 | デバイスインスタンス | 6367 |

このうち、3「アドレス」と4「デバイスインスタンス」は、次のデバイス接続に必要です。
もしかしたら接続するBACnetデバイスにより出力される項目数が異なるかもしれません。

## BACnetデバイスに接続

接続したいデバイスの「アドレス」と「デバイスインスタンス」を指定し、デイバスに接続します。

```python
device = BAC0.device(address='192.168.0.248', device_id=6367, network=bacnet)
```

オブジェクトインスタンスのリストを取得します。

```python
device.read_objects_list()
```

このデバイスは、6個のオブジェクトインスタンスを持つことが分かりました。

```:出力結果
[('device', 6367),
 ('networkPort', 1),
 ('analogInput', 0),
 ('analogOutput', 0),
 ('binaryInput', 0),
 ('binaryOutput', 0)]
```

では、このうちアナログ入力ブジェクトのプロパティを取得します。
bacnetのreadMultipleメソッドに引数として与える文字列は次の順番で記述します。
```
address object object_instance property
```

ここで、propertyをallとすると、指定したオブジェクトインスタンスのすべてのプロパティを取得できます。

```python
bacnet.readMultiple('192.168.0.248 analogInput 0 all')
```
```:出力結果
[('analogInput', 0),
 'myAnalogInput',
 'analogInput',
 0.0,
 [0, 0, 0, 0],
 'normal',
 False,
 'liters',
 'hello world',
 'noFaultDetected',
 1.0]
```

出力された内容を表に整理します。

| # | 項目 | 値 |
|---|----|---|
| 1 | Object Identifier |  ('analogInput', 0) |
| 2 | Object Name | myAnalogInput |
| 3 | Object Type | analogInput |
| 4 | Present Value | 0.0 |
| 5 | Status Flags | [0, 0, 0, 0] |
| 6 | Event State | normal |
| 7 | Out Of Service | False |
| 8 | Units | liters |
| 9 | Description | hello world |
| 10 | Reliability | noFaultDetected |
| 11 | COV Increment | 1.0 |

このうちデータの値を表すのは4番目の「Present Value」です。
この場合は現在値が1.0を表します。

## BACnetデバイスとデータを取得・設定

デバイス間で値の交換として多くの場合使われるのが、analogInput、analogOutput、binaryInput、binaryOutputです。
これらは現在の値を意味するpresentValueプロパティを持ちます。
presentValueプロパティをもつオブジェクトからpresentValueプロパティの値を取得するのが、以下のプログラムです。

```python
device.points
```

```:出力結果
[my-bacnet-device/myAnalogInput : 0.00 liters,
 my-bacnet-device/myAnalogOutput : 1.00 percent,
 my-bacnet-device/myBinaryInput : False,
 my-bacnet-device/myBinaryOutput : False]
```

以下のようにオブジェクト名を指定しても可能です。

```python
device['myAnalogInput']
```

```:出力結果
my-bacnet-device/myAnalogInput : 0.00 liters
```

逆に、BACnetデバイスのデータを変更するには、以下のように記述します。

```python
device['myAnalogOutput'] = 2.0
device['myBinaryOutput'] = True
```

ちなみに、BAC0ではBACnetデバイスに接続した時点で、自動的に定期的にデータを収集しています。
デフォルトでは10秒ごとにすべてのデータを収集します。
収集したデータを出力してみます。

```python
device['myAnalogInput'].history
```

```:出力結果
2024-02-24 15:50:22.588512+09:00    0.0
2024-02-24 15:50:46.295070+09:00    0.0
2024-02-24 15:50:56.744925+09:00    0.0
2024-02-24 15:51:07.195799+09:00    0.0
2024-02-24 15:51:17.626165+09:00    0.0
                                   ... 
2024-02-24 16:13:16.465508+09:00    0.0
2024-02-24 16:13:26.907661+09:00    0.0
2024-02-24 16:13:37.380533+09:00    0.0
2024-02-24 16:13:47.853849+09:00    0.0
2024-02-24 16:13:58.295078+09:00    0.0
Name: my-bacnet-device/myAnalogInput, Length: 134, dtype: float64
```

## BACnetデバイスの値を監視

BACnetデバイスの中にはCOV(Change Of Value)という値の変化を監視する機能があります。

私が接続しているBACnetデバイスでもCOVが利用可能ですので、BAC0でCOVを設定してみます。

```python
def cb(elements):
    print('cov recieved:', elements)
device['myAnalogOutput'].subscribe_cov(callback=cb)
```

これで、COVの設定ができました。
それでは、値を変更してみます。
```python
device['myAnalogOutput'] = 2.0
```

```:出力結果
2024-02-24 16:16:34,045 - INFO    | Received COV Notification for {'source': <Address 192.168.0.248>, 'object_changed': ('analogOutput', 0), 'properties': {'presentValue': 2.0, 'statusFlags': [0, 0, 0, 0]}}
cov recieved: {'source': <Address 192.168.0.248>, 'object_changed': ('analogOutput', 0), 'properties': {'presentValue': 2.0, 'statusFlags': [0, 0, 0, 0]}}
```

無事、値を変更するとコールバック関数`cb`が呼ばれました。


## BACnetデバイスと通信切断

COVをBACnetデバイスに設定したあとは、デバイスとの通信切断の前に、COVの設定解除を行いましょう。
これをしないと、BACnetデバイスをリセットするまで、よく分からないパケットがBACnetデバイスからPCに流れてしまいます。

```python
device['myAnalogOutput'].cancel_cov()
```

次に、BACnetデバイスとの通信を切断します。
これで、BACnetデバイスからの定期的なデータ収集が終了します。

```python
device.disconnect()
```

最後に、参加しているBACnetネットワークから自身を切断します。
```python
bacnet.disconnect()
```

BACnetデバイスからのデータ収集、お疲れ様でした。
ビルのIoT化にご活用ください。