はじめに
IoTという言葉がブームとなって久しいですが、家電に関してもこのブームに乗り、APIで情報取得できる機種が増えています。
そこで今回、家電の中でも特に使用頻度の高い、
・テレビ
・エアコン
・照明
・コンセント(スマートプラグ)
の動作情報を、RaspberryPiで一括取得するシステムを構築してみました
使用した機器
情報取得対象の家電、および情報取得に使用した機器を紹介します
情報取得対象の家電
以下の家電の情報を取得しました
種類 | メーカー | 名称 | 情報取得対象 | 備考 |
---|---|---|---|---|
テレビ | 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-49GX850がPython APIによる情報取得に対応していたため、これを活用しました
最近のVIERAであれば、大半はPythonで情報取得が可能かと思います。
ざっと見た限りでは最近のSONY BRAVIAやSHARP AQUOSも対応していそうな感じです
照明
TPLinkのスマート電球、KL130を使用しています。
1台では暗いので、3台を以下のようなシーリングファンに取り付けました
コンセント
IoTでON-OFF制御できるスマートプラグは、電源供給のON-OFFのみで制御できる機器に幅広く活用でき、非常に便利です。
我が家では、以下のようなIKEAの間接照明のON-OFFに使用しています。
エアコン
三菱電機の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、冷暖房、設定温度、風向、風量情報を取得します。
詳しくは以下の記事を参照ください
ソフトウェア構成
エアコンとそれ以外で、データの取得系統が大きく異なるので、分けて解説します
テレビ・照明・コンセント
以下のようなソフトウェア構成となっています
詳細は後述しますが、Raspberry Pi上のPythonスクリプトでデータを取得し、クラウドDB(MongoDB Atlas)にアップロードしています。
また、バックアップ用としてRaspberry Pi4内にCSVファイルを保存する機能と、トラブル時の原因解析のためのログ保存機能も実装しております。
エアコン
Nature Remoを使用して情報を取得するため、他の家電とは別系統のこちらの記事のシステムの一部としてデータ取得しています。
最終的なデータの出力先は、他の家電と同じくクラウド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()を継承)
詳細は以下の記事を参照ください
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と判定しています。
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へのアップロード機能を追加したものなので、こちらもご参照頂ければと思います。
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等の必要情報を記載 |
[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のコレクション名をここに記載
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のサイトからログインすると、以下のようなクラスタ一覧画面に飛びます
Browse Collectionsをクリックするとコレクション一覧画面に飛ぶので、config.iniでアップロード先に指定したコレクション名をクリックします
下図のように、アップロードした家電名および稼働情報のデータが格納されていれば成功です。
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
データ取得用クラスの作成
データ取得時によく使う処理をクラス化します
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)
電源Onなら1、Offなら0として時間変化を可視化できていることが分かります。
家電同士のOn-Offの相関を見る
ある家電がOnかどうかと、他の家電がOnかどうかの相関を分析してみます。
以下の記事のツールを使用しました
from seaborn_analyzer import CustomPairPlot
cp = CustomPairPlot()
cp.pairanalyzer(df_ison)
以下の情報が読み取れます
相関関係 | 読み取れる情報 |
---|---|
・TPLinkのKL130は3個とも完全な相関(R=1.0)がある | 同じシーリングファンに取り付けているので、同時にOn-Offされる |
・KL130とKL110は別の場所に取り付けられた照明だが、高い相関(R=0.85)がある | 廊下の照明と居室の照明は同時にONとなっていることが分かる → どちらかを消せば電気代節約できそう |
・テレビ(Panasonic Viera)は照明と弱い相関にあるが、バブルチャートを見るとテレビがONであるときは照明もほぼ確実にONであることが分かる | 照明が暗い状態でテレビを見るのは明らかに不自然なので、感覚とも一致する結果 |
上記と逆の関係である「照明がONのときテレビがON」は成り立たない | 居室にいてもテレビを見ていない時間の方が多い |
・スマートプラグ(IKEAの間接照明に接続)はほぼOffで、ほとんど使用されていないことがわかった | 間接照明のようなオシャレアイテムは私には不要であることが判明した……笑 |
これだけのシンプルな分析でも、家電の使用状況が分かって面白いですね!