この1時間ほどやっていたこと(半分仕事、半分趣味)

IT

もう週末でだいたい終わってるから、ちょっと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にインストールしたバイナリから抽出できる)。