async def _check_ws() -> None: # schedule the next call io_loop = IOLoop.instance() io_loop.add_timeout(datetime.timedelta(seconds=10), _start_check_ws) if not motionctl.running(): return now = datetime.datetime.now() for camera_id in config.get_camera_ids(): camera_config = config.get_camera(camera_id) if not utils.is_local_motion_camera(camera_config): continue working_schedule = camera_config.get('@working_schedule') motion_detection = camera_config.get('@motion_detection') working_schedule_type = camera_config.get( '@working_schedule_type') or 'outside' if not working_schedule: # working schedule disabled, motion detection left untouched continue if not motion_detection: # motion detection explicitly disabled continue now_during = _during_working_schedule(now, working_schedule) must_be_enabled = ((now_during and working_schedule_type == 'during') or (not now_during and working_schedule_type == 'outside')) motion_detection_resp = await motionctl.get_motion_detection(camera_id) await _switch_motion_detection_status(camera_id, must_be_enabled, working_schedule_type, motion_detection_resp)
async def get(self, camera_id, op, filename=None, group=None): if camera_id is not None: camera_id = int(camera_id) if camera_id not in config.get_camera_ids(): raise HTTPError(404, 'no such camera') if op == 'current': await self.current(camera_id) elif op == 'list': await self.list(camera_id) elif op == 'frame': await self.frame(camera_id) elif op == 'download': await self.download(camera_id, filename) elif op == 'preview': await self.preview(camera_id, filename) elif op == 'zipped': await self.zipped(camera_id, group) elif op == 'timelapse': await self.timelapse(camera_id, group) else: raise HTTPError(400, 'unknown operation')
async def _disable_initial_motion_detection(): from motioneye import config for camera_id in config.get_camera_ids(): camera_config = config.get_camera(camera_id) if not utils.is_local_motion_camera(camera_config): continue if not camera_config['@motion_detection']: logging.debug( 'motion detection disabled by config for camera with id %s' % camera_id) await set_motion_detection(camera_id, False)
async def get(self, camera_id, op, filename=None): if camera_id is not None: camera_id = int(camera_id) if camera_id not in config.get_camera_ids(): raise HTTPError(404, 'no such camera') if op == 'list': await self.list(camera_id) return elif op == 'preview': await self.preview(camera_id, filename) return else: raise HTTPError(400, 'unknown operation')
def make_media_folders(): from motioneye import config config.get_main() # just to have main config already loaded camera_ids = config.get_camera_ids() for camera_id in camera_ids: camera_config = config.get_camera(camera_id) if 'target_dir' in camera_config: if not os.path.exists(camera_config['target_dir']): try: os.makedirs(camera_config['target_dir']) except Exception as e: logging.error( 'failed to create root media folder "%s" for camera with id %s: %s' % (camera_config['target_dir'], camera_id, e))
async def post(self, camera_id, op, filename=None, group=None): if group == '/': # ungrouped group = '' if camera_id is not None: camera_id = int(camera_id) if camera_id not in config.get_camera_ids(): raise HTTPError(404, 'no such camera') if op == 'delete': await self.delete(camera_id, filename) elif op == 'delete_all': await self.delete_all(camera_id, group) else: raise HTTPError(400, 'unknown operation')
async 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') local_config = config.get_camera(camera_id) if utils.is_remote_camera(local_config): resp = await remote.exec_action(local_config, action) if resp.error: msg = 'Failed to execute action on remote camera at %(url)s: %(msg)s.' % { 'url': remote.pretty_camera_url(local_config), 'msg': resp.error } return self.finish_json({'error': msg}) return self.finish_json() if action == 'snapshot': logging.debug('executing snapshot action for camera with id %s' % camera_id) await self.snapshot(camera_id) return elif action == 'record_start': logging.debug( 'executing record_start action for camera with id %s' % camera_id) return self.record_start(camera_id) elif action == 'record_stop': logging.debug( 'executing record_stop action for camera with id %s' % camera_id) return self.record_stop(camera_id) action_commands = config.get_action_commands(local_config) 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 cleanup_media(media_type: str) -> None: logging.debug('cleaning up %(media_type)ss...' % {'media_type': media_type}) if media_type == 'picture': exts = _PICTURE_EXTS else: # media_type == 'movie' exts = _MOVIE_EXTS + ['.thumb'] for camera_id in config.get_camera_ids(): camera_config = config.get_camera(camera_id) if not utils.is_local_motion_camera(camera_config): continue preserve_media = camera_config.get('@preserve_%(media_type)ss' % {'media_type': media_type}, 0) if preserve_media == 0: continue # preserve forever still_images_enabled = bool(camera_config['picture_filename']) or bool(camera_config['snapshot_filename']) movies_enabled = bool(camera_config['movie_output']) if media_type == 'picture' and not still_images_enabled: continue # only cleanup pictures for cameras with still images enabled elif media_type == 'movie' and not movies_enabled: continue # only cleanup movies for cameras with movies enabled preserve_moment = datetime.datetime.now() - datetime.timedelta(days=preserve_media) target_dir = camera_config.get('target_dir') cloud_enabled = camera_config.get('@upload_enabled') clean_cloud_enabled = camera_config.get('@clean_cloud_enabled') cloud_dir = camera_config.get('@upload_location') service_name = camera_config.get('@upload_service') clean_cloud_info = None if cloud_enabled and clean_cloud_enabled and camera_id and service_name and cloud_dir: clean_cloud_info = {'camera_id': camera_id, 'service_name': service_name, 'cloud_dir': cloud_dir} if os.path.exists(target_dir): # create a sentinel file to make sure the target dir is never removed open(os.path.join(target_dir, '.keep'), 'w').close() logging.debug('calling _remove_older_files: %s %s %s' % (cloud_enabled, clean_cloud_enabled, clean_cloud_info)) _remove_older_files(target_dir, preserve_moment, clean_cloud_info, exts=exts)
async def get_config(self, camera_id): if camera_id: logging.debug('getting config for camera %(id)s' % {'id': camera_id}) if camera_id not in config.get_camera_ids(): raise HTTPError(404, 'no such camera') local_config = config.get_camera(camera_id) if utils.is_local_motion_camera(local_config): ui_config = config.motion_camera_dict_to_ui(local_config) return self.finish_json(ui_config) elif utils.is_remote_camera(local_config): resp = await remote.get_config(local_config) if resp.error: msg = 'Failed to get remote camera configuration for %(url)s: %(msg)s.' % { 'url': remote.pretty_camera_url(local_config), 'msg': resp.error } return self.finish_json_with_error(msg) for key, value in list(local_config.items()): resp.remote_ui_config[key.replace('@', '')] = value # replace the real device url with the remote camera path resp.remote_ui_config['device_url'] = remote.pretty_camera_url( local_config) return self.finish_json(resp.remote_ui_config) else: # assuming simple mjpeg camera ui_config = config.simple_mjpeg_camera_dict_to_ui(local_config) return self.finish_json(ui_config) else: logging.debug('getting main config') ui_config = config.main_dict_to_ui(config.get_main()) return self.finish_json(ui_config)
async def get(self, camera_id, filename=None, include_body=True): logging.debug('downloading movie %(filename)s of camera %(id)s' % { 'filename': filename, 'id': camera_id }) self.pretty_filename = os.path.basename(filename) if camera_id is not None: camera_id = int(camera_id) if camera_id not in config.get_camera_ids(): raise HTTPError(404, 'no such camera') camera_config = config.get_camera(camera_id) if utils.is_local_motion_camera(camera_config): filename = mediafiles.get_media_path(camera_config, filename, 'movie') self.pretty_filename = camera_config[ 'camera_name'] + '_' + self.pretty_filename await StaticFileHandler.get(self, filename, include_body=include_body) return elif utils.is_remote_camera(camera_config): # we will cache the movie since it takes a while to fetch from the remote camera # and we may be going to play it back in the browser, which will fetch the video in chunks tmpfile = self.tmpdir + '/' + self.pretty_filename if os.path.isfile(tmpfile): # have a cached copy, update the timestamp so it's not flushed import time mtime = os.stat(tmpfile).st_mtime os.utime(tmpfile, (time.time(), mtime)) await StaticFileHandler.get(self, tmpfile, include_body=include_body) return resp = await remote.get_media_content(camera_config, filename, media_type='movie') if resp.error: return self.finish_json({ 'error': 'Failed to download movie from %(url)s: %(msg)s.' % { 'url': remote.pretty_camera_url(camera_config), 'msg': resp.error } }) # check if the file has been created by another request while we were fetching the movie if not os.path.isfile(tmpfile): tmp = open(tmpfile, 'wb') tmp.write(resp.result) tmp.close() await StaticFileHandler.get(self, tmpfile, include_body=include_body) return else: # assuming simple mjpeg camera raise HTTPError(400, 'unknown operation')
async def list(self): logging.debug('listing cameras') proto = self.get_argument('proto') if proto == 'motioneye': # remote listing return self._handle_list_cameras_response( await remote.list_cameras(self.get_all_arguments())) elif proto == 'netcam': scheme = self.get_argument('scheme', 'http') if scheme in ['http', 'https', 'mjpeg']: resp = await test_mjpeg_url(self.get_all_arguments(), auth_modes=['basic'], allow_jpeg=True) return self._handle_list_cameras_response(resp) elif scheme == 'rtsp': resp = await test_rtsp_url(self.get_all_arguments()) return self._handle_list_cameras_response(resp) elif scheme == 'rtmp': resp = test_rtmp_url(self.get_all_arguments()) return self._handle_list_cameras_response(resp) else: return self.finish_json_with_error( f'protocol {scheme} not supported') elif proto == 'mjpeg': resp = await test_mjpeg_url(self.get_all_arguments(), auth_modes=['basic', 'digest'], allow_jpeg=False) return self._handle_list_cameras_response(resp) elif proto == 'v4l2': configured_devices = set() for camera_id in config.get_camera_ids(): data = config.get_camera(camera_id) if utils.is_v4l2_camera(data): configured_devices.add(data['videodevice']) cameras = [{ 'id': d[1], 'name': d[2] } for d in v4l2ctl.list_devices() if (d[0] not in configured_devices) and ( d[1] not in configured_devices)] return self.finish_json({'cameras': cameras}) elif proto == 'mmal': configured_devices = set() for camera_id in config.get_camera_ids(): data = config.get_camera(camera_id) if utils.is_mmal_camera(data): configured_devices.add(data['mmalcam_name']) cameras = [{ 'id': d[0], 'name': d[1] } for d in mmalctl.list_devices() if (d[0] not in configured_devices)] return self.finish_json({'cameras': cameras}) else: # assuming local motionEye camera listing cameras = [] camera_ids = config.get_camera_ids() if not config.get_main().get('@enabled'): camera_ids = [] length = [len(camera_ids)] for camera_id in camera_ids: local_config = config.get_camera(camera_id) if local_config is None: continue if utils.is_local_motion_camera(local_config): ui_config = config.motion_camera_dict_to_ui(local_config) cameras.append(ui_config) finished = self.check_finished(cameras, length) if finished: return elif utils.is_remote_camera(local_config): if local_config.get('@enabled') or self.get_argument( 'force', None) == 'true': resp = await remote.get_config(local_config) return self._handle_get_config_response( camera_id, local_config, resp, cameras, length) else: # don't try to reach the remote of the camera is disabled return self._handle_get_config_response( camera_id, local_config, utils.GetConfigResponse(None, error=True), cameras, length) else: # assuming simple mjpeg camera ui_config = config.simple_mjpeg_camera_dict_to_ui( local_config) cameras.append(ui_config) return self.check_finished(cameras, length) if length[0] == 0: return self.finish_json({'cameras': []})
def set_main_config(ui_config): logging.debug('setting main config...') old_main_config = config.get_main() old_admin_username = old_main_config.get('@admin_username') old_normal_username = old_main_config.get('@normal_username') main_config = config.main_ui_to_dict(ui_config) main_config.setdefault('camera', old_main_config.get('camera', [])) admin_username = main_config.get('@admin_username') admin_password = main_config.get('@admin_password') normal_username = main_config.get('@normal_username') normal_password = main_config.get('@normal_password') additional_configs = config.get_additional_structure( camera=False)[1] reboot_config_names = [('@_' + c['name']) for c in list(additional_configs.values()) if c.get('reboot')] reboot = bool([ k for k in reboot_config_names if old_main_config.get(k) != main_config.get(k) ]) config.set_main(main_config) reload = False restart = False if admin_username != old_admin_username or admin_password is not None: logging.debug('admin credentials changed, reload needed') reload = True if normal_username != old_normal_username or normal_password is not None: logging.debug( 'surveillance credentials changed, all camera configs must be updated' ) # reconfigure all local cameras to update the stream authentication options for camera_id in config.get_camera_ids(): local_config = config.get_camera(camera_id) if not utils.is_local_motion_camera(local_config): continue ui_config = config.motion_camera_dict_to_ui(local_config) local_config = config.motion_camera_ui_to_dict( ui_config, local_config) config.set_camera(camera_id, local_config) restart = True if reboot and settings.ENABLE_REBOOT: logging.debug('system settings changed, reboot needed') else: reboot = False return {'reload': reload, 'reboot': reboot, 'restart': restart}
async def set_config(self, camera_id): try: ui_config = json.loads(self.request.body) except Exception as e: logging.error('could not decode json: %(msg)s' % {'msg': str(e)}) raise camera_ids = config.get_camera_ids() async def set_camera_config(camera_id, ui_config, on_finish): logging.debug('setting config for camera %(id)s...' % {'id': camera_id}) if camera_id not in camera_ids: raise HTTPError(404, 'no such camera') local_config = config.get_camera(camera_id) if utils.is_local_motion_camera(local_config): local_config = config.motion_camera_ui_to_dict( ui_config, local_config) config.set_camera(camera_id, local_config) on_finish(None, True) # (no error, motion needs restart) elif utils.is_remote_camera(local_config): # update the camera locally local_config['@enabled'] = ui_config['enabled'] config.set_camera(camera_id, local_config) if 'name' in ui_config: def on_finish_wrapper(e=None): return on_finish(e, False) ui_config[ 'enabled'] = True # never disable the camera remotely result = await remote.set_config(local_config, ui_config) return on_finish(result, False) else: # when the ui config supplied has only the enabled state # and no useful fields (such as "name"), # the camera was probably disabled due to errors on_finish(None, False) else: # assuming simple mjpeg camera local_config = config.simple_mjpeg_camera_ui_to_dict( ui_config, local_config) config.set_camera(camera_id, local_config) on_finish(None, False) # (no error, motion doesn't need restart) def set_main_config(ui_config): logging.debug('setting main config...') old_main_config = config.get_main() old_admin_username = old_main_config.get('@admin_username') old_normal_username = old_main_config.get('@normal_username') main_config = config.main_ui_to_dict(ui_config) main_config.setdefault('camera', old_main_config.get('camera', [])) admin_username = main_config.get('@admin_username') admin_password = main_config.get('@admin_password') normal_username = main_config.get('@normal_username') normal_password = main_config.get('@normal_password') additional_configs = config.get_additional_structure( camera=False)[1] reboot_config_names = [('@_' + c['name']) for c in list(additional_configs.values()) if c.get('reboot')] reboot = bool([ k for k in reboot_config_names if old_main_config.get(k) != main_config.get(k) ]) config.set_main(main_config) reload = False restart = False if admin_username != old_admin_username or admin_password is not None: logging.debug('admin credentials changed, reload needed') reload = True if normal_username != old_normal_username or normal_password is not None: logging.debug( 'surveillance credentials changed, all camera configs must be updated' ) # reconfigure all local cameras to update the stream authentication options for camera_id in config.get_camera_ids(): local_config = config.get_camera(camera_id) if not utils.is_local_motion_camera(local_config): continue ui_config = config.motion_camera_dict_to_ui(local_config) local_config = config.motion_camera_ui_to_dict( ui_config, local_config) config.set_camera(camera_id, local_config) restart = True if reboot and settings.ENABLE_REBOOT: logging.debug('system settings changed, reboot needed') else: reboot = False return {'reload': reload, 'reboot': reboot, 'restart': restart} reload = False # indicates that browser should reload the page reboot = [False] # indicates that the server will reboot immediately restart = [ False ] # indicates that the local motion instance was modified and needs to be restarted error = [None] def finish(): if reboot[0]: if settings.ENABLE_REBOOT: def call_reboot(): PowerControl.reboot() io_loop = IOLoop.instance() io_loop.add_timeout(datetime.timedelta(seconds=2), call_reboot) return self.finish({ 'reload': False, 'reboot': True, 'error': None }) else: reboot[0] = False if restart[0]: logging.debug('motion needs to be restarted') motionctl.stop() if settings.SMB_SHARES: logging.debug('updating SMB mounts') stop, start = smbctl.update_mounts() # @UnusedVariable if start: motionctl.start() else: motionctl.start() self.finish({ 'reload': reload, 'reboot': reboot[0], 'error': error[0] }) if camera_id is not None: if camera_id == 0: # multiple camera configs if len(ui_config) > 1: logging.debug('setting multiple configs') elif len(ui_config) == 0: logging.warning('no configuration to set') return self.finish() so_far = [0] def check_finished(e, r): restart[0] = restart[0] or r error[0] = error[0] or e so_far[0] += 1 if so_far[0] >= len(ui_config): # finished finish() # make sure main config is handled first items = list(ui_config.items()) items.sort(key=lambda key_cfg: key_cfg[0] != 'main') for key, cfg in items: if key == 'main': result = set_main_config(cfg) reload = result['reload'] or reload reboot[0] = result['reboot'] or reboot[0] restart[0] = result['restart'] or restart[0] check_finished(None, False) else: await set_camera_config(int(key), cfg, check_finished) else: # single camera config def on_finish(e, r): error[0] = e restart[0] = r finish() await set_camera_config(camera_id, ui_config, on_finish) else: # main config result = set_main_config(ui_config) reload = result['reload'] reboot[0] = result['reboot'] restart[0] = result['restart']