From 2389533de4c4bdcb95b0a9af507f9070eb5c9640 Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Sun, 7 Dec 2014 17:57:34 +0200 Subject: [PATCH] camera frames now indicate visually when motion is detected --- eventrelay.py | 70 ++++++++++++++++++++++++++++++++++++++++++++ src/config.py | 24 +++++++++++---- src/handlers.py | 27 +++++++++++++++-- src/motionctl.py | 7 ++++- src/remote.py | 13 ++++++-- src/server.py | 2 +- static/css/frame.css | 4 +++ static/css/main.css | 8 +++++ static/js/frame.js | 31 ++++++++++++++++++++ static/js/main.js | 29 +++++++++++++++++- 10 files changed, 201 insertions(+), 14 deletions(-) create mode 100755 eventrelay.py diff --git a/eventrelay.py b/eventrelay.py new file mode 100755 index 0000000..69a7de4 --- /dev/null +++ b/eventrelay.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +# Copyright (c) 2013 Calin Crisan +# This file is part of motionEye. +# +# motionEye is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import base64 +import logging +import os.path +import sys +import urllib2 + +sys.path.append(os.path.join(os.path.dirname(sys.argv[0]),'src')) + +import config +import settings + +from motioneye import _configure_settings, _configure_logging + + +_configure_settings() +_configure_logging() + + +def print_usage(): + print 'Usage: eventrelay.py ' + + +if __name__ == '__main__': + if len(sys.argv) < 3: + print_usage() + sys.exit(-1) + + event = sys.argv[1] + camera_id = sys.argv[2] + + logging.debug('event = %s' % event) + logging.debug('camera_id = %s' % camera_id) + + url = 'http://127.0.0.1:%(port)s/config/%(camera_id)s/_relay_event/?event=%(event)s' % { + 'port': settings.PORT, + 'camera_id': camera_id, + 'event': event} + + main_config = config.get_main() + + username = main_config.get('@admin_username', '') + password = main_config.get('@admin_password', '') + + request = urllib2.Request(url, '') + request.add_header('Authorization', 'Basic %s' % base64.encodestring('%s:%s' % (username, password)).replace('\n', '')) + + try: + urllib2.urlopen(request, timeout=settings.REMOTE_REQUEST_TIMEOUT) + logging.debug('event successfully relayed') + + except Exception as e: + logging.error('failed to relay event: %s' % e) diff --git a/src/config.py b/src/config.py index 7dfc6ee..49659e2 100644 --- a/src/config.py +++ b/src/config.py @@ -566,7 +566,8 @@ def camera_ui_to_dict(ui): '@working_schedule': '', # events - 'on_event_start': '' + 'on_event_start': '', + 'on_event_end': '' } if ui['proto'] == 'v4l2': @@ -713,8 +714,11 @@ def camera_ui_to_dict(ui): data['@working_schedule_type'] = ui['working_schedule_type'] - # event start notifications - on_event_start = [] + # event start + event_relay_path = os.path.join(settings.PROJECT_PATH, 'eventrelay.py') + event_relay_path = os.path.abspath(event_relay_path) + + on_event_start = ['%(script)s start %%t' % {'script': event_relay_path}] if ui['email_notifications_enabled']: send_mail_path = os.path.join(settings.PROJECT_PATH, 'sendmail.py') send_mail_path = os.path.abspath(send_mail_path) @@ -743,8 +747,12 @@ def camera_ui_to_dict(ui): commands = ui['command_notifications_exec'].split(';') on_event_start += [c.strip() for c in commands] - if on_event_start: - data['on_event_start'] = '; '.join(on_event_start) + data['on_event_start'] = '; '.join(on_event_start) + + # event end + on_event_end = ['%(script)s stop %%t' % {'script': event_relay_path}] + + data['on_event_end'] = '; '.join(on_event_end) return data @@ -1009,7 +1017,7 @@ def camera_dict_to_ui(data): ui['sunday_from'], ui['sunday_to'] = days[6].split('-') ui['working_schedule_type'] = data['@working_schedule_type'] - # event start notifications + # event start on_event_start = data.get('on_event_start') or [] if on_event_start: on_event_start = [e.strip() for e in on_event_start.split(';')] @@ -1037,6 +1045,9 @@ def camera_dict_to_ui(data): ui['web_hook_notifications_enabled'] = True ui['web_hook_notifications_http_method'] = e[1] ui['web_hook_notifications_url'] = e[2] + + elif e.count('eventrelay.py'): + continue # ignore internal relay script else: # custom command command_notifications.append(e) @@ -1345,6 +1356,7 @@ def _set_default_motion_camera(camera_id, data, old_motion=False): data.setdefault('@working_schedule_type', 'outside') data.setdefault('on_event_start', '') + data.setdefault('on_event_end', '') def _get_wifi_settings(data): diff --git a/src/handlers.py b/src/handlers.py index e8833c0..4b1d2e9 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -197,6 +197,9 @@ class ConfigHandler(BaseHandler): elif op == 'rem': self.rem_camera(camera_id) + elif op == '_relay_event': + self._relay_event(camera_id) + else: raise HTTPError(400, 'unknown operation') @@ -622,6 +625,22 @@ class ConfigHandler(BaseHandler): self.finish_json() + @BaseHandler.auth(admin=True) + def _relay_event(self, camera_id): + event = self.get_argument('event') + logging.debug('event %(event)s relayed for camera %(id)s' % {'event': event, 'id': camera_id}) + + if event == 'start': + motionctl._motion_detected[camera_id] = True + + elif event == 'stop': + motionctl._motion_detected[camera_id] = False + + else: + logging.warn('unknown event %s' % event) + + self.finish_json() + class PictureHandler(BaseHandler): @asynchronous @@ -680,10 +699,10 @@ class PictureHandler(BaseHandler): height = self.get_argument('height', None) picture = sequence and mediafiles.get_picture_cache(camera_id, sequence, width) or None - + if picture is not None: return self.try_finish(picture) - + camera_config = config.get_camera(camera_id) if utils.local_camera(camera_config): picture = mediafiles.get_current_picture(camera_config, @@ -693,13 +712,15 @@ class PictureHandler(BaseHandler): if sequence and picture: mediafiles.set_picture_cache(camera_id, sequence, width, picture) + self.set_cookie('motion_detected_' + str(camera_id), str(motionctl.is_motion_detected(camera_id)).lower()) self.try_finish(picture) else: # remote camera - def on_response(picture=None, error=None): + def on_response(motion_detected=False, picture=None, error=None): if sequence and picture: mediafiles.set_picture_cache(camera_id, sequence, width, picture) + self.set_cookie('motion_detected_' + str(camera_id), str(motion_detected).lower()) self.try_finish(picture) remote.get_current_picture(camera_config, on_response, width=width, height=height) diff --git a/src/motionctl.py b/src/motionctl.py index 8fa4f32..506b5f9 100644 --- a/src/motionctl.py +++ b/src/motionctl.py @@ -34,6 +34,7 @@ import utils _started = False _motion_binary_cache = None +_motion_detected = {} def find_motion(): @@ -162,7 +163,7 @@ def stop(): except OSError as e: if e.errno not in (errno.ESRCH, errno.ECHILD): raise - + def running(): pid = _get_pid() @@ -247,6 +248,10 @@ def set_motion_detection(camera_id, enabled): http_client.fetch(request, on_response) +def is_motion_detected(camera_id): + return _motion_detected.get(camera_id, False) + + def _get_thread_id(camera_id): # find the corresponding thread_id # (which can be different from camera_id) diff --git a/src/remote.py b/src/remote.py index 8988bcb..45f5167 100644 --- a/src/remote.py +++ b/src/remote.py @@ -237,6 +237,15 @@ def get_current_picture(local_config, callback, width, height): request = _make_request(host, port, username, password, uri + '/picture/%(id)s/current/' % {'id': camera_id}, query=query) def on_response(response): + motion_detected = False + + cookies = response.headers.get('Set-Cookie') + if cookies: + cookies = cookies.split(';') + cookies = [[i.strip() for i in c.split('=')] for c in cookies] + cookies = dict([c for c in cookies if len(c) == 2]) + motion_detected = cookies.get('motion_detected_' + camera_id) == 'true' + if response.error: logging.error('failed to get current picture for remote camera %(id)s on %(url)s: %(msg)s' % { 'id': camera_id, @@ -244,8 +253,8 @@ def get_current_picture(local_config, callback, width, height): 'msg': unicode(response.error)}) return callback(error=unicode(response.error)) - - callback(response.body) + + callback(motion_detected, response.body) http_client = AsyncHTTPClient() http_client.fetch(request, on_response) diff --git a/src/server.py b/src/server.py index 5a00a36..de82013 100644 --- a/src/server.py +++ b/src/server.py @@ -42,7 +42,7 @@ application = Application( [ (r'^/$', handlers.MainHandler), (r'^/config/main/(?Pset|get)/?$', handlers.ConfigHandler), - (r'^/config/(?P\d+)/(?Pget|set|rem|set_preview)/?$', handlers.ConfigHandler), + (r'^/config/(?P\d+)/(?Pget|set|rem|set_preview|_relay_event)/?$', handlers.ConfigHandler), (r'^/config/(?Padd|list|list_devices)/?$', handlers.ConfigHandler), (r'^/picture/(?P\d+)/(?Pcurrent|list|frame)/?$', handlers.PictureHandler), (r'^/picture/(?P\d+)/(?Pdownload|preview|delete)/(?P.+?)/?$', handlers.PictureHandler), diff --git a/static/css/frame.css b/static/css/frame.css index 18b04da..db630c6 100644 --- a/static/css/frame.css +++ b/static/css/frame.css @@ -47,6 +47,10 @@ div.camera-frame { vertical-align: top; } +div.camera-frame.motion-detected { + background-color: #712727; +} + div.camera-container { height: 100%; text-align: center; diff --git a/static/css/main.css b/static/css/main.css index 27b59a2..60e4376 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -728,6 +728,10 @@ div.camera-frame-place-holder { visibility: hidden; } +div.camera-frame.motion-detected { + background-color: #712727; +} + div.modal-container div.camera-frame { width: auto; padding: 0px; @@ -739,6 +743,10 @@ div.camera-frame:HOVER { background-color: #414141; } +div.camera-frame.motion-detected:HOVER { + background-color: #8B3636; +} + div.camera-top-bar { padding: 3px 0px; font-size: 20px; diff --git a/static/js/frame.js b/static/js/frame.js index 89e2c67..ce07f24 100644 --- a/static/js/frame.js +++ b/static/js/frame.js @@ -3,6 +3,28 @@ var refreshDisabled = false; var inProgress = false; var refreshInterval = 50; /* milliseconds */ + + /* utils */ + +function getCookie(name) { + if (document.cookie.length <= 0) { + return null; + } + + var start = document.cookie.indexOf(name + '='); + if (start == -1) { + return null; + } + + var start = start + name.length + 1; + var end = document.cookie.indexOf(';', start); + if (end == -1) { + end = document.cookie.length; + } + + return unescape(document.cookie.substring(start, end)); +} + /* progress */ @@ -36,6 +58,7 @@ function setupCameraFrame() { var cameraPlaceholder = cameraFrameDiv.find('div.camera-placeholder'); var cameraProgress = cameraFrameDiv.find('div.camera-progress'); var cameraImg = cameraFrameDiv.find('img.camera'); + var cameraId = cameraFrameDiv.attr('id').substring(6); var progressImg = cameraFrameDiv.find('img.camera-progress'); var body = $('body'); @@ -58,6 +81,7 @@ function setupCameraFrame() { cameraImg.addClass('error').removeClass('loading'); cameraPlaceholder.css('opacity', 1); cameraProgress.removeClass('visible'); + cameraFrameDiv.removeClass('motion-detected'); }); cameraImg.load(function () { if (refreshDisabled) { @@ -71,6 +95,13 @@ function setupCameraFrame() { cameraPlaceholder.css('opacity', 0); cameraProgress.removeClass('visible'); + if (getCookie('motion_detected_' + cameraId) == 'true') { + cameraFrameDiv.addClass('motion-detected'); + } + else { + cameraFrameDiv.removeClass('motion-detected'); + } + if (this.naturalWidth / this.naturalHeight > body.width() / body.height()) { cameraImg.css('width', '100%'); cameraImg.css('height', 'auto'); diff --git a/static/js/main.js b/static/js/main.js index 793d7c5..632f190 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -194,8 +194,27 @@ function makeDeviceUrl(dict) { } } +function getCookie(name) { + if (document.cookie.length <= 0) { + return null; + } - /* UI initialization */ + var start = document.cookie.indexOf(name + '='); + if (start == -1) { + return null; + } + + var start = start + name.length + 1; + var end = document.cookie.indexOf(';', start); + if (end == -1) { + end = document.cookie.length; + } + + return unescape(document.cookie.substring(start, end)); +} + + +/* UI initialization */ function initUI() { /* checkboxes */ @@ -2392,6 +2411,7 @@ function addCameraFrameUi(cameraConfig) { cameraImg.height(Math.round(cameraImg.width() * 0.75)); cameraPlaceholder.css('opacity', 1); cameraProgress.removeClass('visible'); + cameraFrameDiv.removeClass('motion-detected'); }); cameraImg.load(function () { if (refreshDisabled[cameraId]) { @@ -2406,6 +2426,13 @@ function addCameraFrameUi(cameraConfig) { cameraPlaceholder.css('opacity', 0); cameraProgress.removeClass('visible'); + if (getCookie('motion_detected_' + cameraId) == 'true') { + cameraFrameDiv.addClass('motion-detected'); + } + else { + cameraFrameDiv.removeClass('motion-detected'); + } + if (fullScreenCameraId) { /* update the modal dialog position when image is loaded */ updateModalDialogPosition(); -- 2.39.5