2017年9月5日火曜日

ZoneMinderの検知画像と動画をSlackに投稿する

ZoneMinderでは動体検出時等にメール通知する機能があるみたい。
それをなんとか使えないかと思ったけど、面倒くさいと思ってしまった。

すごく邪道な方法で検出時の画像と動画をSlackに投稿する方法を考えた。
それは、zmの吐き出すログをイベントトリガーとするもの。

ついでに検出時に音を鳴らすようにした。

1.インストール
  sudo apt-get install ffmpeg

2.ZoneMinderの画像保存先を漁るスクリプト
  /usr/local/bin/Victrola4ZM.sh
  sudo chown syslog Victrola4ZM.sh
  これは、NanoPIの/etc/rsyslog.conf内の"$FileOwner syslog"のため
#!/bin/bash
#Victrola For ZoneMinder (Victrola4ZM)
logger -p local1.info "[V4ZM] Victrola For ZoneMinder (Victrola4ZM) $1"
#PSOT方法設定(SELF or AMIS_SERVER)
POST_MODE=SELF
POST_CMD="/usr/local/Gillie/Post2slack.js file"
POST_CHANNEL=********
#作業ディレクトリ設定
ZMPATH=/usr/share/zoneminder/www/events/Monitor-2
TMPPATH=/tmp
TRIGFLG=victrola.trigflg
#通知用LED設定 NOTFI_BLINK_CNT*NOTFI_BLINK_SEC*2が通知時間
NOTFI_PORTwPI=5
NOTFI_BLINK_CNT=10
NOTFI_BLINK_SEC=0.1
#通知用サウンド設定
NOTFI_SOUND=/usr/local/etc/Victrola/blink1.mp3
#動画作成用環境設定
MOVIEFPS=4
TRIGER_HEADER_IMG=/usr/local/etc/Victrola/img/header-img.png
# Slackへプッシュする
# $1:FPATH $2:MdyDT
function pushSlack() {
FPATH=$1
MdyDT=$2
#ファイル名単体取り出し
FNAME="${FPATH##*/}"
FDT_STRING=$(date -d @$MdyDT +"%Y%m%d%H%M%S")
logger -p local1.info "[V4ZM] Upload to Slack ($POST_MODE) $FNAME - $FDT_STRING"
if [ "$POST_MODE" = "SELF" ]; then
echo "[Upload to Slack(Self)] FileName="$FNAME
echo "[Upload to Slack(Self)] DateTime="$FDT_STRING
#channel,filename,filepath,title,text,cleanup
$(nodejs $POST_CMD $POST_CHANNEL $FNAME $FPATH $FNAME $SLACK_MESSAGE 'false')
logger -p local1.info "[V4ZM] nodejs $POST_CMD $POST_CHANNEL $FNAME $FPATH $FNAME $SLACK_MESSAGE 'false'"
fi
}
function blinkLED(){
#検知LED点灯
gpio mode $NOTFI_PORTwPI out
for ((i=0;i<$NOTFI_BLINK_CNT;i++)); do
gpio write $NOTFI_PORTwPI 0
sleep $NOTFI_BLINK_SEC
gpio write $NOTFI_PORTwPI 1
sleep $NOTFI_BLINK_SEC
done
}
function searchDIR(){
# ディレクトリを探す
Cdate=$(date "+%-y/%m/%d")
Cwk=$(ls -altr /usr/share/zoneminder/www/events/Monitor-2/$Cdate/ | grep '^l' | tail -1)
set $Cwk
Cdir=$9
ZJSEARCH=$ZMPATH/$Cdate/$Cdir/*-analyse.jpg
ZJPATH=$(ls -tr $ZJSEARCH | head -1)
MdyDT=$(stat -c %Y $ZJPATH)
}
function prepare_video(){
SECONDS=0
# 検出時の赤画像
convert $ZJPATH -resize 160x120 $WKDIR/${ZJPATH##*/}.mpc
ls -tr $ZMPATH/$Cdate/$Cdir/*-capture.jpg | while read line
do
convert $line -background black -extent 320x360 $WKDIR/${line##*/}.mpc
APATH=${line//-capture/-analyse}
if [ -e $APATH ]; then
convert $APATH -resize 160x120 $WKDIR/${APATH##*/}.mpc
#元画像
#赤枠画像
#解析画像 の順
convert $WKDIR/${line##*/}.mpc \
$WKDIR/${ZJPATH##*/}.mpc -gravity northwest -geometry +0+240 -compose over -composite \
$WKDIR/${APATH##*/}.mpc -gravity northwest -geometry +160+240 -compose over -composite \
$TRIGER_HEADER_IMG -gravity northwest -geometry +0+240 -compose over -composite \
$WKDIR2/${line##*/}
else
convert $WKDIR/${line##*/}.mpc \
$WKDIR/${ZJPATH##*/}.mpc -gravity northwest -geometry +0+240 -compose over -composite \
$TRIGER_HEADER_IMG -gravity northwest -geometry +0+240 -compose over -composite \
$WKDIR2/${line##*/}
fi
done
# 終了待ち
#wait
time=$SECONDS
logger -p local1.info "[V4ZM] SET Processing time=$time"
}
function prepare_video3(){
#1.リサイズ
logger -p local1.info "[V4ZM] Resize and Extent Image=$ZMPATH/$Cdate/$Cdir/*-*.jpg"
#処理時間計測開始
SECONDS=0
ls -tr $ZMPATH/$Cdate/$Cdir/*-capture.jpg | while read line
do
APATH=${line//-capture/-analyse}
if [ -e $APATH ]; then
#アナライズ画像リサイズ
convert $APATH -resize 160x120 $WKDIR/${APATH##*/} &
fi
convert $line -background black -extent 320x360 $WKDIR/${line##*/} &
done
#終了待ち
wait
time=$SECONDS
logger -p local1.info "[V4ZM] Resize and Extent Processing time=$time"
#2.検出画像 ファイル名のみ取得しWKと結合する
SECONDS=0
ZJPATH=$WKDIR/${ZJPATH##*/}
# 検出時の赤画像
convert $ZJPATH -resize 160x120 $WKDIR/${ZJPATH##*/}
#3.静止画作成
ls -tr $WKDIR/*-capture.jpg | while read line
do
#echo $line
#logger -p local1.info "[V4ZM] Synthesis Image=$line"
APATH=${line//-capture/-analyse}
if [ -e $APATH ]; then
convert $line \
$ZJPATH -gravity northwest -geometry +0+240 -compose over -composite \
$APATH -gravity northwest -geometry +160+240 -compose over -composite \
$TRIGER_HEADER_IMG -gravity northwest -geometry +0+240 -compose over -composite \
$WKDIR2/${line##*/} &
else
convert $line \
$ZJPATH -gravity northwest -geometry +0+240 -compose over -composite \
$TRIGER_HEADER_IMG -gravity northwest -geometry +0+240 -compose over -composite \
$WKDIR2/${line##*/} &
fi
done
#終了待ち
wiat
time=$SECONDS
logger -p local1.info "[V4ZM] Synthesis Processing time=$time"
}
#スタートの通知
if [ $1 = "start" ]; then
#flag on
touch $TMPPATH/$TRIGFLG
#サウンド再生
logger -p local1.info "[V4ZM] mpg123 $NOTFI_SOUND"
mpg123 $NOTFI_SOUND&
#検知LED点灯
blinkLED&
#ディレクトリを探す
searchDIR
#検出した静止画(赤線あり)を送信する
SLACK_MESSAGE=":home:ZM検出画像"
logger -p local1.info "[V4ZM] TriggerImage=$ZJPATH"
pushSlack $ZJPATH $MdyDT
exit 0
fi
#検知LED点灯
gpio mode $NOTFI_PORTwPI out
gpio write $NOTFI_PORTwPI 1
# ディレクトリを探す
searchDIR
# 記録された静止画を動画に結合して送信する
#0.作業用ディレクトリ作成
WKDIR=$TMPPATH/$Cdir
WKDIR2=$TMPPATH/$Cdir"2"
mkdir $WKDIR
mkdir $WKDIR2
#
prepare_video3
#4.動画化
SECONDS=0
ZJSEARCH=$WKDIR2/%05d-capture.jpg
ZMNAME=$(date -d @$MdyDT +"%Y-%m-%d_%H-%M-%S")
ZJPATH=$WKDIR2/$ZMNAME.mp4
logger -p local1.info "[V4ZM] Create Video=$ZJPATH"
ffmpeg -r $MOVIEFPS -i $ZJSEARCH -vcodec h264 -y -f mp4 $ZJPATH
time=$SECONDS
logger -p local1.info "[V4ZM] Create Video Processing time=$time"
#5.slackに通知
SLACK_MESSAGE=":home:ZM検出動画"
logger -p local1.info "[V4ZM] Movie=$ZJPATH"
pushSlack $ZJPATH $MdyDT
#6.後片付け
rm -rf $WKDIR
rm -rf $WKDIR2
#flag off
rm -f $TMPPATH/$TRIGFLG
#検知LED消灯
gpio write $NOTFI_PORTwPI 0
view raw Victrola4ZM.sh hosted with ❤ by GitHub
  /usr/local/Gillie/Post2slack.js
  これはSlackAPIのトークンが含まれるためowner(=syslog)以外参照させないchmod 700
 
//Post2slack.js
var request = require('request');
var fs = require('fs');
var slackApiKey = '********';
var slackurl = "https://slack.com/api/chat.postMessage";
var slackurlTest = "https://slack.com/api/auth.test";
var slackurlFile = "https://slack.com/api/files.upload";
//ヘッダーを定義
var headers = {
'Content-Type':'application/x-www-form-urlencoded'
}
console.log(process.argv.length);
if(process.argv.length >= 3){
if(process.argv[2]=="text"){
//テキストの投稿
//channel,emoji,pretext,color,title,text
if(process.argv.length == 9){
channel = process.argv[3];
emoji = process.argv[4];
pretext = process.argv[5];
color = process.argv[6];
title = process.argv[7];
text = process.argv[8];
//attachments
var att = {
'pretext' :emoji+pretext,
'fallback':pretext,
'color' :color,
'title' :title,
'text' :text
}
//オプションを定義
var options = {
url : slackurl,
method : 'POST',
headers: headers,
form: {
'token' :slackApiKey,
'channel' :'#'+channel,
'as_user' :'true',
'attachments':JSON.stringify([att])
}
}
//リクエスト送信
console.log(att);
request(options, function (error, response, body) {
//コールバック
if (!error && response.statusCode == 200) {
console.log(body);
}else{
console.log('error: '+ response.statusCode);
}
});
}else{
console.log("引数が必要:channel,emoji,pretext,color,title,text");
}
}else if(process.argv[2]=="test"){
//auth.test
//オプションを定義
var options = {
url : slackurlTest,
method : 'POST',
headers: headers,
form: {
'token' :slackApiKey
}
}
//リクエスト送信
request(options, function (error, response, body) {
//コールバック
if (!error && response.statusCode == 200) {
console.log(body);
}else{
console.log('error: '+ response.statusCode);
}
});
}else if(process.argv[2]=="file"){
//ファイルのアップロード
//channel,filename,filepath,title,text,cleanup
if(process.argv.length == 9){
channel = process.argv[3];
filename= process.argv[4];
filepath= process.argv[5];
title = process.argv[6];
text = process.argv[7];
cleanup = process.argv[8];
//オプションを定義
var options = {
url : slackurlFile,
method : 'POST',
formData: {
'token' :slackApiKey,
'channels' :'#'+channel,
'filename' :filename,
'file' :fs.createReadStream(filepath),
'title' :title,
'initial_comment' : text
}
}
//リクエスト送信
console.log(options);
request(options, function (error, response, body) {
//コールバック
if (!error && response.statusCode == 200) {
// console.log(body);
var jbody = JSON.parse(body);
console.log(jbody);
if(cleanup.toLowerCase()=="true"){
//ファイルを削除する
fs.unlinkSync(filepath);
}
}else{
console.log('error: '+ response.statusCode);
}
});
}else{
console.log("引数が必要:channel,filename,filepath,title,text,cleanup");
}
}
}else{
console.log("err:引数が不足している");
}
view raw Post2slack.js hosted with ❤ by GitHub
3.ZoneMinderのログを漁るスクリプト
  /etc/rsyslog.d/Victrola4ZM.conf
  sudo systemctl restart rsyslog
if ( $msg contains 'Monitor-2' ) and ( $msg contains 'alarm start' ) then action(type="omprog" binary="/usr/local/bin/Victrola4ZM.sh start")
if ( $msg contains 'Monitor-2' ) and ( $msg contains 'alarm end' ) then action(type="omprog" binary="/usr/local/bin/Victrola4ZM.sh end")
4.音を鳴らすために
  usermod -aG audio syslog

5.実行結果