.NET Core2.1でスマートメーターと戯れる Part2

このエントリは[.NET Core2.1でスマートメーターと戯れる Part1](https://qiita.com/HiroyukiSakoh/items/a93a76db89a9d6b80108)の続きです。

# プロトコル/つくったもの 全体像
![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/126100ab-5467-9f03-8d88-d1a08e06f5d2.png)

# ECHONET Lite通信ミドルウェア層(EchoDotNetLiteプロジェクト)
ECHONET Lite規格書 Ver.1.13をもとにプロトコル上要求される全機能を実装しています。

## EchoClientクラス
[GitHub](https://github.com/HiroyukiSakoh/EchoDotNetLite/blob/master/EchoDotNetLite/Client.cs)
こちらがメインとなるクラスで、ECHONETLiteのノードとして振る舞うことができます。
ECHONETLite通信ネットワーク上の他ノードを識別し各種イベントを発生させます。
他ノードに対して、各種サービス(書き込み要求や、読み出し要求)を送信できます。

# ECHONET Lite機器オブジェクト定義(EchoDotNetLite.Specificationsプロジェクト)
[GitHub](https://github.com/HiroyukiSakoh/EchoDotNetLite/tree/master/EchoDotNetLite.Specifications)

ECHONET機器オブジェクト詳細規定の定義を表すJSONファイル、およびそれを読み取るクラス群
APPENDIX ECHONET機器オブジェクト詳細規定 Release K （日本語版）をもとに生成しています。
（プロファイルオブジェクトも含めています）

## 愚痴
APPENDIX ECHONET機器オブジェクト詳細規定ってPDFでしか一般公開されてなくて、手こずりました。
PDF->Word->Excel->Jsonと段階を踏みまくって、なんとか手でコピペみたいなことは避けられましたが、、
エコーネットコンソーシアムの会員だとPDF版以外の仕様書も入手できるんですかね…？

散々手こずって出来上がった後に、
神奈川工科大学のスマートハウス研究センターが以下を公開していることに気づきましたが、後の祭り。。
[ECHONET-APPENDIX](https://github.com/KAIT-HEMS/ECHONET-APPENDIX)

# 下位プロトコル(1-5層)との結合
ECHONET Liteは下位(1-4(5))プロトコルを規定しておらず、任意に適用できます。
EchoDotNetLiteとしては、下位プロトコルとのやり取りをするインターフェース`IPANAClient`~~名前がおかしい~~を公開しており、
そのインターフェースを実装して下位プロトコルに渡すブリッジ的なアセンブリを別途作成する必要があります。

今回は、Bルート用といわゆるLAN用のブリッジを作成しています。

## SKStackIP-EchonetLite
* [GitHub EchoDotNetLiteSkstackIpBridgeプロジェクト](https://github.com/HiroyukiSakoh/EchoDotNetLite/tree/master/EchoDotNetLiteSkstackIpBridge)
* 下位プロトコルの初期化処理(スキャンや認証)を実装しています。

## LAN-EchonetLite
* [GitHub EchoDotNetLiteLANBridgeプロジェクト](https://github.com/HiroyukiSakoh/EchoDotNetLite/tree/master/EchoDotNetLiteLANBridge)


# ECHONET Liteアプリケーション
サンプル的なアプリケーションを作成して公開しています。
いずれもRaspberryPi上での動作を確認しています。

## Bルートアプリ
* [GitHub EchoDotNetLiteSkstackIpBridge.Exampleプロジェクト](https://github.com/HiroyukiSakoh/EchoDotNetLite/tree/master/EchoDotNetLiteSkstackIpBridge.Example)
低圧スマート電力量メータのコントローラーアプリケーションです。
いろいろ値を読みだしてコンソールに書き出すサービスです。

以下ソースを抜粋

### 初期化
```Csharp:Program.cs(初期化)
//Windows
string devicePort = "COM3";
if (System.Environment.OSVersion.Platform.ToString() != "Win32NT")
{
    //Linux等(Raspbian Stretchで動作確認)
    devicePort = "/dev/ttyUSB0";
}
//シリアルポートOpen
skStackClient.OpenAsync(devicePort, 115200, 8, Parity.None, StopBits.One).Wait();
//スキャン＆Join
if(skStackClient.ScanAndJoinAsync(BRouteId, BRoutePw).Result)
{
    serviceProvider.GetService<EchoClient>().Initialize(skStackClient.SelfIpaddr);
    Task.Run(() => serviceProvider.GetService<Example>().ExecuteAsync());
}
Task.WaitAll(Task.Delay(-1));
```

```CSharp:Example.cs(コンストラクタ)
//コントローラとしてふるまう
this.echoClient.SelfNode.Devices.Add(
    new EchoObjectInstance(
        EchoDotNetLite.Specifications.機器.管理操作関連機器.コントローラ, 0x01));
```
### ノード立ち上げ時の初期シーケンス
```CSharp:Example.cs(L.84-93)
await echoClient.インスタンスリスト通知Async();
await echoClient.インスタンスリスト通知要求Async();

await Task.Delay(2 * 1000);
_logger.LogDebug("プロパティマップ読み込み完了まで待機");
while (echoClient.NodeList?.FirstOrDefault()?.Devices?.FirstOrDefault() == null
        || !echoClient.NodeList.First().Devices.First().IsPropertyMapGet)
{
    await Task.Delay(2 * 1000);
}
```
BルートなのでECHONET上の他ノードは１個だけの想定で実装。
インスタンスリスト通知要求の応答を受けたら、スマートメーターのプロパティマップの読み取り通知がEchoDotNetLite内で送信されるので、終わるまで待機します。

```json:出力
//何もしなくてもインスタンスリストが通知される時もある
受信 {"EDATA":{"SEOJ":{"ClassGroupCode":"0E","ClassCode":"F0","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"0E","ClassCode":"F0","InstanceCode":"01"},"ESV":"73","OPCList":[{"EPC":"D5","PDC":"04","EDT":"01028801"}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"0A5B"}

//インスタンスリスト通知
送信 {"EDATA":{"SEOJ":{"ClassGroupCode":"0E","ClassCode":"F0","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"0E","ClassCode":"F0","InstanceCode":"01"},"ESV":"73","OPCList":[{"EPC":"D5","PDC":"04","EDT":"0105FF01"}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"0100"}
//インスタンスリスト通知要求
送信 {"EDATA":{"SEOJ":{"ClassGroupCode":"0E","ClassCode":"F0","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"0E","ClassCode":"F0","InstanceCode":"01"},"ESV":"63","OPCList":[{"EPC":"D5","PDC":"00","EDT":""}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"0200"}
//インスタンスリスト通知要求の応答
受信 {"EDATA":{"SEOJ":{"ClassGroupCode":"0E","ClassCode":"F0","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"0E","ClassCode":"F0","InstanceCode":"01"},"ESV":"73","OPCList":[{"EPC":"D5","PDC":"04","EDT":"01028801"}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"0200"}

新しいEchoNode スマメIP
インスタンスリスト通知を受信しました
EchoObject Add 0x02住宅設備関連機器 0x88低圧スマート電力量メータ 01

0x02住宅設備関連機器 0x88低圧スマート電力量メータ 01 プロパティマップを読み取ります
送信 {"EDATA":{"SEOJ":{"ClassGroupCode":"0E","ClassCode":"F0","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"02","ClassCode":"88","InstanceCode":"01"},"ESV":"62","OPCList":[{"EPC":"9D","PDC":"00","EDT":""},{"EPC":"9E","PDC":"00","EDT":""},{"EPC":"9F","PDC":"00","EDT":""}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"0300"}

0x0Eプロファイル 0xF0ノードプロファイル 01 プロパティマップを読み取ります
送信 {"EDATA":{"SEOJ":{"ClassGroupCode":"0E","ClassCode":"F0","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"0E","ClassCode":"F0","InstanceCode":"01"},"ESV":"62","OPCList":[{"EPC":"9D","PDC":"00","EDT":""},{"EPC":"9E","PDC":"00","EDT":""},{"EPC":"9F","PDC":"00","EDT":""}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"0400"}

受信 {"EDATA":{"SEOJ":{"ClassGroupCode":"02","ClassCode":"88","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"0E","ClassCode":"F0","InstanceCode":"01"},"ESV":"72","OPCList":[{"EPC":"9D","PDC":"04","EDT":"03808188"},{"EPC":"9E","PDC":"04","EDT":"0381E5ED"},{"EPC":"9F","PDC":"11","EDT":"1941414160404000624300414040430202"}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"0300"}
0x02住宅設備関連機器 0x88低圧スマート電力量メータ 01 プロパティマップの読み取りが成功しました
------
0x80動作状態 Get(Req)  Anno
0x81設置場所 Get(Req) Set(Req) Anno
0x88異常発生状態 Get(Req)  Anno
0xE5積算履歴収集日１ Get(Req) Set(Req)
0xED積算履歴収集日２ Get Set
0xE0積算電力量計測値 (正方向計測値) Get(Req)
0xE1積算電力量単位 （正方向、逆方向計測値） Get(Req)
0xE2積算電力量 計測値履歴１ (正方向計測値) Get(Req)
0x82規格 Version 情報 Get(Req)
0xE3積算電力量計測値 (逆方向計測値) Get(Req)
0xD3係数 Get
0xE4積算電力量 計測値履歴１ (逆方向計測値) Get(Req)
0xE7瞬時電力計測値 Get(Req)
0xD7積算電力量有効桁数 Get(Req)
0x97現在時刻設定 Get
0xE8瞬時電流計測値 Get(Req)
0x98現在年月日設定 Get
0xEA定時積算電力量計測値 (正方向計測値) Get(Req)
0x8Aメーカコード Get(Req)
0xEB定時積算電力量計測値 (逆方向計測値) Get(Req)
0xEC積算電力量 計測値履歴２ （正方向、逆方向計測値） Get
0x9D状変アナウンスプロパティマップ Get(Req)
0x8D製造番号 Get
0x9ESet プロパティマップ Get(Req)
0x9FGet プロパティマップ Get(Req)
------

受信 {"EDATA":{"SEOJ":{"ClassGroupCode":"0E","ClassCode":"F0","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"0E","ClassCode":"F0","InstanceCode":"01"},"ESV":"72","OPCList":[{"EPC":"9D","PDC":"03","EDT":"0280D5"},{"EPC":"9E","PDC":"01","EDT":"00"},{"EPC":"9F","PDC":"0C","EDT":"0B8082838A9D9E9FD3D4D6D7"}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"0400"}
0x0Eプロファイル 0xF0ノードプロファイル 01 プロパティマップの読み取りが成功しました
------
0x80動作状態 Get(Req)  Anno
0xD5インスタンスリスト通知   Anno(Req)
0x82Version 情報 Get(Req)
0x83識別番号 Get(Req)
0x8Aメーカコード Get(Req)
0x9D状変アナウンスプロパティマップ Get(Req)
0x9ESet プロパティマップ Get(Req)
0x9FGet プロパティマップ Get(Req)
0xD3自ノードインスタンス数 Get(Req)
0xD4自ノードクラス数 Get(Req)
0xD6自ノードインスタンスリスト S Get(Req)
0xD7自ノードクラスリスト S Get(Req)
------
```


### 全プロパティを読み出し
```CSharp:Example.cs(L.101-107)
foreach (var prop in echoClient.NodeList.First().Devices.First().GETProperties)
{
    await echoClient.プロパティ値読み出し(
        echoClient.SelfNode.Devices.First(),//コントローラー
        node,device,new EchoPropertyInstance[] { prop }
        , 5 * 1000);
}
```
ECHONETLiteプロトコル的には全プロパティを一括で要求できるのですが、なぜかうちのスマートメーターは不可応答を返すので、１個づつプロパティ値を読んでます。

```json:出力
デバイスのGET対応プロパティの値をすべて取得
送信 {"EDATA":{"SEOJ":{"ClassGroupCode":"05","ClassCode":"FF","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"02","ClassCode":"88","InstanceCode":"01"},"ESV":"62","OPCList":[{"EPC":"80","PDC":"00","EDT":""}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"0500"}
受信 {"EDATA":{"SEOJ":{"ClassGroupCode":"02","ClassCode":"88","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"05","ClassCode":"FF","InstanceCode":"01"},"ESV":"72","OPCList":[{"EPC":"80","PDC":"01","EDT":"30"}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"0500"}
EchoProperty Change 0x80動作状態 Get(Req)  Anno 30
送信 {"EDATA":{"SEOJ":{"ClassGroupCode":"05","ClassCode":"FF","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"02","ClassCode":"88","InstanceCode":"01"},"ESV":"62","OPCList":[{"EPC":"81","PDC":"00","EDT":""}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"0600"}
受信 {"EDATA":{"SEOJ":{"ClassGroupCode":"02","ClassCode":"88","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"05","ClassCode":"FF","InstanceCode":"01"},"ESV":"72","OPCList":[{"EPC":"81","PDC":"01","EDT":"61"}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"0600"}
EchoProperty Change 0x81設置場所 Get(Req) Set(Req) Anno 61
...

```

### 定期的に瞬時電力/瞬時電流とか取得
```CSharp:Example.cs(L.109-142)
var target = new string[] { "瞬時電力計測値", "瞬時電流計測値", "現在時刻設定", "現在年月日設定" };
var properties = device.GETProperties.Where(p => target.Contains(p.Spec.Name));
timer = new Timer(20 * 1000);
timer.Elapsed += (sender, e) =>
{
    try
    {
        timer.Stop();
        Task.Run(() =>
            echoClient.プロパティ値読み出し(
                echoClient.SelfNode.Devices.First(),//コントローラー
                node,device, properties
                , 20 * 1000)
        ).ContinueWith((t) =>
        {
            if (t.Exception != null)
            {
                _logger.LogTrace(t.Exception, "Exception");
            }
            var 瞬時電力計測値 = properties.Where(p => p.Spec.Name == "瞬時電力計測値").First();
            var 瞬時電流計測値 = properties.Where(p => p.Spec.Name == "瞬時電流計測値").First();
            var 現在時刻設定 = properties.Where(p => p.Spec.Name == "現在時刻設定").First();
            var 現在年月日設定 = properties.Where(p => p.Spec.Name == "現在年月日設定").First();

            _logger.LogDebug($"瞬時電力計測値:{EndianBitConverter.BigEndian.ToInt32(瞬時電力計測値.Value,0)}W");
            _logger.LogDebug($"瞬時電流計測値: R相{EndianBitConverter.BigEndian.ToInt16(瞬時電流計測値.Value, 0) * 0.1}A,T相{EndianBitConverter.BigEndian.ToInt16(瞬時電流計測値.Value, 2) * 0.1}A");
        });
    }
    finally
    {
        timer.Start();
    }
};
timer.Start();
```

```json:出力①(エアコン(単相200V)稼働中)
送信 {"EDATA":{"SEOJ":{"ClassGroupCode":"05","ClassCode":"FF","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"02","ClassCode":"88","InstanceCode":"01"},"ESV":"62","OPCList":[{"EPC":"E7","PDC":"00","EDT":""},{"EPC":"97","PDC":"00","EDT":""},{"EPC":"E8","PDC":"00","EDT":""},{"EPC":"98","PDC":"00","EDT":""}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"1F00"}
受信 {"EDATA":{"SEOJ":{"ClassGroupCode":"02","ClassCode":"88","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"05","ClassCode":"FF","InstanceCode":"01"},"ESV":"72","OPCList":[{"EPC":"E7","PDC":"04","EDT":"00000403"},{"EPC":"97","PDC":"02","EDT":"0418"},{"EPC":"E8","PDC":"04","EDT":"00280046"},{"EPC":"98","PDC":"04","EDT":"07E20C1E"}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"1F00"}
EchoProperty Change 0xE7瞬時電力計測値 Get(Req)   00000403
EchoProperty Change 0x97現在時刻設定 Get   0418
EchoProperty Change 0xE8瞬時電流計測値 Get(Req)   00280046
EchoProperty Change 0x98現在年月日設定 Get   07E20C1E
瞬時電力計測値:1027W
瞬時電流計測値: R相4A,T相7A
```
```json:出力②(エアコン(単相200V)停止中)
送信 {"EDATA":{"SEOJ":{"ClassGroupCode":"05","ClassCode":"FF","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"02","ClassCode":"88","InstanceCode":"01"},"ESV":"62","OPCList":[{"EPC":"E7","PDC":"00","EDT":""},{"EPC":"97","PDC":"00","EDT":""},{"EPC":"E8","PDC":"00","EDT":""},{"EPC":"98","PDC":"00","EDT":""}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"2000"}
受信 {"EDATA":{"SEOJ":{"ClassGroupCode":"02","ClassCode":"88","InstanceCode":"01"},"DEOJ":{"ClassGroupCode":"05","ClassCode":"FF","InstanceCode":"01"},"ESV":"72","OPCList":[{"EPC":"E7","PDC":"04","EDT":"00000175"},{"EPC":"97","PDC":"02","EDT":"0418"},{"EPC":"E8","PDC":"04","EDT":"001E0000"},{"EPC":"98","PDC":"04","EDT":"07E20C1E"}],"OPCGetList":null,"OPCSetList":null},"EHD1":"10","EHD2":"81","TID":"2000"}
EchoProperty Change 0xE7瞬時電力計測値 Get(Req)   00000175
EchoProperty Change 0x97現在時刻設定 Get   0418
EchoProperty Change 0xE8瞬時電流計測値 Get(Req)   001E0000
EchoProperty Change 0x98現在年月日設定 Get   07E20C1E
瞬時電力計測値:373W
瞬時電流計測値: R相3A,T相0A
```
取れました！！！


## LAN経由で家電アプリ
[GitHub EchoDotNetLiteLANBridge.Exampleプロジェクト](https://github.com/HiroyukiSakoh/EchoDotNetLite/tree/master/EchoDotNetLiteLANBridge.Example)

EchoDotNetLiteの実装の確認をするために、実装してみました。

我が家にはLANにつながる家電がないので、機器オブジェクトエミュレーター:[MoekadenRoom](https://github.com/SonyCSL/MoekadenRoom/blob/master/README.jp.md)を使って
いろいろ値を読み出した後、エアコンをONにして黙り込むアプリです。

# まとめ

`スマートメーターから瞬間電力量を取りたい`は実現できた。
ECHONET Liteプロトコルまわりは(たぶん/なんとなく)それなりに実装できている気がする。

## 実用的なアプリをつくるには…
実用的なアプリをつくるには、ECHONET Lite規格だけではなく、
ECHONET Lite AIF(アプリケーションインターフェース仕様)仕様を満たす必要があると思われる。

[ECHONET Lite システム設計指針](https://echonet.jp/el_design_guide_1st/)の`第６章スマート電力量メータ AIF 仕様に関する実装事例と指針`を見ると、
ECHONET Lite規格に定義されたシーケンスだけでは分かりえない、
スマートメーター機器固有のふるまいがあることが見受けられる。

ちなみに、AIF仕様はエコーネットコンソーシアム非会員には公開されてない。
＃エコーネットコンソーシアムは企業と教育機関が会員になれます
AIF仕様を公開してほしいマジ。なんで公開されてないんでしょうね…？

~~実は大穴空いたスマメが出回ってて、公開すると大問題になるから公開できないんじゃ とか勘ぐってしまう~~
どなたか理由がわかれば教えてください。

今回作ったプログラムはMITライセンスですべてGitHubに公開しています。
ご利用は法律とか規格とか規約とかライセンスとかに従って、自己責任でお願いします。

## 今後
.NET Coreで作ったので、RaspberryPiで動かして、Azureへ連携するようなアプリを作ろうと思っています。
IoTEdgeを試してみようかと。

でも、IoTEdge(Dockerコンテナ)から、SerialPortにアクセスするハードルを越えないとだめで、
なんか GitHubにIssue [Azure/iotedge Read serialport on Linux #71](https://github.com/Azure/iotedge/issues/71)があったり。