From 0dd3b6835d72ce7a9db6cb5a435b8cbd11afb729 Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Tue, 1 Dec 2015 17:49:07 +0200 Subject: [PATCH] added support for execution custom commands on action buttons --- motioneye/config.py | 15 +++++++ motioneye/handlers.py | 58 +++++++++++++++++++++++++++ motioneye/server.py | 1 + motioneye/static/css/main.css | 6 ++- motioneye/static/js/main.js | 74 +++++++++++++++++++++++++++++++---- 5 files changed, 146 insertions(+), 8 deletions(-) diff --git a/motioneye/config.py b/motioneye/config.py index de42747..7b50fcc 100644 --- a/motioneye/config.py +++ b/motioneye/config.py @@ -38,6 +38,7 @@ import v4l2ctl _CAMERA_CONFIG_FILE_NAME = 'thread-%(id)s.conf' _MAIN_CONFIG_FILE_NAME = 'motion.conf' +_ACTIONS = ['lock', 'unlock', 'light_on', 'light_off', 'alarm_on', 'alarm_off'] _main_config_cache = None _camera_config_cache = {} @@ -1319,6 +1320,10 @@ def motion_camera_dict_to_ui(data): extra_options.append((name, value)) ui['extra_options'] = extra_options + + # action commands + action_commands = get_action_commands(data['@id']) + ui['actions'] = action_commands.keys() return ui @@ -1363,6 +1368,16 @@ def simple_mjpeg_camera_dict_to_ui(data): return ui +def get_action_commands(camera_id): + action_commands = {} + for action in _ACTIONS: + path = os.path.join(settings.CONF_PATH, '%s_%s' % (action, camera_id)) + if os.access(path, os.X_OK): + action_commands[action] = path + + return action_commands + + def backup(): logging.debug('generating config backup file') diff --git a/motioneye/handlers.py b/motioneye/handlers.py index 2ea300e..28dcef2 100644 --- a/motioneye/handlers.py +++ b/motioneye/handlers.py @@ -1437,6 +1437,64 @@ class MovieHandler(BaseHandler): raise HTTPError(400, 'unknown operation') +class ActionHandler(BaseHandler): + @asynchronous + def post(self, camera_id, action): + camera_id = int(camera_id) + if camera_id not in config.get_camera_ids(): + raise HTTPError(404, 'no such camera') + + if action == 'snapshot': + logging.debug('executing snapshot action for camera with id %s' % camera_id) + return self.snapshot() + + elif action == 'record_start': + logging.debug('executing record_start action for camera with id %s' % camera_id) + return self.record_start() + + elif action == 'record_stop': + logging.debug('executing record_stop action for camera with id %s' % camera_id) + return self.record_stop() + + action_commands = config.get_action_commands(camera_id) + command = action_commands.get(action) + if not command: + raise HTTPError(400, 'unknown action') + + logging.debug('executing %s action for camera with id %s: "%s"' % (action, camera_id, command)) + self.run_command_bg(command) + + def run_command_bg(self, command): + self.p = subprocess.Popen(command, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) + self.command = command + + self.io_loop = IOLoop.instance() + self.io_loop.add_timeout(datetime.timedelta(milliseconds=100), self.check_command) + + def check_command(self): + exit_status = self.p.poll() + if exit_status is not None: + output = self.p.stdout.read() + lines = output.split('\n') + if not lines[-1]: + lines = lines[:-1] + command = os.path.basename(self.command) + if exit_status: + logging.warn('%s: command has finished with non-zero exit status: %s' % (command, exit_status)) + for line in lines: + logging.warn('%s: %s' % (command, line)) + + else: + logging.debug('%s: command has finished' % command) + for line in lines: + logging.debug('%s: %s' % (command, line)) + + self.finish_json({'status': exit_status}) + + else: + self.io_loop.add_timeout(datetime.timedelta(milliseconds=100), self.check_command) + + class PrefsHandler(BaseHandler): def get(self, key): self.finish_json(self.get_pref(key)) diff --git a/motioneye/server.py b/motioneye/server.py index 0c8af20..a9d006d 100644 --- a/motioneye/server.py +++ b/motioneye/server.py @@ -176,6 +176,7 @@ handler_mapping = [ (r'^/movie/(?P\d+)/(?Plist)/?$', handlers.MovieHandler), (r'^/movie/(?P\d+)/(?Pdownload|preview|delete)/(?P.+?)/?$', handlers.MovieHandler), (r'^/movie/(?P\d+)/(?Pdelete_all)/(?P.*?)/?$', handlers.MovieHandler), + (r'^/action/(?P\d+)/(?P\w+)/?$', handlers.ActionHandler), (r'^/prefs/(?P\w+)/?$', handlers.PrefsHandler), (r'^/_relay_event/?$', handlers.RelayEventHandler), (r'^/log/(?P\w+)/?$', handlers.LogHandler), diff --git a/motioneye/static/css/main.css b/motioneye/static/css/main.css index 9ac4edf..23ec58d 100644 --- a/motioneye/static/css/main.css +++ b/motioneye/static/css/main.css @@ -902,7 +902,7 @@ div.camera-overlay-bottom { div.camera-frame:HOVER div.camera-overlay-top, div.camera-frame:HOVER div.camera-overlay-bottom { - background-color: rgba(65, 65, 65, 0.8); + background-color: rgba(40, 40, 40, 0.8); } div.camera-frame.motion-detected div.camera-overlay-top, @@ -1012,6 +1012,10 @@ div.camera-action-button { vertical-align: top; } +div.camera-action-button.pending { + opacity: 0.5 !important; +} + div.camera-action-button.lock { background-position: 0px 0px; } diff --git a/motioneye/static/js/main.js b/motioneye/static/js/main.js index 1ca9238..13c0a3a 100644 --- a/motioneye/static/js/main.js +++ b/motioneye/static/js/main.js @@ -2421,6 +2421,18 @@ function doDeleteAllFiles(mediaType, cameraId, groupKey, callback) { }, {stack: true}); } +function doAction(cameraId, action, callback) { + ajax('POST', basePath + 'action/' + cameraId + '/' + action + '/', null, function (data) { + if (data == null || data.error) { + showErrorMessage(data && data.error); + } + + if (callback) { + callback(); + } + }); +} + /* fetch & push */ @@ -3723,7 +3735,7 @@ function addCameraFrameUi(cameraConfig) { var alarmOnButton = cameraFrameDiv.find('div.camera-action-button.alarm-on'); var alarmOffButton = cameraFrameDiv.find('div.camera-action-button.alarm-off'); var snapshotButton = cameraFrameDiv.find('div.camera-action-button.snapshot'); - var recordButton = cameraFrameDiv.find('div.camera-action-button.record'); + var recordButton = cameraFrameDiv.find('div.camera-action-button.record-start'); var cameraOverlay = cameraFrameDiv.find('div.camera-overlay'); var cameraPlaceholder = cameraFrameDiv.find('div.camera-placeholder'); @@ -3787,7 +3799,7 @@ function addCameraFrameUi(cameraConfig) { /* fade in */ cameraFrameDiv.animate({'opacity': 1}, 100); - /* add the top button handlers */ + /* add the top buttons handlers */ configureButton.click(function () { doConfigureCamera(cameraId); }); @@ -3811,12 +3823,53 @@ function addCameraFrameUi(cameraConfig) { }; }(cameraId)); - /* add the action button handlers */ -// if (cameraConfig.at-most-4-buttons) { TODO -// cameraOverlay.find('div.camera-overlay-bottom').addClass('few-buttons'); -// } - //TODO add handlers + /* action buttons */ + + cameraFrameDiv.find('div.camera-action-button').css('display', 'none'); + var actionButtonDict = { + 'lock': lockButton, + 'unlock': unlockButton, + 'light_on': lightOnButton, + 'light_off': lightOffButton, + 'alarm_on': alarmOnButton, + 'alarm_off': alarmOffButton, + 'snapshpt': snapshotButton, + 'record': recordButton + }; + cameraConfig.actions.forEach(function (action) { + var button = actionButtonDict[action]; + if (!button) { + return; + } + + button.css('display', ''); + button.click(function () { + if (button.hasClass('pending')) { + return; + } + + button.addClass('pending'); + + if (action == 'record') { + if (button.hasClass('record-start')) { + action = 'record_start'; + } + else { + action = 'record_stop'; + } + } + + doAction(cameraId, action, function () { + button.removeClass('pending'); + }); + }) + }); + + if (cameraConfig.actions.length <= 4) { + cameraOverlay.find('div.camera-overlay-bottom').addClass('few-buttons'); + } + var FPS_LEN = 4; cameraImg[0].fpsTimes = []; @@ -3862,6 +3915,13 @@ function addCameraFrameUi(cameraConfig) { else { cameraFrameDiv.removeClass('motion-detected'); } + + if (getCookie('record_active_' + cameraId) == 'true') { + recordButton.removeClass('record-start').addClass('record-stop'); + } + else { + recordButton.removeClass('record-stop').addClass('record-start'); + } this.lastCookieTime = now; -- 2.39.5