TPLinkとは?
ルータを主力とする中国・深圳のネットワーク機器メーカーです。
近年はスマート電球、スマートプラグ等のIoT家電に力を入れており、コスパの良さからAmazonで独自の地位を築いています。
今回は、APIを使用して、
・機器のON-OFF操作
・ON-OFF、電球の明るさ等の情報取得
を、PythonおよびNode.jsで実行してみました
IoT家電として思いつく用途の多くを上記でカバーできるので
応用の可能性を感じる結果となりました!
必要なもの
・PC
・RaspberryPi
・TPLink製スマートプラグあるいは電球
今回は下記3製品を試しました
HS105:スマートプラグ
KL110:ホワイト電球
KL130:カラー電球
①データ取得の確認
まずは、TPLinkからデータが取得できるかターミナル上でテストします。
※参考にさせて頂いた記事
https://lmjs7.net/blog/tag/tp-link/
https://qiita.com/tmisuoka0423/items/582ff0c303abe8570ee5
IPを調べる
tplink-smarthome-api(参考)をインストール
sudo npm install -g tplink-smarthome-api
下記コマンドで、接続しているTPLinkデバイス一覧を取得
tplink-smarthome-api search
HS105(JP) plug IOT.SMARTPLUGSWITCH 192.168.0.101 9999 B0BE76‥ スマートプラグ
KL110(JP) bulb IOT.SMARTBULB 192.168.0.102 9999 98DAC4‥ ホワイト電球
KL130(JP) bulb IOT.SMARTBULB 192.168.0.103 9999 0C8063‥ カラー電球
3つのデバイス全てが検出できていることが分かります
デバイス動作情報の取得確認
下記コマンドで、デバイスの設定やOnOffが取得できる
tplink-smarthome-api getSysInfo [デバイスのIPアドレス]:9999
・KL130(カラー電球)の例
:
ctrl_protocols: { name: 'Linkie', version: '1.0' },
↓ここからがデバイスの設定
light_state: {
on_off: 1,
mode: 'normal',
hue: 0,
saturation: 0,
color_temp: 2700,
brightness: 100
},
↑ここまでがデバイスの設定
is_dimmable: 1,
is_color: 1,
:
on_off:0なら電源OFF、1なら電源ON
hue:色?(白色モードのとき0)
color_temp:色温度(白色モード以外のとき0)
brightness:明るさ(%単位)
と思われます
・KL110(ホワイト電球)の例
:
ctrl_protocols: { name: 'Linkie', version: '1.0' },
↓ここからがデバイスの設定
light_state: {
on_off: 1,
mode: 'normal',
hue: 0,
saturation: 0,
color_temp: 2700,
brightness: 100
},
↑ここまでがデバイスの設定
is_dimmable: 1,
is_color: 0,
:
on_off:0なら電源OFF、1なら電源ON
hue:色相(白色モードのとき0)
saturation:彩度
color_temp:色温度(白色モード以外のとき0)
brightness:明るさ(%単位)
と思われます。
KL130とほぼ同じですが、カラーではないのでis_color: 0となっていると思われます。
・KL105(スマートプラグ)の例
alias: '',
↓ここからがデバイスの設定
relay_state: 1,
on_time: 288,
active_mode: 'none',
feature: 'TIM',
updating: 0,
icon_hash: '',
rssi: -52,
led_off: 0,
longitude_i: 1356352,
latitude_i: 348422,
↑ここまでがデバイスの設定
hwId: '047D‥',
relay_state:0なら電源OFF、1なら電源ON
on_time:連続電源ON時間
rssi: WiFiの信号強度
と思われます。
経度(logitude)と緯度(latitude)も表示されていますが、実際の場所と5キロくらいずれていて謎が深まります。
上記で、コマンドで欲しい情報が取得できることが確認できました!
次章以降で、プログラム(Node.js&Python)から取得・操作する方法を記載します。
②Node.jsで状態取得
※「Pythonを使うからNode.jsの説明はいらん!」という方は、この章を飛ばして③に移動してください
こちらを参考に、Node.jsを
npmにパスを通す(Windowの場合)
Windowsだとnpmのグローバルインストール先にパスが通っておらず、Node.jsでモジュールが読み込めないので、下記を参考にパスを通してください
https://qiita.com/shiftsphere/items/5610f692899796b03f99
npmにパスを通す(RaspberryPiの場合)
下記コマンドで、グローバルでのnpmモジュールインストール先を調べます
(なぜかWindowsのときのコマンド"npm bin -g"で見つかるフォルダとは違うようです)
npm ls -g
下記コマンドで.profileを編集します。
※SSH環境では.profileの代わりに、.bash_profileを編集してください
nano /home/[ユーザ名]/.profile
.profileの最後に下記の1行を追加してrebootしてください
export NODE_PATH=[上で調べたパス]/node_modules
下記コマンドで指定したパスが表示されれば成功です
printenv NODE_PATH
node.jsスクリプトの作成
下記スクリプトを作成します
const{Client}=require('tplink-smarthome-api');constclient=newClient();client.getDevice({host:'192.168.0.102'}).then(device=>{device.getSysInfo().then(console.log);});
下記コマンドでスクリプトを実行すると、①と同様に各種情報が取得できます
node tplink_test.js
※上記をcsvロギングするスクリプト(③のPythonスクリプトと同機能)も作成しましたが、私のJavaScriptスキルが低くうまく動作しないときがある(非同期部分の処理順が逆転する)ので、コードはここには貼らないこととします
下記GitHubにアップロードしたので、自己責任で改造して使用していただければと思います。
(願わくば無知な私に処理順が逆転する理由もコメント…頂けると嬉しいです笑)
https://github.com/c60evaporator/TPLink_Info_Nodejs
③Pythonで状態取得
私のJavaScriptスキル不足でNode.jsでのロギングが上手くいかなかったので、
気を取り直してPythonで操作・ロギングするスクリプトを作りました。
PythonはNode.jsほど丁寧なドキュメントが見当たらず苦戦しましたが、こちらやこちらのコードを解読して、スクリプトを作成しました。
TPLink操作クラスの作成
上記コードを参考に、下記の4つのクラスを作成しました
TPLink_Common():プラグ、電球共通機能のクラス
TPLink_Plug():プラグ専用機能のクラス(TPLink_Common()を継承)
TPLink_Bulb():電球専用機能のクラス(TPLink_Common()を継承)
GetTPLinkData():上記クラスを利用して、データを取得するクラス
importsocketfromstructimportpackimportjson#TPLinkデータ取得用クラス
classGetTPLinkData():#プラグデータ取得用メソッド
defget_plug_data(self,ip):#プラグ操作用クラス作成
plg=TPLink_Plug(ip)#データを取得し、dictに変換
rjson=plg.info()rdict=json.loads(rjson)returnrdict#電球データ取得用メソッド
defget_bulb_data(self,ip):#電球操作用クラス作成
blb=TPLink_Bulb(ip)#データを取得し、dictに変換
rjson=blb.info()rdict=json.loads(rjson)returnrdict#TPLink電球&プラグ共通クラス
classTPLink_Common():def__init__(self,ip,port=9999):"""Default constructor
"""self.__ip=ipself.__port=portdefinfo(self):cmd='{"system":{"get_sysinfo":{}}}'receive=self.send_command(cmd)returnreceivedefsend_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)returndecryptedexceptsocket.error:quit("Could not connect to host "+self.__ip+":"+str(self.__port))returnNonedefencrypt(self,string):key=171result=pack('>I',len(string))foriinstring:a=key^ord(i)key=aresult+=bytes([a])returnresultdefdecrypt(self,string):key=171result=""foriinstring:a=key^ikey=iresult+=chr(a)returnresult#TPLinkプラグ操作用クラス
classTPLink_Plug(TPLink_Common):defon(self):cmd='{"system":{"set_relay_state":{"state":1}}}'receive=self.send_command(cmd)defoff(self):cmd='{"system":{"set_relay_state":{"state":0}}}'receive=self.send_command(cmd)defledon(self):cmd='{"system":{"set_led_off":{"off":0}}}'receive=self.send_command(cmd)defledoff(self):cmd='{"system":{"set_led_off":{"off":1}}}'receive=self.send_command(cmd)defset_countdown_on(self,delay):cmd='{"count_down":{"add_rule":{"enable":1,"delay":'+str(delay)+',"act":1,"name":"turn on"}}}'receive=self.send_command(cmd)defset_countdown_off(self,delay):cmd='{"count_down":{"add_rule":{"enable":1,"delay":'+str(delay)+',"act":0,"name":"turn off"}}}'receive=self.send_command(cmd)defdelete_countdown_table(self):cmd='{"count_down":{"delete_all_rules":null}}'receive=self.send_command(cmd)defenergy(self):cmd='{"emeter":{"get_realtime":{}}}'receive=self.send_command(cmd)returnreceive#TPLink電球操作用クラス
classTPLink_Bulb(TPLink_Common):defon(self):cmd='{"smartlife.iot.smartbulb.lightingservice":{"transition_light_state":{"on_off":1}}}'receive=self.send_command(cmd)defoff(self):cmd='{"smartlife.iot.smartbulb.lightingservice":{"transition_light_state":{"on_off":0}}}'receive=self.send_command(cmd)deftransition_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:vfork,vinlocals().items()ifkisnot'self'andvisnotNone}r={'smartlife.iot.smartbulb.lightingservice':{'transition_light_state':d}}cmd=json.dumps(r)receive=self.send_command(cmd)print(receive)defbrightness(self,brightness):self.transition_light_state(brightness=brightness)defpurple(self,brightness=None,transition_period=None):self.transition_light_state(hue=277,saturation=86,color_temp=0,brightness=brightness,transition_period=transition_period)defblue(self,brightness=None,transition_period=None):self.transition_light_state(hue=240,saturation=100,color_temp=0,brightness=brightness,transition_period=transition_period)defcyan(self,brightness=None,transition_period=None):self.transition_light_state(hue=180,saturation=100,color_temp=0,brightness=brightness,transition_period=transition_period)defgreen(self,brightness=None,transition_period=None):self.transition_light_state(hue=120,saturation=100,color_temp=0,brightness=brightness,transition_period=transition_period)defyellow(self,brightness=None,transition_period=None):self.transition_light_state(hue=60,saturation=100,color_temp=0,brightness=brightness,transition_period=transition_period)deforange(self,brightness=None,transition_period=None):self.transition_light_state(hue=39,saturation=100,color_temp=0,brightness=brightness,transition_period=transition_period)defred(self,brightness=None,transition_period=None):self.transition_light_state(hue=0,saturation=100,color_temp=0,brightness=brightness,transition_period=transition_period)deflamp_color(self,brightness=None):self.transition_light_state(color_temp=2700,brightness=brightness)
TPLink操作クラスの実行方法
上記クラスは、Pythonコード上で下記のように実行できます
・電球の電源をONにしたいとき
TPLink_Bulb(電球のIPアドレス).on()
・プラグの電源をOFFにしたいとき
TPLink_Plug(プラグのIPアドレス).off()
・10秒後にプラグをONにしたいとき
TPLink_Plug(プラグのIPアドレス).set_countdown_on(10)
・電球の明るさを10%にしたいとき
TPLink_Bulb(電球のIPアドレス).brightness(10)
・電球を赤色にしたいとき(カラー電球のみ)
TPLink_Bulb(電球のIPアドレス).red()
・電球のOn-Off等の情報を取得
info=GetTPLinkData().get_plug_data(プラグのIPアドレス)
※上記メソッドは、取得したjson情報をdict形式に変換して出力されます。
出力される電球情報は①を参照ください
④ロギング用Pythonスクリプトの作成
前章最後の方法を利用して、電球やプラグの情報をロギングするスクリプトを作成しました。
スクリプトの構造はこちらと同じなので、リンク先をご一読いただければと思います。
設定ファイル
こちらの記事同様、管理をしやすくするため下記2種類の設定ファイルを作成しました
・DeviceList.csv:センサごとに必要情報を記載
DeviceList.csv
ApplianceName,ApplianceType,IP,Retry
TPLink_KL130_ColorBulb_1,TPLink_ColorBulb,192.168.0.103,2
TPLink_KL110_WhiteBulb_1,TPLink_WhiteBulb,192.168.0.102,2
TPLink_HS105_Plug_1,TPLink_Plug,192.168.0.101,2
カラムの意味は下記となります
ApplianceName:デバイス名を管理、同種類のデバイスが複数あるときの識別用
ApplianceType:デバイスの種類。
TPLink_ColorBulb:カラー電球(KL130等)
TPLink_WhiteBulb:白色電球(KL110等)
TPLink_Plug:スマートプラグ(HS105等)
IP:デバイスのIPアドレス
Retry:最大再実行回数詳細(取得失敗時の再実行回数、詳しくはこちら)
・config.ini:CSVおよびログ出力ディレクトリを指定
config.ini
[Path]
CSVOutput = /share/Data/Appliance
LogOutput = /share/Log/Appliance
どちらもsambaで作成した共有フォルダ内に出力すると、RaspberryPi外からアクセスできて便利です。
実際のスクリプト
fromtplinkimportGetTPLinkDataimportloggingfromdatetimeimportdatetime,timedeltaimportosimportcsvimportconfigparserimportpandasaspd#グローバル変数
globalmasterdate######TPLinkのデータ取得######
defgetdata_tplink(appliance):#データ値が得られないとき、最大appliance.Retry回スキャンを繰り返す
foriinrange(appliance.Retry):try:#プラグのとき
ifappliance.ApplianceType=='TPLink_Plug':applianceValue=GetTPLinkData().get_plug_data(appliance.IP)#電球のとき
elifappliance.ApplianceType=='TPLink_ColorBulb'orappliance.ApplianceType=='TPLink_WhiteBulb':applianceValue=GetTPLinkData().get_bulb_data(appliance.IP)else:applianceValue=None#エラー出たらログ出力
except:logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, appliance{appliance.ApplianceName}]')applianceValue=Nonecontinueelse:break#値取得できていたら、POSTするデータをdictに格納
ifapplianceValueisnotNone:#プラグのとき
ifappliance.ApplianceType=='TPLink_Plug':data={'ApplianceName':appliance.ApplianceName,'Date_Master':str(masterdate),'Date':str(datetime.today()),'IsOn':str(applianceValue['system']['get_sysinfo']['relay_state']),'OnTime':str(applianceValue['system']['get_sysinfo']['on_time'])}#電球のとき
else:data={'ApplianceName':appliance.ApplianceName,'Date_Master':str(masterdate),'Date':str(datetime.today()),'IsOn':str(applianceValue['system']['get_sysinfo']['light_state']['on_off']),'Color':str(applianceValue['system']['get_sysinfo']['light_state']['hue']),'ColorTemp':str(applianceValue['system']['get_sysinfo']['light_state']['color_temp']),'Brightness':str(applianceValue['system']['get_sysinfo']['light_state']['brightness'])}returndata#取得できていなかったら、ログ出力
else:logging.error(f'cannot get data [loop{str(appliance.Retry)}, date{str(masterdate)}, appliance{appliance.ApplianceName}]')returnNone######データのCSV出力######
defoutput_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'#出力ファイル存在しないとき、新たに作成
ifnotos.path.exists(outpath):withopen(outpath,'w',newline="")asf:writer=csv.DictWriter(f,data.keys())writer.writeheader()writer.writerow(data)#出力ファイル存在するとき、1行追加
else:withopen(outpath,'a',newline="")asf:writer=csv.DictWriter(f,data.keys())writer.writerow(data)######メイン######
if__name__=='__main__':#開始時刻を取得
startdate=datetime.today()#開始時刻を分単位で丸める
masterdate=startdate.replace(second=0,microsecond=0)ifstartdate.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######デバイスごとにデータ取得######
forapplianceindf_appliancelist.itertuples():#ApplianceTypeがTPLinkeであることを確認
ifappliance.ApplianceTypein['TPLink_Plug','TPLink_ColorBulb','TPLink_WhiteBulb']:data=getdata_tplink(appliance)#上記以外
else:data=None#データが存在するとき、全データ保持用Dictに追加し、CSV出力
ifdataisnotNone:#all_values_dictがNoneのとき、新たに辞書を作成
ifall_values_dictisNone:all_values_dict={data['ApplianceName']:data}#all_values_dictがNoneでないとき、既存の辞書に追加
else:all_values_dict[data['ApplianceName']]=data#CSV出力
output_csv(data,cfg['Path']['CSVOutput'])#成功数プラス
success_num+=1#処理終了をログ出力
logging.info(f'[masterdate{str(masterdate)} startdate{str(startdate)} enddate{str(datetime.today())} success{str(success_num)}/{str(appliance_num)}]')
上記を実行すれば、設定ファイル"CSVOutput"で指定したフォルダに、取得データがデバイス名と日時の名称でCSV出力されます
以上で、情報取得が完了です
おわりに
RaspberrypPiで24時間稼働、かつPythonはIFTTTよりも自由度が高いので、色々なアイデアを具現化可能です
・人感センサと組み合わせて、人が入ったら電気が点くようにする
・30分以上人がいなければ、電気を消す
・人によって電球の明るさを自動で切り替える
などなどです。
いくつか作りたいものがあるので、製作が完了したらまた記事にしようと思います