【pyzm】ZoneMinder APIをPythonでいじる

# 1.かゆいところに手を

https://qiita.com/takasyu260/items/a8dceddfaded9c7f66d2

前回、ZoneMinderを必死こいて導入しましたが
こういうことがやりたくなったわけです。
- 屋内用のカメラ、夜は暗視が効かなくてほとんど映らないから
夜は録画切りたいんだよなあ
- 古い録画データは定期的に消したいよなあ

録画のオンオフ手段としてはスケジュール機能なんてあると良いのですが
あいにくZoneMinderにはそのような機能は実装されていないようです。

また容量がいっぱいになったら古い動画を削除するという機能もZoneMinderのフィルタ機能を使えばできそうですが
調べた限りでは容量検知トリガーしかないようで、"定期的に"削除するには他の方法を探すしかないようです。

ん？ZoneMinderってAPIがあるのか？Pythonでラッパーしたやつもあるのか？
ほう、いいじゃないか―

# 2.pyzmの導入
というわけで今回使っていくのはこちら

https://pyzm.readthedocs.io/en/latest/

単純なAPIラッパーかと思いきや、機械学習とか他にもいろいろできるみたいですね。
モジュール解説等のドキュメントも一通りここに載っています。

早速環境を整えます。
今回は前回ZoneMinderをインストールしたサーバーに環境を整えていきますが、もちろん他の端末でもオーケーです。
```shell-session:ますはpipをインストール
$ sudo apt install python3-pip
```
```shell-session:つぎにpipでインストール
$ pip install pyzm
```
どうでもいいけどpipのインストールシーケンスはカラフルで好きですね

それから、APIを叩く際には実行するユーザーのAPI_EnabledはYESにしておいてください。
これをやらないと403になります。
![hoge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985190/b6306a53-c213-9c15-32ce-b4454d953b16.png)

# 3.サクッとサンプルコードを実行してみる
```py:sample.py
import pyzm.api as zmapi

api_options = {
        'apiurl': 'http://localhost/zm/api',  # zoneminderのアドレス /zm/apiまで書く
        'portalurl': 'http://localhost/zm',   # zoneminderのアドレス /zmまで書く
        'user': 'admin',                      # ログインユーザー名
        'password': 'admin',                  # ログインパスワード
        'logger': None,                       # とりあえず書いとく
}

zmapi = zmapi.ZMApi(options=api_options)

result = zmapi.monitors()

for e in result.list():
        buf = 'name:' + e.name() + ' func:' + e.function()
        print (buf)
```
登録されているカメラの名前とその動作モードを表示するコードです。
api_optionsのところのパラメータは書き換えてください。

ユーザー認証を有効化していない場合の挙動は確かめてないです。
初期ユーザーはidがadmin、pwもadminなのでそれで通るかと。

こんな感じでカメラの名前と状態が返ってきたら成功です。上4つがRecord、下3つがMonitorになっていますね。
![SnapCrab_NoName_2022-11-24_3-46-35_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985190/3bf0f7f1-6995-da45-56f6-9a4de6dbb02d.png)

# 4.カメラの状態(function)を変更するコード
カメラ名やパラメータは適当ですが、実際に使っているコードになります。
引数で変更後の状態を指定してあげます。
```py:setMonFunc.py
import sys
import pyzm.api as zmapi

args = sys.argv
if 2 != len(args):
        print ('Argument Exception')
        sys.exit()

api_options = {
        'apiurl': 'http://localhost/zm/api',
        'portalurl': 'http://localhost/zm',
        'user': 'admin',
        'password': 'admin',
        'logger': None,
}

zmapi = zmapi.ZMApi(options=api_options)

result = zmapi.monitors()

mon_CAM_A = result.find(name='CAM_A')
mon_CAM_B = result.find(name='CAM_B')
mon_CAM_C = result.find(name='CAM_C')


param = {
        'function': args[1]
}


mon_CAM_A.set_parameter(param)
mon_CAM_B.set_parameter(param)
mon_CAM_C.set_parameter(param)
```
実行するときはこんな感じで
```shell-session
$ python3 setMonFunc.py Record
```
ちょっと走らせてみます。実際に変わったかどうかはブラウザからアクセスしても確認できます。
(実行結果のスクショは一部隠しています。)
![SnapCrab_19216825544 - zm@hogehoge ~ VT_2022-11-24_3-59-4_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985190/4699f122-83ce-abe9-684e-00978c6eacf0.png)
Monitorクラスのset_parameterメソッドでは、パラメータをいじることでfuncの他にもいろいろ渡すことができます。
```py
{
    'function': string # function of monitor
    'name': string # name of monitor
    'enabled': boolean
    'raw': {
        # Any other monitor value that is not exposed above. Example:
        'Monitor[Colours]': '4',
        'Monitor[Method]': 'simple'
    }

}
```
しばらくの間このコードをcronで起爆させて、屋内カメラに対して朝5:00に録画開始、19:00に録画停止させるようにさせていましたが
夜中に撮ったものも欲しくなったので結局止めちゃいました。
```sh:crontab -l
0 5 * * * /usr/bin/python3 /chomechome/setMonFunc.py Record
0 19 * * * /usr/bin/python3 /chomechome/setMonFunc.py Monitor
```

# 5.x日前以前のイベントを削除する
```py:unTest_eventDelete.py
import pyzm.api as zmapi

api_options = {
        'apiurl': 'http://localhost/zm/api',
        'portalurl': 'http://localhost/zm',
        'user': 'admin',
        'password': 'admin',
        'logger': None,
}

zmapi = zmapi.ZMApi(options=api_options)

e_options = {
        'to': '60 days ago'
}

result = zmapi.events(e_options)

e_options['limit'] = result.count() + 1    # limitオプションを付け足して再取得する

result = zmapi.events(e_options)

e_list = result.list()

for e in e_list:
        try:    # 例外処理必須
                e.delete()     # 動くかな？
        except:
                pass
```
2023/2　録画容量が90%を超え、そろそろ溢れそうな感じだったので削除コードを導入

このコードでは90日前までのイベントを一括削除する動作をします。
途中なぜlimitオプションを追加して再取得しているかといいますと
何も指定していない状態だと最大100個までしかイベントがリストに格納させれてこないからです。
eventsのcountメソッドではきちんとイベント数が記録されて帰ってきますので
これを元にリストの上限を変更して再取得しています。
面倒であれば最初からデカい数字をlimitオプションにつけておくのも良いかと思います。

肝心のdeleteメソッドの部分ですが、例外処理は必須です。
一応、例外処理をしなくても削除自体はされるようですが
APIリクエストの処理の関係でdeleteを叩くとどうしても例外を吐くような仕様になっています(2023/2現在)
ここで吐かれる例外自体は動作には直接影響がないものですので、雑に処理してしまって大丈夫かと
イベントの量によっては結構エグい出力量になるので、適宜標準出力させないようなコードを差し込まれた方がいいかもしれません
```py:標準出力を捨てる例
import os
from contextlib import redirect_stdout

with redirect_stdout(open(os.devnull, 'w')):
    for e in e_list:
        try:
            e.delete()
        except:
            pass
```

こちらは毎日0時に実行させ、実行日を起点に60日以上前の動画を一括で消すようにしました
大体3TB前後で推移するようになりました

# 6.おわりに
APIステキ、Pythonもステキ
pyzm、僕はちょっとしか触る用がなかったですが
他にもいろいろできそうなので、興味のある方はお試しあれ