diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fa258ca7e..259bd9c7c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,7 +12,6 @@ "esbenp.prettier-vscode", "ms-python.vscode-pylance", "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode", "mikestead.dotenv", "csstools.postcss", "blanu.vscode-styled-jsx", diff --git a/docker/Dockerfile b/docker/Dockerfile index d758a8b5a..622da1432 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -57,7 +57,8 @@ RUN pip3 wheel --wheel-dir=/wheels \ peewee_migrate \ pydantic \ zeroconf \ - ws4py + ws4py \ + requests # Frigate Container FROM debian:11-slim diff --git a/docs/docs/configuration/birdseye.md b/docs/docs/configuration/birdseye.md new file mode 100644 index 000000000..a3f05a107 --- /dev/null +++ b/docs/docs/configuration/birdseye.md @@ -0,0 +1,14 @@ +# Birdseye + +Birdseye allows a heads-up view of your cameras to see what is going on around your property / space without having to watch all cameras that may have nothing happening. Birdseye allows specific modes that intelligently show and disappear based on what you care about. + +### Birdseye Modes + +Birdseye offers different modes to customize which cameras show under which circumstances. + - **continuous:** All cameras are always included + - **motion:** Cameras that have detected motion within the last 30 seconds are included + - **objects:** Cameras that have tracked an active object within the last 30 seconds are included + +### Custom Birdseye Icon + +A custom icon can be added to the birdseye background by provided a file `custom.png` inside of the Frigate `media` folder. The file must be a png with the icon as transparent, any non-transparent pixels will be white when displayed in the birdseye view. diff --git a/docs/sidebars.js b/docs/sidebars.js index 2b3669bdf..df330936f 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -22,6 +22,7 @@ module.exports = { 'configuration/objects', 'configuration/rtmp', 'configuration/zones', + 'configuration/birdseye', 'configuration/advanced', 'configuration/hardware_acceleration', 'configuration/nvdec', diff --git a/frigate/app.py b/frigate/app.py index bf593d6ac..8f0aa36da 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -16,7 +16,7 @@ from playhouse.sqliteq import SqliteQueueDatabase from pydantic import ValidationError from frigate.config import DetectorTypeEnum, FrigateConfig -from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR +from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR, PLUS_ENV_VAR, PLUS_API_HOST from frigate.edgetpu import EdgeTPUProcess from frigate.events import EventCleanup, EventProcessor from frigate.http import create_app @@ -25,6 +25,7 @@ from frigate.models import Event, Recordings from frigate.mqtt import MqttSocketRelay, create_mqtt_client from frigate.object_processing import TrackedObjectProcessor from frigate.output import output_frames +from frigate.plus import PlusApi from frigate.record import RecordingCleanup, RecordingMaintainer from frigate.stats import StatsEmitter, stats_init from frigate.version import VERSION @@ -44,6 +45,11 @@ class FrigateApp: self.detection_out_events: Dict[str, mp.Event] = {} self.detection_shms: List[mp.shared_memory.SharedMemory] = [] self.log_queue = mp.Queue() + self.plus_api = ( + PlusApi(PLUS_API_HOST, os.environ.get(PLUS_ENV_VAR)) + if PLUS_ENV_VAR in os.environ + else None + ) self.camera_metrics = {} def set_environment_vars(self): @@ -146,6 +152,7 @@ class FrigateApp: self.db, self.stats_tracking, self.detected_frames_processor, + self.plus_api, ) def init_mqtt(self): diff --git a/frigate/const.py b/frigate/const.py index afb6075a7..004e3c28e 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -3,3 +3,5 @@ CLIPS_DIR = f"{BASE_DIR}/clips" RECORD_DIR = f"{BASE_DIR}/recordings" CACHE_DIR = "/tmp/cache" YAML_EXT = (".yaml", ".yml") +PLUS_ENV_VAR = "PLUS_API_KEY" +PLUS_API_HOST = "https://api.frigate.video" diff --git a/frigate/http.py b/frigate/http.py index f21d1f8e6..ca73f76c0 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -29,7 +29,7 @@ from flask import ( from peewee import SqliteDatabase, operator, fn, DoesNotExist, Value from playhouse.shortcuts import model_to_dict -from frigate.const import CLIPS_DIR, RECORD_DIR +from frigate.const import CLIPS_DIR, PLUS_ENV_VAR from frigate.models import Event, Recordings from frigate.stats import stats_snapshot from frigate.util import calculate_region @@ -45,6 +45,7 @@ def create_app( database: SqliteDatabase, stats_tracking, detected_frames_processor, + plus_api, ): app = Flask(__name__) @@ -61,6 +62,7 @@ def create_app( app.frigate_config = frigate_config app.stats_tracking = stats_tracking app.detected_frames_processor = detected_frames_processor + app.plus_api = plus_api app.register_blueprint(bp) @@ -137,6 +139,58 @@ def set_retain(id): ) +@bp.route("/events//plus", methods=("POST",)) +def send_to_plus(id): + if current_app.plus_api is None: + return make_response( + jsonify( + { + "success": False, + "message": "PLUS_API_KEY environment variable is not set", + } + ), + 400, + ) + + try: + event = Event.get(Event.id == id) + except DoesNotExist: + return make_response( + jsonify({"success": False, "message": "Event" + id + " not found"}), 404 + ) + + if event.plus_id: + return make_response( + jsonify({"success": False, "message": "Already submitted to plus"}), 400 + ) + + # load clean.png + try: + filename = f"{event.camera}-{event.id}-clean.png" + image = cv2.imread(os.path.join(CLIPS_DIR, filename)) + except Exception: + return make_response( + jsonify( + {"success": False, "message": "Unable to load clean png for event"} + ), + 400, + ) + + try: + plus_id = current_app.plus_api.upload_image(image, event.camera) + except Exception as ex: + return make_response( + jsonify({"success": False, "message": str(ex)}), + 400, + ) + + # store image id in the database + event.plus_id = plus_id + event.save() + + return make_response(jsonify({"success": True, "plus_id": plus_id}), 200) + + @bp.route("/events//retain", methods=("DELETE",)) def delete_retain(id): try: @@ -153,6 +207,7 @@ def delete_retain(id): jsonify({"success": True, "message": "Event " + id + " un-retained"}), 200 ) + @bp.route("/events//sub_label", methods=("POST",)) def set_sub_label(id): try: @@ -167,19 +222,31 @@ def set_sub_label(id): else: new_sub_label = None - if new_sub_label and len(new_sub_label) > 20: return make_response( - jsonify({"success": False, "message": new_sub_label + " exceeds the 20 character limit for sub_label"}), 400 + jsonify( + { + "success": False, + "message": new_sub_label + + " exceeds the 20 character limit for sub_label", + } + ), + 400, ) - event.sub_label = new_sub_label event.save() return make_response( - jsonify({"success": True, "message": "Event " + id + " sub label set to " + new_sub_label}), 200 + jsonify( + { + "success": True, + "message": "Event " + id + " sub label set to " + new_sub_label, + } + ), + 200, ) + @bp.route("/events/", methods=("DELETE",)) def delete_event(id): try: @@ -252,6 +319,7 @@ def event_thumbnail(id): response.headers["Cache-Control"] = "private, max-age=31536000" return response + @bp.route("//