家電の動作情報をRaspberryPiで一括取得

# はじめに
IoTという言葉がブームとなって久しいですが、家電に関してもこのブームに乗り、**APIで情報取得できる機種**が増えています。
そこで今回、**家電の中でも特に使用頻度の高い**、

**・テレビ**
**・エアコン**
**・照明**
**・コンセント**（スマートプラグ）

の**動作情報を、RaspberryPiで一括取得するシステム**を構築してみました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/3edd832f-ac98-ef3d-e490-725f78fec615.png)

# 使用した機器
情報取得対象の家電、および情報取得に使用した機器を紹介します

## 情報取得対象の家電
以下の家電の情報を取得しました

|種類|メーカー|名称|情報取得対象|備考|
|---|---|---|---|---|
|テレビ|Panasonic|VIERA TH-49GX850|OnOff情報|専用APIあり|
|照明(LED電球)|TPLink|KL130|OnOff情報|リビング1台目|
|照明(LED電球)|TPLink|KL130|OnOff情報|リビング2台目|
|照明(LED電球)|TPLink|KL130|OnOff情報|リビング3台目|
|照明(LED電球)|TPLink|KL110|OnOff情報|廊下|
|コンセント|TPLink|HS105|OnOff情報|[IKEA間接照明](https://www.ikea.com/jp/ja/p/ledberg-led-spotlight-white-70430822/)に使用|
|エアコン|三菱電機|MSZ-SV287-W|OnOff・冷暖房・設定温度・風量・風向|NatureRemoで情報取得|

上記家電の設置と使用方法を、写真を交えて紹介します
### テレビ
以前購入したPanasonicの[VIERA TH-49GX850](https://panasonic.jp/viera/p-db/TH-49GX850.html)が[Python APIによる情報取得に対応](https://github.com/Turgon37/panasonic-viera/blob/master/panasonic_viera/remote_control.py)していたため、これを活用しました
![image.png](https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRoE62ZrJ0FjCLOneVO3WdCB-mAnA_0B_pRBIFP2QyrbJDZ2PNpWg36_OZyNg_ndCmkyNg&usqp=CAU)
最近のVIERAであれば、大半はPythonで情報取得が可能かと思います。
ざっと見た限りでは最近の[SONY BRAVIA](https://dev.classmethod.jp/articles/bravia-rest-api/)や[SHARP AQUOS](https://github.com/jmoore987/sharp_aquos_rc)も対応していそうな感じです

### 照明
TPLinkのスマート電球、KL130を使用しています。
1台では暗いので、3台を以下のようなシーリングファンに取り付けました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/97b59c56-a069-9832-e4e0-5d3f412f81c5.png)

### コンセント
IoTでON-OFF制御できる**スマートプラグ**は、電源供給のON-OFFのみで制御できる機器に幅広く活用でき、非常に便利です。

我が家では、以下のような[IKEAの間接照明](https://www.ikea.com/jp/ja/p/ledberg-led-spotlight-white-70430822/)のON-OFFに使用しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/110da460-af5b-8e10-4eaf-60327c44a4d4.png)

### エアコン
三菱電機の15年前のモデルです

https://www.mitsubishielectric.co.jp/ldg/wink/ssl/displayProduct.do?pid=152034

当然IoTには対応していませんが(笑)、なんと**Nature Remoのエアコン制御機能**を使う事で、**古いリモコン対応のエアコンを遠隔制御**することができます。

制御だけでなく、**On-Off・冷暖房・設定温度などの情報をAPIで取得**することもできるので、エアコンでIoTぽい事をしたい！と言う人はNature Remoは非常におすすめです！

なお詳細は後述しますが、本システムではエアコンのみ別系統（[他の家電とは別のRaspberry Pi](https://qiita.com/c60evaporator/items/283d0569eba58830f86e#nature-remo)）で情報取得しているのでご注意ください。

# 情報取得に使用した技術
下記のAPI等を利用して、各機器のOn-Off等の情報を取得しました
### テレビ（Panasonic VIERA）
テレビ
以下のモジュールを使用させていただきました。

https://github.com/Turgon37/panasonic-viera

上記モジュールは、VIERA内部で稼働しているHTTPサーバから、APIを通じて情報を取得するようです。上記以外にも、[こちら](https://qiita.com/arthur87/items/1740efcd2164730db6a8)や[こちら](https://github.com/tomokas/pyviera/blob/master/viera.py)を参考にさせて頂きました。

上記モジュールには、On-Off情報自体を取得するAPIは見当たらなかったので、少し回りくどいやり方ですが、

**「音量を取得し、取得できたらTVがON、取得できなかったらOFFとみなす」**

という処理を作成し、On-Off情報取得を実現しています。

### 照明・コンセント
製造元のTPLink社のAPIを使用しました。
詳しくは以下の記事を参照ください

https://qiita.com/c60evaporator/items/74fe4234d9ed5eb713d3

### エアコン（Nature Remoから取得）
Nature Remoを利用して、エアコンのOn-Off、冷暖房、設定温度、風向、風量情報を取得します。
詳しくは以下の記事を参照ください

https://qiita.com/c60evaporator/items/283d0569eba58830f86e#nature-remo

# ソフトウェア構成
エアコンとそれ以外で、データの取得系統が大きく異なるので、分けて解説します

## テレビ・照明・コンセント
以下のようなソフトウェア構成となっています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/3edd832f-ac98-ef3d-e490-725f78fec615.png)
詳細は[後述](https://qiita.com/c60evaporator/items/73e1726a23a1f5692bf7#%E5%AE%9F%E8%A3%85)しますが、Raspberry Pi上の**Pythonスクリプト**でデータを取得し、クラウドDB（MongoDB Atlas）にアップロードしています。

また、バックアップ用としてRaspberry Pi4内にCSVファイルを保存する機能と、トラブル時の原因解析のためのログ保存機能も実装しております。

## エアコン
Nature Remoを使用して情報を取得するため、他の家電とは別系統の[こちらの記事のシステム](https://qiita.com/c60evaporator/items/283d0569eba58830f86e)の一部としてデータ取得しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/c40e2b1a-256a-b9c7-e8c4-a7dbb7d28d7c.png)
最終的なデータの出力先は、他の家電と同じくクラウドDBのMongoDB Atlasですが、他の家電が「appliances」コレクション（RDBにおけるテーブルに相当）に格納しているのに対し、**エアコンのみ「sensors」コレクション**に格納しております。

Remoを通じたエアコンからの情報取得は[上記記事](https://qiita.com/c60evaporator/items/283d0569eba58830f86e#nature-remo)を、クラウドDBへのアップロードに関しては以下の記事をご参照ください。

https://qiita.com/c60evaporator/items/92d0ebde001f568f2707#2-raspberrypi%E3%81%8B%E3%82%89%E3%82%B5%E3%83%BC%E3%83%90db%E3%81%B8%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89

## 情報取得に使用した機器
以下の機器およびサービスを、家電からの情報取得に使用しました

|機器名|機能|備考|
|---|---|---|
|Raspberry Pi4|エアコン以外の家電情報取得とDBへのアップロード||
|MongoDB Atlas|データアップロード先のクラウドDB||
|Raspberry Pi3|エアコン情報（＆センサ情報）取得とDBへのアップロード|エアコン情報取得のみ使用|
|Nature Remo|エアコン情報取得|エアコン情報取得のみ使用|

# 実装
エアコン以外の家電情報取得に使用したコードの実装方法を解説します

## MongoDB Atlasへの登録とコレクションの作成
以下の記事を参考に、MongoDB Atlasへの登録と、コレクションの作成（「appliances」という名称で作成）を実施します

https://qiita.com/c60evaporator/items/92d0ebde001f568f2707#1-mongodb-atlas%E3%81%AE%E5%88%9D%E6%9C%9F%E8%A8%AD%E5%AE%9A%E3%81%A8%E3%82%B5%E3%83%BC%E3%83%90db%E3%81%AE%E4%BD%9C%E6%88%90

## フォルダ構成
以下のフォルダ構成で、Raspberry Pi内にモジュールを設置します。
各モジュールの詳細は後述します

```
./ ... ルートディレクトリ
├─ appliance_data_logger.py ... 全体統括およびクラウドDBへのデータアップロード
├─ tplink.py ... TPLink製品（照明・コンセント）のOn-Off情報を取得
├─ viera_gx.py ... テレビのOn-Off情報を取得
├─ config.ini ... 全体統括およびクラウドDB設定ファイル
├─ ApplianceList.csv ... 家電情報の設定ファイル
└─ panasonic_viera/ ... テレビ（VIERA）の情報取得モジュール※
    ├─ constants.py
    ├─ exceptions.py
    ├─ remote_control.py
    ├─ utils.py
    └─ __init__.py
```
※「panasonic_viera」フォルダ内のVIERAの情報取得モジュールは[前述のAPIモジュール](https://github.com/Turgon37/panasonic-viera)をそのままコピーさせて頂きました。

## 主要なモジュール
|モジュール名|機能|
|---|---|
|tplink.py|TPLink製品（照明・コンセント）のOn-Off情報を取得|
|viera_gx.py|テレビのOn-Off情報を取得|
|appliance_data_logger.py|全体統括およびクラウドDBへのデータアップロード|

※なお、前述のようにエアコンの情報取得は別システムで行っているため、本記事では詳細に触れません。
エアコン情報取得に関しては以下の記事のremo.pyをご参照ください

https://qiita.com/c60evaporator/items/283d0569eba58830f86e#nature-remo

### tplink.py
TPLink製の電球、コンセント（スマートプラグ）から情報を取得するモジュールです。
以下の3つのクラスからなります

**TPLink_Common()**：プラグ、電球共通機能のクラス
**TPLink_Plug()**：プラグ専用機能のクラス（TPLink_Common()を継承）
**TPLink_Bulb()**：電球専用機能のクラス（TPLink_Common()を継承）

詳細は以下の記事を参照ください

https://qiita.com/c60evaporator/items/74fe4234d9ed5eb713d3#tplink%E6%93%8D%E4%BD%9C%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E4%BD%9C%E6%88%90

```tplink.py
import socket
from struct import pack
import json

#TPLink電球＆プラグ共通クラス
class TPLink_Common():
    def __init__(self, ip, port=9999):
        """Default constructor
        """
        self.__ip = ip
        self.__port = port
    
    def info(self):
        cmd = '{"system":{"get_sysinfo":{}}}'
        receive = self.send_command(cmd)
        return receive

    def info_dict(self):
        rjson = self.info()
        rdict = json.loads(rjson)
        return rdict
    
    def send_command(self, cmd, timeout=10):
        try:
            sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock_tcp.settimeout(timeout)
            sock_tcp.connect((self.__ip, self.__port))
            sock_tcp.settimeout(None)
            sock_tcp.send(self.encrypt(cmd))
            data = sock_tcp.recv(2048)
            sock_tcp.close()

            decrypted = self.decrypt(data[4:])
            print("Sent:     ", cmd)
            print("Received: ", decrypted)
            return decrypted

        except socket.error:
            quit("Could not connect to host " + self.__ip + ":" + str(self.__port))
            return None

    def encrypt(self, string):
        key = 171
        result = pack('>I', len(string))
        for i in string:
            a = key ^ ord(i)
            key = a
            result += bytes([a])
        return result

    def decrypt(self, string):
        key = 171
        result = ""
        for i in string:
            a = key ^ i
            key = i
            result += chr(a)
        return result

#TPLinkプラグ操作用クラス
class TPLink_Plug(TPLink_Common):
    def on(self):
        cmd = '{"system":{"set_relay_state":{"state":1}}}'
        receive = self.send_command(cmd)

    def off(self):
        cmd = '{"system":{"set_relay_state":{"state":0}}}'
        receive = self.send_command(cmd)

    def ledon(self):
        cmd = '{"system":{"set_led_off":{"off":0}}}'
        receive = self.send_command(cmd)

    def ledoff(self):
        cmd = '{"system":{"set_led_off":{"off":1}}}'
        receive = self.send_command(cmd)
    
    def set_countdown_on(self, delay):
        cmd = '{"count_down":{"add_rule":{"enable":1,"delay":' + str(delay) +',"act":1,"name":"turn on"}}}'
        receive = self.send_command(cmd)

    def set_countdown_off(self, delay):
        cmd = '{"count_down":{"add_rule":{"enable":1,"delay":' + str(delay) +',"act":0,"name":"turn off"}}}'
        receive = self.send_command(cmd)
    
    def delete_countdown_table(self):
        cmd = '{"count_down":{"delete_all_rules":null}}'
        receive = self.send_command(cmd)

    def energy(self):
        cmd = '{"emeter":{"get_realtime":{}}}'
        receive = self.send_command(cmd)
        return receive

#TPLink電球操作用クラス
class TPLink_Bulb(TPLink_Common):
    def on(self):
        cmd = '{"smartlife.iot.smartbulb.lightingservice":{"transition_light_state":{"on_off":1}}}'
        receive = self.send_command(cmd)

    def off(self):
        cmd = '{"smartlife.iot.smartbulb.lightingservice":{"transition_light_state":{"on_off":0}}}'
        receive = self.send_command(cmd)

    def transition_light_state(self, hue: int = None, saturation: int = None, brightness: int = None,
                               color_temp: int = None, on_off: bool = None, transition_period: int = None,
                               mode: str = None, ignore_default: bool = None):
        # copy all given argument name-value pairs as a dict
        d = {k: v for k, v in locals().items() if k is not 'self' and v is not None}
        r = {
            'smartlife.iot.smartbulb.lightingservice': {
                'transition_light_state': d
            }
        }
        cmd = json.dumps(r)
        receive = self.send_command(cmd)
        print(receive)

    def brightness(self, brightness):
        self.transition_light_state(brightness=brightness)

    def purple(self, brightness = None, transition_period = None):
        self.transition_light_state(hue=277, saturation=86, color_temp=0, brightness=brightness, transition_period=transition_period)

    def blue(self, brightness = None, transition_period = None):
        self.transition_light_state(hue=240, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period)

    def cyan(self, brightness = None, transition_period = None):
        self.transition_light_state(hue=180, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period)

    def green(self, brightness = None, transition_period = None):
        self.transition_light_state(hue=120, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period)
    
    def yellow(self, brightness = None, transition_period = None):
        self.transition_light_state(hue=60, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period)

    def orange(self, brightness = None, transition_period = None):
        self.transition_light_state(hue=39, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period)
    
    def red(self, brightness = None, transition_period = None):
        self.transition_light_state(hue=0, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period)
    
    def lamp_color(self, brightness = None):
        self.transition_light_state(color_temp=2700, brightness=brightness)
```

### viera_gx.py
テレビ（Panasonic VIERA TH-49GX850）のOn-Off情報（および音量情報）を取得します。
前述のように、[こちらのモジュール](https://github.com/Turgon37/panasonic-viera)を呼び出して音量情報を取得し、音量が取得出来たらON、取得できなかったらOFFと判定しています。

``` viera_gx.py
import panasonic_viera

#Vieraから各種情報を取得するクラス
class GetVieraData():
    #人感タグデータ取得
    def get_gx850_data(self, IP):
        #データ取得用クラス作成
        rc = panasonic_viera.RemoteControl(IP)
        #音量取得
        try:
            volume = rc.getVolume()
            rdict = {'Power':1, 'Volume':volume}
        #音量取得できなかったとき、テレビ電源オフとみなす
        except:
            rdict = {'Power':0, 'Volume':None}

        return rdict
```

### appliance_data_logger.py
**全体を統括するメインスクリプト**です。
各モジュールを呼び出して家電からの情報を取得し、クラウドDBにアップロード（POST）します

以下の記事のスクリプトをベースに、テレビ情報取得およびクラウドDBへのアップロード機能を追加したものなので、こちらもご参照頂ければと思います。

https://qiita.com/c60evaporator/items/74fe4234d9ed5eb713d3#%E3%83%AD%E3%82%AE%E3%83%B3%E3%82%B0%E7%94%A8python%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90

```appliance_data_logger.py
from tplink import TPLink_Plug, TPLink_Bulb
from viera_gx import GetVieraData
import logging
from datetime import datetime, timedelta
import os
import csv
import configparser
import pandas as pd
import pymongo
from pit import Pit

#グローバル変数
global masterdate

######TPLinkのデータ取得######
def getdata_tplink(appliance):
    #データ値が得られないとき、最大appliance.Retry回スキャンを繰り返す
    for i in range(appliance.Retry):
        try:
            #プラグのとき
            if appliance.ApplianceType == 'TPLink_Plug':
                plg = TPLink_Plug(appliance.IP)
                applianceValue = plg.info_dict()
            #電球のとき
            elif appliance.ApplianceType == 'TPLink_ColorBulb' or appliance.ApplianceType == 'TPLink_WhiteBulb':
                blb = TPLink_Bulb(appliance.IP)
                applianceValue = blb.info_dict()
            else:
                applianceValue = None
        #エラー出たらログ出力
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, appliance{appliance.ApplianceName}]')
            applianceValue = None
            continue
        else:
            break
    
    #値取得できていたら、POSTするデータをdictに格納
    if applianceValue is not None:
        #プラグのとき
        if appliance.ApplianceType == 'TPLink_Plug':
            data = {        
                'ApplianceName': appliance.ApplianceName,        
                'Date_Master': masterdate,
                'Date': datetime.today(),
                'IsOn': applianceValue['system']['get_sysinfo']['relay_state'],
                'OnTime': applianceValue['system']['get_sysinfo']['on_time']
            }
        #電球のとき
        else:
            data = {        
                'ApplianceName': appliance.ApplianceName,        
                'Date_Master': masterdate,
                'Date': datetime.today(),
                'IsOn': applianceValue['system']['get_sysinfo']['light_state']['on_off'],
                'Color': applianceValue['system']['get_sysinfo']['light_state']['hue'],
                'ColorTemp': applianceValue['system']['get_sysinfo']['light_state']['color_temp'],
                'Brightness': applianceValue['system']['get_sysinfo']['light_state']['brightness']
            }
        return data
        
    #取得できていなかったら、ログ出力
    else:
        logging.error(f'cannot get data [loop{str(appliance.Retry)}, date{str(masterdate)}, appliance{appliance.ApplianceName}]')
        return None

######Vieraのデータ取得######
def getdata_viera(appliance):
    #データ値が得られないとき、最大appliance.Retry回スキャンを繰り返す
    for i in range(appliance.Retry):
        try:
            #GX850のとき
            if appliance.ApplianceType == 'Panasonic_VieraGX850':
                applianceValue = GetVieraData().get_gx850_data(appliance.IP)
            else:
                applianceValue = None
        #エラー出たらログ出力
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, appliance{appliance.ApplianceName}]')
            applianceValue = None
            continue
        else:
            break
    
    #値取得できていたら、POSTするデータをdictに格納
    if applianceValue is not None:
        data = {        
            'ApplianceName': appliance.ApplianceName,
            'Date_Master': masterdate,
            'Date': datetime.today(),
            'IsOn': applianceValue['Power'],
            'Volume': applianceValue['Volume']
        }
        return data
        
    #取得できていなかったら、ログ出力
    else:
        logging.error(f'cannot get data [loop{str(appliance.Retry)}, date{str(masterdate)}, appliance{appliance.ApplianceName}]')
        return None

######データのCSV出力######
def output_csv(data, csvpath):
    appliancename = data['ApplianceName']
    monthstr = masterdate.strftime('%Y%m')
    #出力先フォルダ名
    outdir = f'{csvpath}/{appliancename}/{masterdate.year}'
    #出力先フォルダが存在しないとき、新規作成
    os.makedirs(outdir, exist_ok=True)
    #出力ファイルのパス
    outpath = f'{outdir}/{appliancename}_{monthstr}.csv'
    
    #出力ファイル存在しないとき、新たに作成
    if not os.path.exists(outpath):        
        with open(outpath, 'w', newline="") as f:
            writer = csv.DictWriter(f, data.keys())
            writer.writeheader()
            writer.writerow(data)
    #出力ファイル存在するとき、1行追加
    else:
        with open(outpath, 'a', newline="") as f:
            writer = csv.DictWriter(f, data.keys())
            writer.writerow(data)

######MongoDB Atlasにアップロードする処理######
def output_mongodb_atlas(all_values_dict, user_name, cluster_name, db_name, collection_name, retry):
    passwd = ****  # Atlasのパスワード (適宜パスワード隠蔽処理を作成してください)
    for i in range(retry):
        try:
            client = pymongo.MongoClient(f"mongodb+srv://{user_name}:{passwd}@{cluster_name}.jipvx.mongodb.net/{db_name}?retryWrites=true&w=majority")
            db = client[db_name]
            collection = db[collection_name]
            result = collection.insert_one(all_values_dict)
        #エラー出たらログ出力
        except:
            if i == retry:
                logging.error(f'cannot upload to DB [loop{str(i)}, date{str(masterdate)}]')
            else:
                logging.warning(f'retry to upload to DB [loop{str(i)}, date{str(masterdate)}]')
            continue
        else:
            break

######メイン######
if __name__ == '__main__':    
    #開始時刻を取得
    startdate = datetime.today()
    #開始時刻を分単位で丸める
    masterdate = startdate.replace(second=0, microsecond=0)   
    if startdate.second >= 30:
        masterdate += timedelta(minutes=1)

    #設定ファイルと家電リスト読込
    cfg = configparser.ConfigParser()
    cfg.read('./config.ini', encoding='utf-8')
    df_appliancelist = pd.read_csv('./ApplianceList.csv')
    #全家電数とデータ取得成功数
    appliance_num = len(df_appliancelist)
    success_num = 0

    #ログの初期化
    logname = f"/appliancelog_{str(masterdate.strftime('%y%m%d'))}.log"
    logging.basicConfig(filename=cfg['Path']['LogOutput'] + logname, level=logging.INFO)

    #取得した全データ保持用dict
    all_values_dict = None

    #データ取得開始時刻
    scan_start_date = datetime.today()

    ######デバイスごとにデータ取得######
    for appliance in df_appliancelist.itertuples():
        # TPLink製品（電球＋スマートプラグ）
        if appliance.ApplianceType in ['TPLink_Plug','TPLink_ColorBulb','TPLink_WhiteBulb']:
            data = getdata_tplink(appliance)
        # テレビ
        elif appliance.ApplianceType in ['Panasonic_VieraGX850','Panasonic_VieraTX850']:
            data = getdata_viera(appliance)
        #上記以外
        else:
            data = None        

        #データが存在するとき、全データ保持用Dictに追加し、CSV出力
        if data is not None:
            #all_values_dictがNoneのとき、新たに辞書を作成
            if all_values_dict is None:
                #all_values_dictを作成（Date_MasterとDate_ScanStartのみ追加）
                all_values_dict = {'Date_Master':data['Date_Master'], 'Date_ScanStart':scan_start_date}

            #all_values_dictにDate_Master以外の要素を追加
            all_values_dict.update(dict([('no'+format(appliance.No,'02d')+'_'+k, v) for k,v in data.items() if k != 'Date_Master']))

            #CSV出力
            output_csv(data, cfg['Path']['CSVOutput'])
            #成功数プラス
            success_num+=1
    
    ######MongoDB Atlasにアップロードする処理######
    all_values_dict['_partition'] = 'Project HomeIoT'
    output_mongodb_atlas(all_values_dict, cfg['DB']['UserName'], cfg['DB']['ClusterName'], cfg['DB']['DBName'], cfg['DB']['TableName'], int(cfg['Process']['DBUploadRetry']))

    #処理終了をログ出力
    logging.info(f'[masterdate{str(masterdate)} startdate{str(startdate)} enddate{str(datetime.today())} success{str(success_num)}/{str(appliance_num)}]')
```

## 設定ファイル
家電のネットワーク情報やDBのアカウント情報を保持する設定ファイルです。

|設定ファイル名|機能|
|---|---|
|config.ini|全体統括およびクラウドDBへのデータアップロード|
|ApplianceList.csv|家電ごとにIP等の必要情報を記載|

```config.ini
[Path]
CSVOutput = CSV出力先のRaspberryPi内ディレクトリ指定
LogOutput = ログ出力先のRaspberryPi内ディレクトリ指定

[Process]
DBUploadRetry = 2 （DBへのPOST失敗時繰り返し回数）

[DB]
UserName = MongoDB Atlasのユーザ名をここに記載
ClusterName = MongoDB Atlasのクラスタ名をここに記載
DBName = MongoDB AtlasのDB名をここに記載
TableName = MongoDB Atlasのコレクション名をここに記載
```

```ApplianceList.csv
No,ApplianceName,ApplianceType,IP,Retry
1,TPLink_KL130ColorBulb_1,TPLink_ColorBulb,[IPアドレス],2
2,TPLink_KL130ColorBulb_2,TPLink_ColorBulb,[IPアドレス],2
3,TPLink_KL130ColorBulb_3,TPLink_ColorBulb,[IPアドレス],2
4,TPLink_KL110WhiteBulb_1,TPLink_WhiteBulb,[IPアドレス],2
5,TPLink_HS105Plug_1,TPLink_Plug,[IPアドレス],2
6,Panasonic_VieraTH49GX850_1,Panasonic_VieraGX850,[IPアドレス],2
```

基本的には、以下の記事の設定ファイルとほぼ同内容なので、こちらもご参照頂ければと思います。

https://qiita.com/c60evaporator/items/92d0ebde001f568f2707#2-raspberrypi%E3%81%8B%E3%82%89%E3%82%B5%E3%83%BC%E3%83%90db%E3%81%B8%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89

## cronでの定期実行を設定
Raspberry Pi内のメインスクリプトappliance_data_logger.pyに対し、**cronで5分ごとに定期実行を設定**します。
詳細は以下の記事を参照ください（Raspberry Piのシェルで行う処理です）

https://qiita.com/c60evaporator/items/283d0569eba58830f86e#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%AE%E5%AE%9A%E6%9C%9F%E5%AE%9F%E8%A1%8C

## 実行結果の確認
cronでの定期実行を設定したのち、MongoDB Atlasにデータがアップロードできていることを確認します

[MongoDB Atlasのサイト](https://cloud.mongodb.com/)からログインすると、以下のようなクラスタ一覧画面に飛びます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/c84da6e4-1d20-4472-c1b4-35dccaa6fe70.png)
**Browse Collections**をクリックするとコレクション一覧画面に飛ぶので、config.iniでアップロード先に指定したコレクション名をクリックします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/6bdefda3-5064-fc85-a04d-296eee41416b.png)
下図のように、アップロードした家電名および稼働情報のデータが格納されていれば成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/6f588db7-c368-03c3-6c52-35efae0b3cd9.png)
MongoDB AtlasはNoSQLのデータベースなので、JSONのリスト形式でデータが格納されていることが分かります

# おまけ：取得したデータのJupyterによる可視化
DBにアップロードしたデータを、Jupyterで分析してみました
以下の記事の内容をベースにしています。

https://qiita.com/c60evaporator/items/341be03ec76156ecb197#%E5%85%B7%E4%BD%93%E7%9A%84%E3%81%AA%E6%89%8B%E9%A0%86%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89db%E8%87%AA%E4%BD%9C%E5%8F%AF%E8%A6%96%E5%8C%96%E3%82%A2%E3%83%97%E3%83%AA

## 前準備
### pymongoのインストール
PythonからMongoDBにアクセスするためのライブラリ「PyMongo」をインストールします

```
pip install pymongo
```
これだけだと
The "dnspython" module must be installed to use mongodb+srv
というエラーが出るので、下記コマンドでdnspythonをインストールします

```
pip install dnspython
```

### データ取得用クラスの作成
データ取得時によく使う処理をクラス化します

```pyatlas.py
import pymongo
import pandas as pd
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta

class AtlasClient():
    #初期化
    def __init__(self, user_name, cluster_name, db_name, password):
        self.user_name = user_name
        self.db_name = db_name
        self.password = password
        self.client = pymongo.MongoClient(f"mongodb+srv://{user_name}:{password}@{cluster_name}.jipvx.mongodb.net/{db_name}?retryWrites=true&w=majority")

    #コレクション内容を取得してpd.DataFrameに格納
    #filter, projectionはこちら参照https://qiita.com/rsm223_rip/items/141eb146ad610215e5f7#%E6%A4%9C%E7%B4%A2%E6%96%B9%E6%B3%95
    def get_collection_to_df(self, collection_name, filter=None, projection=None):
        collection = self.client[self.db_name][collection_name]
        cursor = collection.find(filter=filter, projection=projection)
        df = pd.DataFrame(list(cursor))
        return df

    #前月データをCSV保存
    def backup_previous_month(self, collection_name, date_column, ref_time, output_dir):
        prev_month = ref_time - relativedelta(months=1)
        startdate = prev_month.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
        enddate = ref_time.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
        flt = {date_column:{"$gte":startdate, "$lt":enddate}}
        df = self.get_collection_to_df(collection_name, filter=flt)
        df.to_csv(f'{output_dir}/{startdate.strftime("%Y%m")}.csv', index=False)

    #コレクションを全て削除
    def drop_collection(self, collection_name):
        self.client[self.db_name][collection_name].remove()
    
    #コレクションからフィルタ条件で削除
    def delete_collection_data(self, collection_name, del_filter):
        collection = self.client[self.db_name][collection_name]
        collection.delete_many(del_filter)

    #コレクションから一定日以上前のデータを削除
    def delete_previous_data(self, collection_name, date_column, delete_days):
        del_end = datetime.now() - timedelta(days=delete_days)
        del_filter = {date_column:{"$lt":del_end}}
        self.delete_collection_data(collection_name, del_filter)
```

### 家電一覧のリスト化
可視化対象の家電一覧をリスト化します。

IsOnNaN列は、On-Offを表すIsOn列がNaNだったときの補完文字を指定します（基本的には0で補完）

```appliance_disp
No,ApplianceName,IsOnNaN
1,TPLink_KL130ColorBulb_1,0
2,TPLink_KL130ColorBulb_2,0
3,TPLink_KL130ColorBulb_3,0
4,TPLink_KL110WhiteBulb_1,0
5,TPLink_HS105Plug_1,0
6,Panasonic_VieraTH49GX850_1,0
```

## データの可視化
実際にデータを可視化してみます

### データの読込確認
14日前までのデータをPandasのDataFrameに読み込み、直近5回の取得内容を確認します

```python
from pyatlas import AtlasClient
from datetime import datetime, timedelta
import pandas as pd
import configparser

#各種定数の定義
USER_NAME = MongoDB Atlasのユーザ名
PASSWORD = MongoDB Atlasのユーザ名
CLUSTER_NAME = MongoDB AtlasのCluster名
DB_NAME = MongoDB AtlasのDB名
COLLECTION_NAME = MongoDB Atlasのコレクション名

#コレクション内容読込
atlasclient = AtlasClient(user_name=USER_NAME, cluster_name=CLUSTER_NAME, db_name=DB_NAME, password=PASSWORD)
startdate = datetime.now() - timedelta(days=14)  # 14日前の日時
flt = {"Date_Master":{"$gte":startdate}}  # 直近14日間のデータに絞るためのフィルタdict
df = atlasclient.get_collection_to_df(COLLECTION_NAME, filter=flt)  # フィルタ適用

#IsOn列のNaN補完
df_appliance_settings = pd.read_csv('./appliance_disp_settings.csv')
for appliance in df_appliance_settings.itertuples():
    if f'no{format(appliance.No,"02d")}_IsOn' in list(df.columns):
        df = df.fillna({f'no{format(appliance.No,"02d")}_IsOn': 0})
df.tail(5)
```

```:実行結果
_id	Date_Master	Date_ScanStart	no01_ApplianceName	no01_Date	no01_IsOn	no01_Color	no01_ColorTemp	no01_Brightness	no02_ApplianceName	...	no04_Brightness	no05_ApplianceName	no05_Date	no05_IsOn	no05_OnTime	no06_ApplianceName	no06_Date	no06_IsOn	no06_Volume	_partition
3978	611646284b706f748fd80466	2021-08-13 19:15:00	2021-08-13 19:15:03.070	TPLink_KL130ColorBulb_1	2021-08-13 19:15:03.302	1.0	0.0	2700.0	100.0	TPLink_KL130ColorBulb_2	...	100.0	TPLink_HS105Plug_1	2021-08-13 19:15:04.044	0	0	Panasonic_VieraTH49GX850_1	2021-08-13 19:15:04.062	0	NaN	Project HomeIoT
3979	61164753095694901faf6c9b	2021-08-13 19:20:00	2021-08-13 19:20:02.911	TPLink_KL130ColorBulb_1	2021-08-13 19:20:03.144	1.0	0.0	2700.0	100.0	TPLink_KL130ColorBulb_2	...	100.0	TPLink_HS105Plug_1	2021-08-13 19:20:03.804	0	0	Panasonic_VieraTH49GX850_1	2021-08-13 19:20:03.820	0	NaN	Project HomeIoT
3980	6116487f894fba67eebd8b76	2021-08-13 19:25:00	2021-08-13 19:25:02.788	TPLink_KL130ColorBulb_1	2021-08-13 19:25:02.866	1.0	0.0	2700.0	100.0	TPLink_KL130ColorBulb_2	...	100.0	TPLink_HS105Plug_1	2021-08-13 19:25:03.560	0	0	Panasonic_VieraTH49GX850_1	2021-08-13 19:25:03.579	0	NaN	Project HomeIoT
3981	611649abbcff49bf41646e85	2021-08-13 19:30:00	2021-08-13 19:30:02.847	TPLink_KL130ColorBulb_1	2021-08-13 19:30:02.977	1.0	0.0	2700.0	100.0	TPLink_KL130ColorBulb_2	...	100.0	TPLink_HS105Plug_1	2021-08-13 19:30:03.626	0	0	Panasonic_VieraTH49GX850_1	2021-08-13 19:30:03.646	0	NaN	Project HomeIoT
3982	61164ad7fe9f868c1914b9a2	2021-08-13 19:35:00	2021-08-13 19:35:02.600	TPLink_KL130ColorBulb_1	2021-08-13 19:35:02.850	1.0	0.0	2700.0	100.0	TPLink_KL130ColorBulb_2	...	100.0	TPLink_HS105Plug_1	2021-08-13 19:35:03.239	0	0	Panasonic_VieraTH49GX850_1	2021-08-13 19:35:03.258	0	NaN	Project HomeIoT
```
無事DataFrameに読込できていることがわかりました

### On-Offを折れ線グラフで可視化
電源がOnかOffかを折れ線グラフで可視化します

```python
import numpy as np
import matplotlib.pyplot as plt
#抽出対象列（'Date_Master' + '_IsOn'で終わる列）
datecols = df.columns.str.endswith('Date_Master')
isoncols = df.columns.str.endswith('_IsOn')
extractcols = np.logical_or(datecols, isoncols)
df_ison = df.iloc[:, extractcols]
#列名変更
name_dict = {f'no{format(appliance.No,"02d")}_IsOn':appliance.ApplianceName for appliance in df_appliance_settings.itertuples()}
df_ison = df_ison.rename(columns=name_dict)

#縦並びのaxesを作成
fig, axes = plt.subplots(len(df_ison.columns)-1,sharex=True, figsize=(10, len(df_ison.columns)-1))
fig.suptitle('Appliance Operation')
#色をcolor cycleから取得(https://qiita.com/skotaro/items/5c9893d186ccd31f459d)
cmap = plt.get_cmap("tab10")
#デバイス数分の折れ線グラフをプロット
for i in range(len(df_ison.columns)-1):
    axes[i].plot(df_ison['Date_Master'], df_ison[df_ison.columns[i + 1]], label=df_ison.columns[i + 1], color=cmap(i))
    #凡例を表示
    axes[i].legend(framealpha=0.6)
#軸ラベルを表示＆90度回転
axes[len(df_ison.columns)-2].set_xlabel('Date')
axes[len(df_ison.columns)-2].tick_params(axis='x',labelrotation=90)
```
![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/2c61640b-7c75-8c82-5a82-5f7b1fd189de.png)
電源Onなら1、Offなら0として時間変化を可視化できていることが分かります。

### 家電同士のOn-Offの相関を見る
ある家電がOnかどうかと、他の家電がOnかどうかの相関を分析してみます。
以下の記事のツールを使用しました

https://qiita.com/c60evaporator/items/20f11b6ee965cec48570

```python
from seaborn_analyzer import CustomPairPlot
cp = CustomPairPlot()
cp.pairanalyzer(df_ison)
```
![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/bcf4158a-1b62-9874-8896-8a82ba8d3f86.png)

以下の情報が読み取れます

|相関関係|読み取れる情報|
|---|---|
|・**TPLinkのKL130は3個とも完全な相関**（R=1.0）がある|同じシーリングファンに取り付けているので、同時にOn-Offされる|
|・**KL130とKL110**は別の場所に取り付けられた照明だが、**高い相関**（R=0.85）がある|廊下の照明と居室の照明は同時にONとなっていることが分かる　→　**どちらかを消せば電気代節約できそう**|
|・**テレビ（Panasonic Viera）は照明と弱い相関**にあるが、バブルチャートを見ると**テレビがONであるときは照明もほぼ確実にON**であることが分かる|照明が暗い状態でテレビを見るのは明らかに不自然なので、感覚とも一致する結果|
|上記と**逆の関係**である「照明がONのときテレビがON」は**成り立たない**|居室にいてもテレビを見ていない時間の方が多い|
|・**スマートプラグ**（IKEAの間接照明に接続）はほぼOffで、**ほとんど使用されていない**ことがわかった|間接照明のような**オシャレアイテムは私には不要**であることが判明した……笑|

これだけのシンプルな分析でも、家電の使用状況が分かって面白いですね！