Skip to content

Instantly share code, notes, and snippets.

@peterhoeg
Created August 10, 2020 06:47
Show Gist options
  • Save peterhoeg/01fe3ccb54cea4d9a437b9e8ffb3c55c to your computer and use it in GitHub Desktop.
Save peterhoeg/01fe3ccb54cea4d9a437b9e8ffb3c55c to your computer and use it in GitHub Desktop.
MotionEye
{ 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";
};
}
{ 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