LoginSignup
16
17

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-08-28

はじめに

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

・テレビ
・エアコン
・照明
・コンセント(スマートプラグ)

動作情報を、RaspberryPiで一括取得するシステムを構築してみました
image.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間接照明に使用
エアコン 三菱電機 MSZ-SV287-W OnOff・冷暖房・設定温度・風量・風向 NatureRemoで情報取得

上記家電の設置と使用方法を、写真を交えて紹介します

テレビ

以前購入したPanasonicのVIERA TH-49GX850Python APIによる情報取得に対応していたため、これを活用しました
image.png
最近のVIERAであれば、大半はPythonで情報取得が可能かと思います。
ざっと見た限りでは最近のSONY BRAVIASHARP AQUOSも対応していそうな感じです

照明

TPLinkのスマート電球、KL130を使用しています。
1台では暗いので、3台を以下のようなシーリングファンに取り付けました
image.png

コンセント

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

我が家では、以下のようなIKEAの間接照明のON-OFFに使用しています。
image.png

エアコン

三菱電機の15年前のモデルです

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

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

なお詳細は後述しますが、本システムではエアコンのみ別系統(他の家電とは別のRaspberry Pi)で情報取得しているのでご注意ください。

情報取得に使用した技術

下記のAPI等を利用して、各機器のOn-Off等の情報を取得しました

テレビ(Panasonic VIERA)

テレビ
以下のモジュールを使用させていただきました。

上記モジュールは、VIERA内部で稼働しているHTTPサーバから、APIを通じて情報を取得するようです。上記以外にも、こちらこちらを参考にさせて頂きました。

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

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

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

照明・コンセント

製造元のTPLink社のAPIを使用しました。
詳しくは以下の記事を参照ください

エアコン(Nature Remoから取得)

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

ソフトウェア構成

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

テレビ・照明・コンセント

以下のようなソフトウェア構成となっています
image.png
詳細は後述しますが、Raspberry Pi上のPythonスクリプトでデータを取得し、クラウドDB(MongoDB Atlas)にアップロードしています。

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

エアコン

Nature Remoを使用して情報を取得するため、他の家電とは別系統のこちらの記事のシステムの一部としてデータ取得しています。
image.png
最終的なデータの出力先は、他の家電と同じくクラウドDBのMongoDB Atlasですが、他の家電が「appliances」コレクション(RDBにおけるテーブルに相当)に格納しているのに対し、エアコンのみ「sensors」コレクションに格納しております。

Remoを通じたエアコンからの情報取得は上記記事を、クラウドDBへのアップロードに関しては以下の記事をご参照ください。

情報取得に使用した機器

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

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

実装

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

MongoDB Atlasへの登録とコレクションの作成

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

フォルダ構成

以下のフォルダ構成で、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モジュールをそのままコピーさせて頂きました。

主要なモジュール

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

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

tplink.py

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

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

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

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情報(および音量情報)を取得します。
前述のように、こちらのモジュールを呼び出して音量情報を取得し、音量が取得出来たら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へのアップロード機能を追加したものなので、こちらもご参照頂ければと思います。

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

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

cronでの定期実行を設定

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

実行結果の確認

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

MongoDB Atlasのサイトからログインすると、以下のようなクラスタ一覧画面に飛びます
image.png
Browse Collectionsをクリックするとコレクション一覧画面に飛ぶので、config.iniでアップロード先に指定したコレクション名をクリックします
image.png
下図のように、アップロードした家電名および稼働情報のデータが格納されていれば成功です。
image.png
MongoDB AtlasはNoSQLのデータベースなので、JSONのリスト形式でデータが格納されていることが分かります

おまけ:取得したデータのJupyterによる可視化

DBにアップロードしたデータを、Jupyterで分析してみました
以下の記事の内容をベースにしています。

前準備

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で補完)

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回の取得内容を確認します

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かを折れ線グラフで可視化します

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
電源Onなら1、Offなら0として時間変化を可視化できていることが分かります。

家電同士のOn-Offの相関を見る

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

from seaborn_analyzer import CustomPairPlot
cp = CustomPairPlot()
cp.pairanalyzer(df_ison)

output.png

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

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

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

16
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
17