もう週末でだいたい終わってるから、ちょっとCosmos DBのネタでも考えるか…。
そういや、TP-Linkのカメラ・Tapoって、JSONを返してくるんじゃないか?そのままCosmosに突っ込めると何か遊べる(?)かも。
あー、PyTapoってのがあるじゃん、ステキ♡
pip install pytapoして、from pytapo import Tapoね、おけ。
from pytapo import Tapo
host = '192.168.50.6'
user = 'user'
password = 'password'
tapo = Tapo(host, user, password)
print(tapo.getBasicInfo())
じゃあ、実行。
% python3 test_tapo.py
Traceback (most recent call last):
File "/opt/homebrew/lib/python3.11/site-packages/urllib3/connectionpool.py", line 467, in _make_request
self._validate_conn(conn)
File "/opt/homebrew/lib/python3.11/site-packages/urllib3/connectionpool.py", line 1092, in _validate_conn
conn.connect()
File "/opt/homebrew/lib/python3.11/site-packages/urllib3/connection.p
......
requests.exceptions.SSLError: HTTPSConnectionPool(host='192.168.50.6', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLError(1, '[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1002)')))
SSLのエラー?
なんで?
% git clone https://github.com/JurajNyiri/pytapo
__init__pyとか、requestsにverify=Falseって指定してるやん。なんで??
81 res = requests.post(
82 url, data=json.dumps(data), headers=self.headers, verify=False
83 )
macOSだからか?(Linuxで実行しても同じ。)
あ。Pythonのバージョン???
% ll /opt/homebrew/bin/python3
lrwxr-xr-x 1 rifujita admin 40 6 12 20:29 /opt/homebrew/bin/python3 -> ../Cellar/python@3.11/3.11.4/bin/python3
今実行したの、3.11か。確か3.10もあったよな。
% python3.10 -m pip install pytapo
どれ?
% python3.10 test_tapo.py
Traceback (most recent call last):
File "/Users/rifujita/Desktop/test_tapo.py", line 8, in <module>
tapo = Tapo(host, user, password)
File "/opt/homebrew/lib/python3.10/site-packages/pytapo/__init__.py", line 48, in __init__
self.basicInfo = self.getBasicInfo()
File "/opt/homebrew/lib/python3.10/site-packages/pytapo/__init__.py", line 446, in getBasicInfo
return self.executeFunction(
File "/opt/homebrew/lib/python3.10/site-packages/pytapo/__init__.py", line 119, in executeFunction
data = self.performRequest(
File "/opt/homebrew/lib/python3.10/site-packages/pytapo/__init__.py", line 148, in performRequest
self.ensureAuthenticated()
File "/opt/homebrew/lib/python3.10/site-packages/pytapo/__init__.py", line 68, in ensureAuthenticated
return self.refreshStok()
File "/opt/homebrew/lib/python3.10/site-packages/pytapo/__init__.py", line 98, in refreshStok
raise Exception("Invalid authentication data")
Exception: Invalid authentication data
認証データがあかんと。
んー、コードに埋めたユーザーとパスワードは合ってるんだよなぁ。Issueとかあるかな?(もう一度、PyPiのページを見る。)
お前か…。じゃあ、ユーザー名をadminにしてみっか。
from pytapo import Tapo
host = '192.168.50.6'
user = 'admin'
password = 'password'
tapo = Tapo(host, user, password)
print(tapo.getBasicInfo())
で、実行、と。
% python3.10 test_tapo.py
{'device_info': {'basic_info': {'ffs': False, 'device_type': 'SMART.IPCAMERA', 'device_model': 'C320WS', 'device_name': 'C320WS 1.0', 'device_info': 'C320WS 1.0 IPC', 'hw_version': '1.0', 'sw_version': '1.3.0 Build 220830 Rel.74146n(4555)', 'device_alias': 'Tapo_Entrance', 'avatar': 'Entrance', 'longitude': 0, 'latitude': 0, 'has_set_location_info': 0, 'features': '3', 'barcode': '', 'mac': '34-60-F9-1B-EE-7D', 'dev_id': '8021A902119C7D9AAAA253BDAC5715671FF0BCA2', 'oem_id': '3BC367785BFAAAAE7FB40DBE6F5833', 'hw_desc': '00000000000000000000000000000000', 'is_cal': True}}}
お、取得できたできた。
でも面白そうなのは、動体検知のデータなのよね。さっきcloneしたやつで見ると…
237 def getEvents(self, startTime=False, endTime=False):
これか。
from pytapo import Tapo
from datetime import datetime
host = '192.168.50.6'
user = 'admin'
password = 'password'
tapo = Tapo(host, user, password)
print(tapo.getEvents())
どれ、実行。
% python3.10 test_tapo.py
Traceback (most recent call last):
File "/Users/rifujita/Desktop/test_tapo.py", line 10, in <module>
print(tapo.getEvents())
File "/opt/homebrew/lib/python3.10/site-packages/pytapo/__init__.py", line 240, in getEvents
raise Exception("Failed to get correct camera time.")
Exception: Failed to get correct camera time.
なんて???
237 def getEvents(self, startTime=False, endTime=False):
238 timeCorrection = self.getTimeCorrection()
239 if not timeCorrection:
240 raise Exception("Failed to get correct camera time.")
240行か。timeCorrectionがFalseで返ってきてるんかな?
あ、すぐ上にある、getTimeCorrection()ね。(ここで、1分ほど眺めて考える)。
221 def getTimeCorrection(self):
222 if self.timeCorrection is False:
223 currentTime = self.getTime()
224
225 timeReturned = (
226 "system" in currentTime
227 and "clock_status" in currentTime["system"]
228 and "seconds_from_1970" in currentTime["system"]["clock_status"]
229 )
230 if timeReturned:
231 nowTS = int(datetime.timestamp(datetime.now()))
232 self.timeCorrection = (
233 nowTS - currentTime["system"]["clock_status"]["seconds_from_1970"]
234 )
235 return self.timeCorrection
お前、これ、nowTSとself.getTime()の結果に差が無かったら、0(ゼロ)をreturnするやん。not 0はTrueじゃん。239行、こうじゃね。
239 if timeCorrection is False:
/opt/homebrew/lib/python3.10/site-packages/pytapo/__init__pyを直して、再度実行。
% python3.10 test_tapo.py
[{'start_time': 1686908513, 'end_time': 1686908635, 'alarm_type': 2, 'startRelative': 461, 'endRelative': 339}, {'start_time': 1686908635, 'end_time': 1686908756, 'alarm_type': 6, 'startRelative': 339, 'endRelative': 218}, {'start_time': 1686908822, 'end_time': 1686908943, 'alarm_type': 6, 'startRelative': 152, 'endRelative': 31}, {'start_time': 1686908943, 'end_time': 1686908972, 'alarm_type': 6, 'startRelative': 31, 'endRelative': 2}]
いえーい(^^)v なんかイベントっぽいのが取得できた。PRしとこ。
これは、半分仕事、半分趣味、の話。
プログラムを書くことが仕事ではなく、こうやって得られるJSONのデータをCosmosに入れて、じゃあどうやって利用するか、みたいなことをお客さんと考えるのが「仕事」です。
追記:SSLのエラーがPython 3.10と3.11で出る・出ないは、おそらくrequestsのバージョン違いに起因している。3.10には2.25.1が、3.11には2.31.0がインストールされていた。
追記2:これを書いている時点で、pytapo-3.1.13には2つ問題がある。1つは上述した通り、__init__.pyの239行。もう1つは、253・257行。
237 def getEvents(self, startTime=False, endTime=False):
238 timeCorrection = self.getTimeCorrection()
239 if timeCorrection is False:
240 raise Exception("Failed to get correct camera time.")
241
242 nowTS = int(datetime.timestamp(datetime.now()))
243 if startTime is False:
244 startTime = nowTS + (-1 * timeCorrection) - (10 * 60)
245 if endTime is False:
246 endTime = nowTS + (-1 * timeCorrection) + 60
247
248 responseData = self.executeFunction(
249 "searchDetectionList",
250 {
251 "playback": {
252 "search_detection_list": {
253 "start_index": 0,
254 "channel": 0,
255 "start_time": startTime,
256 "end_time": endTime,
257 "end_index": 99,
getEvents()は2つの引数を取り、startTimeとendTimeの間の動体検知イベントを返すが、start_indexが0、end_indexが99となっているため、最大で100件しかイベントを返さない。では、startTimeをずらして、101件目以降を取得出来るかというと、それも出来ない。つまり、TapoのAPIそのものは、start_time / end_time / start_index / end_indexの4つのパラメータを変更しないとならないはずだが、getEvents()はその仕様に対応していない。とりあえずの修正として、257行のend_indexを999にすれば1日分のイベントは取得出来る。
というか、このTapoのAPIの仕様がおかしい気もする。start_time / end_timeだけで、その時間内のイベントを全て返すべきだろう。おそらく公式アプリが、時間ウインドウをずらして操作するようになっているため、時間を計算するのが面倒だから表示済みの最初と最後のイベントのインデックスを増減する実装としてしまったんじゃないかと。iOS / iPad用の公式アプリはmacOSでも動くので、リバースすればある程度は分かりそう(実際、SSL/TLSの自己署名証明書の公開鍵はmacOSにインストールしたバイナリから抽出できる)。