Created
August 10, 2020 06:47
-
-
Save peterhoeg/01fe3ccb54cea4d9a437b9e8ffb3c55c to your computer and use it in GitHub Desktop.
MotionEye
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ curl | |
, ffmpeg | |
, lib | |
, lsb-release | |
, makeWrapper | |
, motion | |
, python2Packages | |
, v4l_utils | |
, which | |
}: | |
let | |
pypkgs = python2Packages; | |
in | |
pypkgs.buildPythonApplication rec { | |
pname = "motioneye"; | |
version = "0.42"; | |
src = pypkgs.fetchPypi { | |
inherit pname version; | |
sha256 = "1m4awnd9q9gq27f1i8sd7sb38nvryyxakh2d5zg7s3fa410w3j82"; | |
}; | |
buildInputs = [ ffmpeg lsb-release motion v4l_utils which ]; | |
postPatch = '' | |
substituteInPlace motioneye/scripts/relayevent.sh \ | |
--replace curl ${curl}/bin/curl | |
''; | |
postInstall = '' | |
mv $out/${pypkgs.python.sitePackages}/motioneye/scripts/*.sh $out/bin | |
rmdir $out/${pypkgs.python.sitePackages}/motioneye/scripts | |
wrapProgram $out/bin/meyectl \ | |
--prefix PATH : ${lib.makeBinPath buildInputs} | |
''; | |
nativeBuildInputs = [ makeWrapper ]; | |
propagatedBuildInputs = with pypkgs; [ | |
jinja2 | |
pillow | |
pycurl | |
pytz | |
tornado | |
]; | |
doCheck = false; | |
meta = { | |
description = "MotionEye"; | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ config, lib, pkgs, fetchPypi, ... }: | |
let | |
py = pkgs.python2; | |
pypkgs = py.pkgs; | |
declarative = true; | |
pkg = pkgs.callPackage ../../pkgs/motioneye {}; | |
mqtt = pkgs.writeShellScript "mqtt" '' | |
set -eEuo pipefail | |
PATH=$PATH:${lib.makeBinPath (with pkgs; [ mosquitto utillinux ])} | |
DEBUG=''${DEBUG:-0} | |
if [ $DEBUG -eq 2 ]; then | |
set -x | |
fi | |
MQTT_HOST=''${MQTT_HOST:-"10.1.1.1"} | |
MQTT_PORT=''${MQTT_PORT:-"1883"} | |
MQTT_USER=''${MQTT_USER:-"$(whoami)"} | |
MQTT_PASSWORD=''${MQTT_PASSWORD} | |
TOPIC_PREFIX=''${TOPIC_PREFIX:-"homie/"} | |
INTERVAL=60 | |
MOTIONEYE=1 | |
usage() { | |
cat <<_EOF | |
Usage: $0 -c camera_id [ -f file_name ] [ -s state ] [ -v version ] <command> | |
-c : camera id | |
-f : the file path with the video/picture | |
-s : the state for a state update | |
-v : MottionEye version | |
command : one of start, stop, update | |
_EOF | |
} | |
exit_abnormal() { | |
usage | |
exit 1 | |
} | |
# by default there is no detection | |
state=OFF | |
while getopts "c:f:s:v:" opt; do | |
case $opt in | |
c) | |
camera=cam-$OPTARG | |
;; | |
f) | |
file_name=$OPTARG | |
;; | |
s) | |
state=$OPTARG | |
;; | |
v) | |
version=$OPTARG | |
;; | |
*) | |
usage | |
exit_abnormal | |
;; | |
esac | |
done | |
shift $((OPTIND - 1)) | |
command=$1 | |
BASH_ARGV0=mqtt-$camera | |
log() { | |
local cam=''${camera:-"unknown"} | |
local stat=''${state:-"unknown"} | |
local cmd=''${command:-"unknown"} | |
if [ $DEBUG -gt 0 ]; then | |
echo "command: $cmd, $camera: $cam, state: $stat ''${1:-""}" | |
fi | |
} | |
log "prestart" | |
pub() { | |
local topic=$1 | |
local state=$2 | |
mosquitto_pub \ | |
-r \ | |
-h $MQTT_HOST \ | |
-p $MQTT_PORT \ | |
-u $MQTT_USER \ | |
-P $MQTT_PASSWORD \ | |
-q 1 \ | |
-t $TOPIC_PREFIX$camera/$topic -m "$state" | |
} | |
announce() { | |
log "start" | |
pub '$state' 'init' | |
pub '$name' "$camera" | |
pub '$implementation' 'MotionEye' | |
pub '$homie' '4.0' | |
if [ $MOTIONEYE -eq 1 ]; then | |
pub '$nodes' "motion_detection" | |
fi | |
pub 'motion_detection/$name' 'Motion Detection' | |
pub 'motion_detection/$type' 'Camera' | |
pub 'motion_detection/$properties' 'enabled,motion_detected' | |
pub 'motion_detection/enabled/$name' 'Motion Detection Enabled' | |
pub 'motion_detection/enabled/$settable' 'true' | |
pub 'motion_detection/enabled/$datatype' 'boolean' | |
pub 'motion_detection/motion_detected' "$state" | |
pub 'motion_detection/motion_detected/$name' 'Motion Detected' | |
pub 'motion_detection/motion_detected/$settable' 'false' | |
pub 'motion_detection/motion_detected/$datatype' 'boolean' | |
pub '$state' 'ready' | |
} | |
stop() { | |
log "stop" | |
pub '$state' 'disconnected' | |
} | |
alive() { | |
# interval should only be part of the initial announcement but due to a bug in | |
# openHAB 2.4, this needs to be updated | |
# pub '$stats/interval' "$INTERVAL" | |
pub '$state' 'ready' | |
} | |
update() { | |
log "update" | |
pub 'motion_detection/motion_detected' $state | |
} | |
case $command in | |
start) | |
announce | |
while true; do | |
alive | |
sleep $(($INTERVAL / 2 + $((1 + RANDOM % 10)))) | |
done | |
;; | |
stop) | |
stop | |
;; | |
announce) | |
announce | |
update | |
;; | |
update) | |
update | |
;; | |
*) | |
usage | |
exit_abnormal | |
;; | |
esac | |
''; | |
toText = val: | |
if (builtins.isBool val) | |
then lib.boolToString val | |
else toString val; | |
attrsToFile = file: attrs: pkgs.writeText file ( | |
lib.concatStringsSep "\n" ( | |
lib.mapAttrsToList ( | |
k: v: | |
"${k} ${toText v}" | |
) attrs | |
) | |
); | |
baseStreamingPort = 8080; | |
retention = "60d"; | |
camId = id: "cam-${toString id}"; | |
cfgFile = attrsToFile "motioneye.conf" cfg; | |
cfg = rec { | |
# path to the configuration directory (must be writable by motionEye) | |
conf_path = "/var/lib/motioneye"; | |
run_path = "/run/motioneye"; | |
log_path = "/var/log/motioneye"; | |
media_path = "/storage/DATA/motioneye"; | |
# the log level (use quiet, error, warning, info or debug) | |
log_level = "info"; | |
# the IP address to listen on | |
# (0.0.0.0 for all interfaces, 127.0.0.1 for localhost) | |
listen = "0.0.0.0"; | |
# the TCP port to listen on | |
port = 8765; | |
# path to the motion binary to use (automatically detected if commented) | |
motion_binary = "${pkgs.motion}/bin/motion"; | |
# whether motion HTTP control interface listens on | |
# localhost or on all interfaces | |
motion_control_localhost = true; | |
# the TCP port that motion HTTP control interface listens on | |
motion_control_port = 7999; | |
# interval in seconds at which motionEye checks if motion is running | |
motion_check_interval = 10; | |
# whether to restart the motion daemon when an error occurs while communicating with it | |
# this seems to randomly restart the whole motioneye process if one camera is unavailable | |
motion_restart_on_errors = false; | |
# interval in seconds at which motionEye checks the SMB mounts | |
mount_check_interval = 300; | |
# interval in seconds at which the janitor is called | |
# to remove old pictures and movies | |
cleanup_interval = 0; | |
# timeout in seconds to wait for response from a remote motionEye server | |
remote_request_timeout = 10; | |
# timeout in seconds to wait for mjpg data from the motion daemon | |
mjpg_client_timeout = 10; | |
# timeout in seconds after which an idle mjpg client is removed | |
# (set to 0 to disable) | |
mjpg_client_idle_timeout = 10; | |
smb_shares = false; | |
smb_mount_root = "/media"; | |
local_time_file = /etc/localtime; | |
# enables shutdown and rebooting after changing system settings | |
# (such as wifi settings or time zone) | |
enable_reboot = false; | |
# timeout in seconds to use when talking to the SMTP server | |
smtp_timeout = 60; | |
# timeout in seconds to wait for media files list | |
list_media_timeout = 120; | |
# timeout in seconds to wait for media files list, when sending emails | |
list_media_timeout_email = 10; | |
# timeout in seconds to wait for zip file creation | |
zip_timeout = 500; | |
# timeout in seconds to wait for timelapse creation | |
timelapse_timeout = 500; | |
# enable adding and removing cameras from UI | |
add_remove_cameras = true; | |
# enables HTTP basic authentication scheme (in addition to, not instead of the signature mechanism) | |
http_basic_auth = false; | |
# overrides the hostname (useful if motionEye runs behind a reverse proxy) | |
# server_name motionEye | |
}; | |
camera = camera: | |
let | |
id = toString camera.id; | |
boolToStr = bool: if bool then "on" else "off"; | |
in | |
pkgs.writeText "camera-${id}.conf" '' | |
# @clean_cloud_enabled off | |
# @enabled on | |
# @id ${id} | |
# @manual_record off | |
# @manual_snapshots on | |
# @motion_detection on | |
# @network_password | |
# @network_server | |
# @network_share_name | |
# @network_smb_ver 1.0 | |
# @network_username | |
# @preserve_movies 0 | |
# @preserve_pictures 0 | |
# @storage_device custom-path | |
# @upload_enabled off | |
# @upload_location | |
# @upload_method post | |
# @upload_movie on | |
# @upload_password | |
# @upload_picture on | |
# @upload_port | |
# @upload_server | |
# @upload_service ftp | |
# @upload_subfolders on | |
# @upload_username | |
# @webcam_resolution 100 | |
# @webcam_server_resize off | |
# @working_schedule | |
# @working_schedule_type outside | |
auto_brightness off | |
camera_id ${id} | |
camera_name cam-${id} | |
despeckle_filter | |
emulate_motion off | |
event_gap 30 | |
framerate 5 | |
height 720 | |
lightswitch_percent 0 | |
locate_motion_mode off | |
locate_motion_style redbox | |
${lib.optionalString (builtins.hasAttr "mask" camera) | |
"mask_file ${camera.mask}" | |
} | |
minimum_motion_frames 20 | |
movie_codec mkv | |
movie_filename %Y-%m-%d/%H-%M-%S | |
movie_max_time 0 | |
movie_output ${boolToStr camera.saveMovie} | |
movie_output_motion off | |
movie_passthrough off | |
movie_quality 75 | |
${lib.optionalString (! lib.hasPrefix camera.url "rtsp") '' | |
netcam_keepalive on | |
netcam_tolerant_check on | |
''} | |
netcam_url ${camera.url} | |
netcam_use_tcp on | |
noise_level ${toString camera.noise_level} | |
noise_tune on | |
# on_event_end ${pkg}/bin/relayevent.sh ${cfgFile} stop %t | |
# on_event_start ${pkg}/bin/relayevent.sh ${cfgFile} start %t | |
# on_movie_end ${pkg}/bin/relayevent.sh ${cfgFile} movie_end %t %f | |
# on_picture_save ${pkg}/bin/relayevent.sh ${cfgFile} picture_save %t %f | |
# these trigger several times per second | |
# on_area_detected {log} area_detected %t | |
# on_motion_detected {log} motion_detected %t | |
# on_camera_found ${mqtt} -c %t -s OFF update | |
# on_camera_lost ${mqtt} -c %t -s OFF update | |
# on_movie_start ${mqtt} -c %t -f %f movie_start | |
# on_movie_end ${mqtt} -c %t -f %f movie_end | |
# on_picture_save ${mqtt} -c %t -f %f picture_save | |
on_event_start ${mqtt} -c %t -s ON update | |
on_event_end ${mqtt} -c %t -s OFF update | |
picture_filename %Y-%m-%d/%H-%M-%S | |
picture_output best | |
picture_output_motion off | |
picture_quality 85 | |
post_capture 1 | |
pre_capture 1 | |
rotate 0 | |
smart_mask_speed 5 | |
snapshot_filename %Y-%m-%d/%H-%M-%S | |
snapshot_interval 0 | |
stream_auth_method 0 | |
stream_authentication user: | |
stream_localhost off | |
stream_maxrate 5 | |
stream_motion off | |
stream_port ${toString (cameraPort camera.id)} | |
stream_quality 75 | |
target_dir ${cameraDir camera.id} | |
text_changes on | |
text_left %Y-%m-%d %T | |
text_right ${camera.location} | |
text_scale 2 | |
threshold ${toString camera.threshold} | |
width 1280 | |
''; | |
motionConf = pkgs.writeText "motion.conf" '' | |
# @admin_password | |
# @admin_username admin | |
# @enabled on | |
# @normal_password | |
# @normal_username user | |
# @show_advanced on | |
${lib.concatMapStringsSep "\n" ( | |
e: | |
"camera camera-${toString e.id}.conf" | |
) cameras} | |
setup_mode off | |
webcontrol_interface 1 | |
webcontrol_localhost off | |
webcontrol_parms 0 | |
webcontrol_port ${toString cfg.motion_control_port} | |
''; | |
cameraDir = id: | |
"${cfg.media_path}/${cameraDirName id}"; | |
cameraDirName = id: | |
"Camera${toString id}"; | |
cameraPort = id: | |
baseStreamingPort + id; | |
cameras = let | |
domain = "home.hoeg.com"; | |
bulletUrl = addr: | |
"rtsp://admin:admin@${addr}:5544/live0.264"; | |
domeUrl = addr: | |
""; | |
in | |
[ | |
{ | |
id = 1; | |
location = "Nursery"; | |
url = "rtsp://cam-1.${domain}:8554/unicast"; | |
threshold = 6000; | |
noise_level = 32; | |
saveMovie = true; | |
} # mask = ../assets/mask_camera_1.pgm; } | |
{ | |
id = 2; | |
location = "Lounge"; | |
url = "rtsp://cam-2.${domain}:8554/unicast"; | |
threshold = 25000; | |
noise_level = 64; | |
saveMovie = false; | |
} | |
# { | |
# id = 3; | |
# location = "TV"; | |
# url = "mjpeg://maureen.${domain}:8081/0/stream"; | |
# threshold = 3000; | |
# noise_level = 32; | |
# saveMovie = false; | |
# } | |
{ | |
id = 4; | |
location = "Nursery"; | |
# url = bulletUrl "cam-3.${domain}" | |
url = bulletUrl "10.1.100.23"; | |
threshold = 25000; | |
noise_level = 64; | |
saveMovie = false; | |
} | |
{ | |
id = 5; | |
location = "Lounge"; | |
# url = bulletUrl "cam-4.${domain}" | |
url = bulletUrl "10.1.100.24"; | |
threshold = 25000; | |
noise_level = 64; | |
saveMovie = false; | |
} | |
]; | |
cfgDrv = pkgs.stdenv.mkDerivation ( | |
let | |
cfgFile = e: | |
"$dir/camera-${toString e.id}.conf"; | |
in { | |
name = "motioneye-config"; | |
buildCommand = '' | |
dir=$out/etc/motioneye | |
mkdir -p $dir | |
install -Dm644 ${motionConf} $dir/motion.conf | |
${lib.concatMapStringsSep "\n" ( | |
e: | |
"install -Dm644 ${camera e} ${cfgFile e}" | |
) cameras} | |
''; | |
} | |
); | |
in | |
{ | |
networking.firewall = { | |
allowedTCPPorts = [ cfg.port cfg.motion_control_port ]; | |
allowedTCPPortRanges = [ | |
{ from = (cameraPort 1); to = (cameraPort (builtins.length cameras)); } | |
]; | |
}; | |
systemd.services = let | |
commonServiceConfig = { | |
Restart = "on-failure"; | |
ProtectSystem = "strict"; | |
ProtectHome = "tmpfs"; | |
PrivateTmp = true; | |
RemoveIPC = true; | |
NoNewPrivileges = true; | |
RestrictSUIDSGID = lib.mkIf (lib.versionAtLeast pkgs.systemd.version "242") true; | |
TimeoutStopSec = "10s"; | |
}; | |
environment = { | |
DEBUG = "1"; | |
MQTT_USER = "camera"; | |
MQTT_PASSWORD = "favorably_afflicted"; | |
}; | |
in | |
{ | |
motioneye = rec { | |
description = "MotionEye"; | |
wants = [ "mqtt-cameras.target" ]; | |
after = [ "mqtt-cameras.target" "network-online.target" ]; | |
wantedBy = [ "multi-user.target" ]; | |
preStart = let | |
cmd = if declarative | |
then "ln -sf" | |
else "cp --no-preserve=owner,mode"; | |
in | |
'' | |
rm -rf ${cfg.conf_path}/{camera-*,motion,motioneye}.conf | |
rm -rf ${cfg.conf_path}/mask_*.pgm | |
for f in ${cfgDrv}/etc/motioneye/*.conf ; do | |
${cmd} $f ${cfg.conf_path}/ | |
done | |
${cmd} ${cfgFile} ${cfg.conf_path}/motioneye.conf | |
${lib.concatMapStringsSep "\n" ( | |
e: | |
"${cmd} ${e.mask} ${cfg.conf_path}/mask_${toString e.id}.pgm" | |
) (builtins.filter (e: builtins.hasAttr "mask" e) cameras)} | |
''; | |
inherit environment; | |
serviceConfig = commonServiceConfig // { | |
User = "motioneye"; | |
Group = "motioneye"; | |
ExecStart = "${pkg}/bin/meyectl startserver -c ${cfg.conf_path}/motioneye.conf"; | |
ReadWriteDirectories = [ cfg.media_path ]; | |
LogsDirectory = builtins.baseNameOf cfg.log_path; | |
RuntimeDirectory = builtins.baseNameOf cfg.conf_path; | |
StateDirectory = builtins.baseNameOf cfg.conf_path; | |
TasksMax = 32 + (5 * (builtins.length cameras)); | |
}; | |
}; | |
"mqtt-camera@" = { | |
description = "MQTT Camera Motion Sensor - %i"; | |
inherit environment; | |
serviceConfig = commonServiceConfig // { | |
DynamicUser = true; | |
ExecStart = "${mqtt} -c %i -v ${pkg.version} start"; | |
ExecStopPost = "${mqtt} -c %i stop"; | |
Slice = "mqtt-cameras.slice"; | |
}; | |
}; | |
}; | |
systemd.slices.mqtt-cameras = { | |
description = "MQTT Cameras"; | |
sliceConfig.TasksMax = 10 + 3 * (builtins.length cameras); | |
}; | |
systemd.targets.mqtt-cameras = { | |
description = "MQTT Cameras"; | |
wantedBy = [ "multi-user.target" ]; | |
wants = map (e: "mqtt-camera@${toString e.id}.service") cameras; | |
}; | |
systemd.tmpfiles.rules = [ | |
"d ${cfg.media_path} 0755 motioneye motioneye - -" | |
] ++ map ( | |
e: | |
"d ${cameraDir e.id} 0755 motioneye motioneye ${retention} -" | |
) cameras; | |
users = { | |
users.motioneye = { | |
description = "MotionEye"; | |
home = cfg.conf_path; | |
isSystemUser = true; | |
group = "motioneye"; | |
}; | |
groups.motioneye = {}; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment