diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7c91f4f6f..3e654335f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -37,42 +37,54 @@ "onAutoForward": "silent" } }, - "extensions": [ - "ms-python.vscode-pylance", - "ms-python.python", - "visualstudioexptteam.vscodeintellicode", - "mhutchie.git-graph", - "ms-azuretools.vscode-docker", - "streetsidesoftware.code-spell-checker", - "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint", - "mikestead.dotenv", - "csstools.postcss", - "blanu.vscode-styled-jsx", - "bradlc.vscode-tailwindcss" - ], - "settings": { - "remote.autoForwardPorts": false, - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.formatting.provider": "black", - "python.languageServer": "Pylance", - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "python.testing.pytestEnabled": false, - "python.testing.unittestEnabled": true, - "python.testing.unittestArgs": ["-v", "-s", "./frigate/test"], - "files.trimTrailingWhitespace": true, - "eslint.workingDirectories": ["./web"], - "[json][jsonc]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[jsx][js][tsx][ts]": { - "editor.codeActionsOnSave": ["source.addMissingImports", "source.fixAll"], - "editor.tabSize": 2 - }, - "cSpell.ignoreWords": ["rtmp"], - "cSpell.words": ["preact"] + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.black-formatter", + "visualstudioexptteam.vscodeintellicode", + "mhutchie.git-graph", + "ms-azuretools.vscode-docker", + "streetsidesoftware.code-spell-checker", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "mikestead.dotenv", + "csstools.postcss", + "blanu.vscode-styled-jsx", + "bradlc.vscode-tailwindcss" + ], + "settings": { + "remote.autoForwardPorts": false, + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "none", + "python.languageServer": "Pylance", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true, + "python.testing.unittestArgs": ["-v", "-s", "./frigate/test"], + "files.trimTrailingWhitespace": true, + "eslint.workingDirectories": ["./web"], + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true + }, + "[json][jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsx][js][tsx][ts]": { + "editor.codeActionsOnSave": [ + "source.addMissingImports", + "source.fixAll" + ], + "editor.tabSize": 2 + }, + "cSpell.ignoreWords": ["rtmp"], + "cSpell.words": ["preact"] + } + } } } diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 528d1e20e..1c1d2cd76 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -65,7 +65,7 @@ jobs: - name: Check out the repository uses: actions/checkout@v3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.5.0 + uses: actions/setup-python@v4.6.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Install requirements diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/frigate/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/frigate/run index 6c1c1a442..0a835550e 100755 --- a/docker/rootfs/etc/s6-overlay/s6-rc.d/frigate/run +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/frigate/run @@ -12,7 +12,7 @@ s6-svc -O . function migrate_db_path() { # Find config file in yaml or yml, but prefer yaml local config_file="${CONFIG_FILE:-"/config/config.yml"}" - local config_file_yaml="${config_file//.yaml/.yml}" + local config_file_yaml="${config_file//.yml/.yaml}" if [[ -f "${config_file_yaml}" ]]; then config_file="${config_file_yaml}" elif [[ ! -f "${config_file}" ]]; then diff --git a/docker/rootfs/usr/local/nginx/conf/nginx.conf b/docker/rootfs/usr/local/nginx/conf/nginx.conf index eff884c14..72725efe4 100644 --- a/docker/rootfs/usr/local/nginx/conf/nginx.conf +++ b/docker/rootfs/usr/local/nginx/conf/nginx.conf @@ -221,6 +221,7 @@ http { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; proxy_pass http://frigate_api/; proxy_pass_request_headers on; proxy_set_header Host $host; diff --git a/docs/docs/configuration/advanced.md b/docs/docs/configuration/advanced.md index 818c4b9cd..40efd67e5 100644 --- a/docs/docs/configuration/advanced.md +++ b/docs/docs/configuration/advanced.md @@ -24,7 +24,6 @@ Examples of available modules are: - `frigate.app` - `frigate.mqtt` - `frigate.object_detection` -- `frigate.zeroconf` - `detector.` - `watchdog.` - `ffmpeg..` NOTE: All FFmpeg logs are sent as `error` level. diff --git a/docs/docs/configuration/cameras.md b/docs/docs/configuration/cameras.md index d8fefed8f..8f907cb3f 100644 --- a/docs/docs/configuration/cameras.md +++ b/docs/docs/configuration/cameras.md @@ -48,3 +48,21 @@ cameras: ``` For camera model specific settings check the [camera specific](camera_specific.md) infos. + +## Setting up camera PTZ controls + +Add onvif config to camera + +```yaml +cameras: + back: + ffmpeg: + ... + onvif: + host: 10.0.10.10 + port: 8000 + user: admin + password: password +``` + +then PTZ controls will be available in the cameras WebUI. diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 2fc5f883f..5a1284ebb 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -55,6 +55,14 @@ mqtt: - path: rtsp://{FRIGATE_RTSP_USER}:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:8554/unicast ``` +```yaml +onvif: + host: 10.0.10.10 + port: 8000 + user: "{FRIGATE_RTSP_USER}" + password: "{FRIGATE_RTSP_PASSWORD}" +``` + ```yaml mqtt: # Optional: Enable mqtt server (default: shown below) @@ -497,6 +505,19 @@ cameras: # Optional: Whether or not to show the camera in the Frigate UI (default: shown below) dashboard: True + # Optional: connect to ONVIF camera + # to enable PTZ controls. + onvif: + # Required: host of the camera being connected to. + host: 0.0.0.0 + # Optional: ONVIF port for device (default: shown below). + port: 8000 + # Optional: username for login. + # NOTE: Some devices require admin to access ONVIF. + user: admin + # Optional: password for login. + password: admin + # Optional: Configuration for how to sort the cameras in the Birdseye view. birdseye: # Optional: Adjust sort order of cameras in the Birdseye view. Larger numbers come later (default: shown below) diff --git a/docs/docs/integrations/api.md b/docs/docs/integrations/api.md index 2b89288db..9aec392a4 100644 --- a/docs/docs/integrations/api.md +++ b/docs/docs/integrations/api.md @@ -172,11 +172,11 @@ Events from the database. Accepts the following query string parameters: Timeline of key moments of an event(s) from the database. Accepts the following query string parameters: -| param | Type | Description | -| -------------------- | ---- | --------------------------------------------- | -| `camera` | int | Name of camera | -| `source_id` | str | ID of tracked object | -| `limit` | int | Limit the number of events returned | +| param | Type | Description | +| ----------- | ---- | ----------------------------------- | +| `camera` | str | Name of camera | +| `source_id` | str | ID of tracked object | +| `limit` | int | Limit the number of events returned | ### `GET /api/events/summary` @@ -198,6 +198,14 @@ Sets retain to true for the event id. Submits the snapshot of the event to Frigate+ for labeling. +| param | Type | Description | +| -------------------- | ---- | ---------------------------------- | +| `include_annotation` | int | Submit annotation to Frigate+ too. | + +### `PUT /api/events//false_positive` + +Submits the snapshot of the event to Frigate+ for labeling and adds the detection as a false positive. + ### `DELETE /api/events//retain` Sets retain to false for the event id (event may be deleted quickly after removing). @@ -283,3 +291,7 @@ Get ffprobe output for camera feed paths. | param | Type | Description | | ------- | ------ | ---------------------------------- | | `paths` | string | `,` separated list of camera paths | + +### `GET /api//ptz/info` + +Get PTZ info for the camera. diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index 2a2195abd..814656258 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -157,4 +157,15 @@ Topic to adjust motion contour area for a camera. Expected value is an integer. ### `frigate//motion_contour_area/state` -Topic with current motion contour area for a camera. Published value is an integer. \ No newline at end of file +Topic with current motion contour area for a camera. Published value is an integer. + +### `frigate//ptz` + +Topic to send PTZ commands to camera. + +| Command | Description | +| ---------------------- | --------------------------------------------------------------------------------------- | +| `preset-` | send command to move to preset with name `` | +| `MOVE_` | send command to continuously move in ``, possible values are [UP, DOWN, LEFT, RIGHT] | +| `ZOOM_` | send command to continuously zoom ``, possible values are [IN, OUT] | +| `STOP` | send command to stop moving | diff --git a/frigate/app.py b/frigate/app.py index 8c1cd4433..54d2825c8 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -27,6 +27,7 @@ from frigate.models import Event, Recordings, Timeline from frigate.object_processing import TrackedObjectProcessor from frigate.output import output_frames from frigate.plus import PlusApi +from frigate.ptz import OnvifController from frigate.record import RecordingCleanup, RecordingMaintainer from frigate.stats import StatsEmitter, stats_init from frigate.storage import StorageMaintainer @@ -173,9 +174,13 @@ class FrigateApp: self.stats_tracking, self.detected_frames_processor, self.storage_maintainer, + self.onvif_controller, self.plus_api, ) + def init_onvif(self) -> None: + self.onvif_controller = OnvifController(self.config) + def init_dispatcher(self) -> None: comms: list[Communicator] = [] @@ -183,7 +188,9 @@ class FrigateApp: comms.append(MqttClient(self.config)) comms.append(WebSocketClient(self.config)) - self.dispatcher = Dispatcher(self.config, self.camera_metrics, comms) + self.dispatcher = Dispatcher( + self.config, self.onvif_controller, self.camera_metrics, comms + ) def start_detectors(self) -> None: for name in self.config.cameras.keys(): @@ -382,6 +389,7 @@ class FrigateApp: self.set_log_levels() self.init_queues() self.init_database() + self.init_onvif() self.init_dispatcher() except Exception as e: print(e) diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index d304509e4..7a2c98392 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -7,6 +7,7 @@ from typing import Any, Callable from abc import ABC, abstractmethod from frigate.config import FrigateConfig +from frigate.ptz import OnvifController, OnvifCommandEnum from frigate.types import CameraMetricsTypes from frigate.util import restart_frigate @@ -39,10 +40,12 @@ class Dispatcher: def __init__( self, config: FrigateConfig, + onvif: OnvifController, camera_metrics: dict[str, CameraMetricsTypes], communicators: list[Communicator], ) -> None: self.config = config + self.onvif = onvif self.camera_metrics = camera_metrics self.comms = communicators @@ -63,12 +66,21 @@ class Dispatcher: """Handle receiving of payload from communicators.""" if topic.endswith("set"): try: + # example /cam_name/detect/set payload=ON|OFF camera_name = topic.split("/")[-3] command = topic.split("/")[-2] self._camera_settings_handlers[command](camera_name, payload) - except Exception as e: + except IndexError as e: logger.error(f"Received invalid set command: {topic}") return + elif topic.endswith("ptz"): + try: + # example /cam_name/ptz payload=MOVE_UP|MOVE_DOWN|STOP... + camera_name = topic.split("/")[-2] + self._on_ptz_command(camera_name, payload) + except IndexError as e: + logger.error(f"Received invalid ptz command: {topic}") + return elif topic == "restart": restart_frigate() @@ -204,3 +216,18 @@ class Dispatcher: snapshots_settings.enabled = False self.publish(f"{camera_name}/snapshots/state", payload, retain=True) + + def _on_ptz_command(self, camera_name: str, payload: str) -> None: + """Callback for ptz topic.""" + try: + if "preset" in payload.lower(): + command = OnvifCommandEnum.preset + param = payload.lower().split("-")[1] + else: + command = OnvifCommandEnum[payload.lower()] + param = "" + + self.onvif.handle_command(camera_name, command, param) + logger.info(f"Setting ptz command to {command} for {camera_name}") + except KeyError as k: + logger.error(f"Invalid PTZ command {payload}: {k}") diff --git a/frigate/comms/mqtt.py b/frigate/comms/mqtt.py index d106aae71..37fdd44ae 100644 --- a/frigate/comms/mqtt.py +++ b/frigate/comms/mqtt.py @@ -167,6 +167,12 @@ class MqttClient(Communicator): # type: ignore[misc] self.on_mqtt_command, ) + if self.config.cameras[name].onvif.host: + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/{name}/ptz", + self.on_mqtt_command, + ) + self.client.message_callback_add( f"{self.mqtt_config.topic_prefix}/restart", self.on_mqtt_command ) diff --git a/frigate/config.py b/frigate/config.py index 9de25e8a2..f3e760b6a 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -18,6 +18,7 @@ from frigate.const import ( REGEX_CAMERA_NAME, YAML_EXT, ) +from frigate.detectors.detector_config import BaseDetectorConfig from frigate.util import ( create_mask, deep_merge, @@ -124,6 +125,13 @@ class MqttConfig(FrigateBaseModel): return v +class OnvifConfig(FrigateBaseModel): + host: str = Field(default="", title="Onvif Host") + port: int = Field(default=8000, title="Onvif Port") + user: Optional[str] = Field(title="Onvif Username") + password: Optional[str] = Field(title="Onvif Password") + + class RetainModeEnum(str, Enum): all = "all" motion = "motion" @@ -607,6 +615,9 @@ class CameraConfig(FrigateBaseModel): detect: DetectConfig = Field( default_factory=DetectConfig, title="Object detection configuration." ) + onvif: OnvifConfig = Field( + default_factory=OnvifConfig, title="Camera Onvif Configuration." + ) ui: CameraUiConfig = Field( default_factory=CameraUiConfig, title="Camera UI Modifications." ) @@ -771,7 +782,7 @@ def verify_config_roles(camera_config: CameraConfig) -> None: def verify_valid_live_stream_name( frigate_config: FrigateConfig, camera_config: CameraConfig -) -> None: +) -> ValueError | None: """Verify that a restream exists to use for live view.""" if ( camera_config.live.stream_name @@ -849,7 +860,7 @@ class FrigateConfig(FrigateBaseModel): model: ModelConfig = Field( default_factory=ModelConfig, title="Detection model configuration." ) - detectors: Dict[str, DetectorConfig] = Field( + detectors: Dict[str, BaseDetectorConfig] = Field( default=DEFAULT_DETECTORS, title="Detector hardware configuration.", ) @@ -939,6 +950,15 @@ class FrigateConfig(FrigateBaseModel): for input in camera_config.ffmpeg.inputs: input.path = input.path.format(**FRIGATE_ENV_VARS) + # ONVIF substitution + if camera_config.onvif.user or camera_config.onvif.password: + camera_config.onvif.user = camera_config.onvif.user.format( + **FRIGATE_ENV_VARS + ) + camera_config.onvif.password = camera_config.onvif.password.format( + **FRIGATE_ENV_VARS + ) + # Add default filters object_keys = camera_config.objects.track if camera_config.objects.filters is None: @@ -1032,7 +1052,15 @@ class FrigateConfig(FrigateBaseModel): detector_config.model.dict(exclude_unset=True), config.model.dict(exclude_unset=True), ) + + if not "path" in merged_model: + if detector_config.type == "cpu": + merged_model["path"] = "/cpu_model.tflite" + elif detector_config.type == "edgetpu": + merged_model["path"] = "/edgetpu_model.tflite" + detector_config.model = ModelConfig.parse_obj(merged_model) + detector_config.model.compute_model_hash() config.detectors[key] = detector_config return config diff --git a/frigate/detectors/detector_config.py b/frigate/detectors/detector_config.py index 25dede107..46f92e6ab 100644 --- a/frigate/detectors/detector_config.py +++ b/frigate/detectors/detector_config.py @@ -1,3 +1,4 @@ +import hashlib import logging from enum import Enum from typing import Dict, List, Optional, Tuple, Union, Literal @@ -49,6 +50,7 @@ class ModelConfig(BaseModel): ) _merged_labelmap: Optional[Dict[int, str]] = PrivateAttr() _colormap: Dict[int, Tuple[int, int, int]] = PrivateAttr() + _model_hash: str = PrivateAttr() @property def merged_labelmap(self) -> Dict[int, str]: @@ -58,6 +60,10 @@ class ModelConfig(BaseModel): def colormap(self) -> Dict[int, Tuple[int, int, int]]: return self._colormap + @property + def model_hash(self) -> str: + return self._model_hash + def __init__(self, **config): super().__init__(**config) @@ -67,6 +73,13 @@ class ModelConfig(BaseModel): } self._colormap = {} + def compute_model_hash(self) -> None: + with open(self.path, "rb") as f: + file_hash = hashlib.md5() + while chunk := f.read(8192): + file_hash.update(chunk) + self._model_hash = file_hash.hexdigest() + def create_colormap(self, enabled_labels: set[str]) -> None: """Get a list of colors for enabled labels.""" cmap = plt.cm.get_cmap("tab10", len(enabled_labels)) diff --git a/frigate/detectors/plugins/cpu_tfl.py b/frigate/detectors/plugins/cpu_tfl.py index fb9cbbfae..b22ac9a54 100644 --- a/frigate/detectors/plugins/cpu_tfl.py +++ b/frigate/detectors/plugins/cpu_tfl.py @@ -27,7 +27,7 @@ class CpuTfl(DetectionApi): def __init__(self, detector_config: CpuDetectorConfig): self.interpreter = Interpreter( - model_path=detector_config.model.path or "/cpu_model.tflite", + model_path=detector_config.model.path, num_threads=detector_config.num_threads or 3, ) diff --git a/frigate/detectors/plugins/edgetpu_tfl.py b/frigate/detectors/plugins/edgetpu_tfl.py index 840d41f66..6837adcb0 100644 --- a/frigate/detectors/plugins/edgetpu_tfl.py +++ b/frigate/detectors/plugins/edgetpu_tfl.py @@ -37,7 +37,7 @@ class EdgeTpuTfl(DetectionApi): edge_tpu_delegate = load_delegate("libedgetpu.so.1.0", device_config) logger.info("TPU found") self.interpreter = Interpreter( - model_path=detector_config.model.path or "/edgetpu_model.tflite", + model_path=detector_config.model.path, experimental_delegates=[edge_tpu_delegate], ) except ValueError: diff --git a/frigate/events.py b/frigate/events.py index 416b28fd7..558cb2139 100644 --- a/frigate/events.py +++ b/frigate/events.py @@ -12,6 +12,7 @@ from frigate.const import CLIPS_DIR from frigate.models import Event from frigate.timeline import TimelineSourceEnum from frigate.types import CameraMetricsTypes +from frigate.util import to_relative_box from multiprocessing.queues import Queue from multiprocessing.synchronize import Event as MpEvent @@ -20,22 +21,18 @@ from typing import Dict logger = logging.getLogger(__name__) -def should_insert_db(prev_event: Event, current_event: Event) -> bool: - """If current event has new clip or snapshot.""" - return (not prev_event["has_clip"] and not prev_event["has_snapshot"]) and ( - current_event["has_clip"] or current_event["has_snapshot"] - ) - - def should_update_db(prev_event: Event, current_event: Event) -> bool: """If current_event has updated fields and (clip or snapshot).""" if current_event["has_clip"] or current_event["has_snapshot"]: + # if this is the first time has_clip or has_snapshot turned true + if not prev_event["has_clip"] and not prev_event["has_snapshot"]: + return True + # or if any of the following values changed if ( prev_event["top_score"] != current_event["top_score"] or prev_event["entered_zones"] != current_event["entered_zones"] or prev_event["thumbnail"] != current_event["thumbnail"] - or prev_event["has_clip"] != current_event["has_clip"] - or prev_event["has_snapshot"] != current_event["has_snapshot"] + or prev_event["end_time"] != current_event["end_time"] ): return True return False @@ -85,81 +82,91 @@ class EventProcessor(threading.Thread): ) ) - event_config: EventsConfig = self.config.cameras[camera].record.events - + # if this is the first message, just store it and continue, its not time to insert it in the db if event_type == "start": self.events_in_process[event_data["id"]] = event_data + continue - elif event_type == "update" and should_insert_db( - self.events_in_process[event_data["id"]], event_data - ): + if should_update_db(self.events_in_process[event_data["id"]], event_data): + camera_config = self.config.cameras[camera] + event_config: EventsConfig = camera_config.record.events + width = camera_config.detect.width + height = camera_config.detect.height + first_detector = list(self.config.detectors.values())[0] + + start_time = event_data["start_time"] - event_config.pre_capture + end_time = ( + None + if event_data["end_time"] is None + else event_data["end_time"] + event_config.post_capture + ) + # score of the snapshot + score = ( + None + if event_data["snapshot"] is None + else event_data["snapshot"]["score"] + ) + # detection region in the snapshot + region = ( + None + if event_data["snapshot"] is None + else to_relative_box( + width, + height, + event_data["snapshot"]["region"], + ) + ) + # bounding box for the snapshot + box = ( + None + if event_data["snapshot"] is None + else to_relative_box( + width, + height, + event_data["snapshot"]["box"], + ) + ) + + # keep these from being set back to false because the event + # may have started while recordings and snapshots were enabled + # this would be an issue for long running events + if self.events_in_process[event_data["id"]]["has_clip"]: + event_data["has_clip"] = True + if self.events_in_process[event_data["id"]]["has_snapshot"]: + event_data["has_snapshot"] = True + + event = { + Event.id: event_data["id"], + Event.label: event_data["label"], + Event.camera: camera, + Event.start_time: start_time, + Event.end_time: end_time, + Event.top_score: event_data["top_score"], + Event.score: score, + Event.zones: list(event_data["entered_zones"]), + Event.thumbnail: event_data["thumbnail"], + Event.region: region, + Event.box: box, + Event.has_clip: event_data["has_clip"], + Event.has_snapshot: event_data["has_snapshot"], + Event.model_hash: first_detector.model.model_hash, + Event.model_type: first_detector.model.model_type, + Event.detector_type: first_detector.type, + } + + ( + Event.insert(event) + .on_conflict( + conflict_target=[Event.id], + update=event, + ) + .execute() + ) + + # update the stored copy for comparison on future update messages self.events_in_process[event_data["id"]] = event_data - # TODO: this will generate a lot of db activity possibly - Event.insert( - id=event_data["id"], - label=event_data["label"], - camera=camera, - start_time=event_data["start_time"] - event_config.pre_capture, - end_time=None, - top_score=event_data["top_score"], - false_positive=event_data["false_positive"], - zones=list(event_data["entered_zones"]), - thumbnail=event_data["thumbnail"], - region=event_data["region"], - box=event_data["box"], - area=event_data["area"], - has_clip=event_data["has_clip"], - has_snapshot=event_data["has_snapshot"], - ).execute() - - elif event_type == "update" and should_update_db( - self.events_in_process[event_data["id"]], event_data - ): - self.events_in_process[event_data["id"]] = event_data - # TODO: this will generate a lot of db activity possibly - Event.update( - label=event_data["label"], - camera=camera, - start_time=event_data["start_time"] - event_config.pre_capture, - end_time=None, - top_score=event_data["top_score"], - false_positive=event_data["false_positive"], - zones=list(event_data["entered_zones"]), - thumbnail=event_data["thumbnail"], - region=event_data["region"], - box=event_data["box"], - area=event_data["area"], - ratio=event_data["ratio"], - has_clip=event_data["has_clip"], - has_snapshot=event_data["has_snapshot"], - ).where(Event.id == event_data["id"]).execute() - - elif event_type == "end": - if event_data["has_clip"] or event_data["has_snapshot"]: - # Full update for valid end of event - Event.update( - label=event_data["label"], - camera=camera, - start_time=event_data["start_time"] - event_config.pre_capture, - end_time=event_data["end_time"] + event_config.post_capture, - top_score=event_data["top_score"], - false_positive=event_data["false_positive"], - zones=list(event_data["entered_zones"]), - thumbnail=event_data["thumbnail"], - region=event_data["region"], - box=event_data["box"], - area=event_data["area"], - ratio=event_data["ratio"], - has_clip=event_data["has_clip"], - has_snapshot=event_data["has_snapshot"], - ).where(Event.id == event_data["id"]).execute() - else: - # Event ended after clip & snapshot disabled, - # only end time should be updated. - Event.update( - end_time=event_data["end_time"] + event_config.post_capture - ).where(Event.id == event_data["id"]).execute() + if event_type == "end": del self.events_in_process[event_data["id"]] self.event_processed_queue.put((event_data["id"], camera)) diff --git a/frigate/http.py b/frigate/http.py index cb8c1cbe8..cd2a0c523 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -35,6 +35,8 @@ from frigate.config import FrigateConfig from frigate.const import CLIPS_DIR, MAX_SEGMENT_DURATION, RECORD_DIR from frigate.models import Event, Recordings, Timeline from frigate.object_processing import TrackedObject +from frigate.plus import PlusApi +from frigate.ptz import OnvifController from frigate.stats import stats_snapshot from frigate.util import ( clean_camera_user_pass, @@ -42,6 +44,7 @@ from frigate.util import ( restart_frigate, vainfo_hwaccel, get_tz_modifiers, + to_relative_box, ) from frigate.storage import StorageMaintainer from frigate.version import VERSION @@ -57,7 +60,8 @@ def create_app( stats_tracking, detected_frames_processor, storage_maintainer: StorageMaintainer, - plus_api, + onvif: OnvifController, + plus_api: PlusApi, ): app = Flask(__name__) @@ -75,6 +79,7 @@ def create_app( app.stats_tracking = stats_tracking app.detected_frames_processor = detected_frames_processor app.storage_maintainer = storage_maintainer + app.onvif = onvif app.plus_api = plus_api app.camera_error_image = None app.hwaccel_errors = [] @@ -179,6 +184,10 @@ def send_to_plus(id): 400, ) + include_annotation = ( + request.json.get("include_annotation") if request.is_json else None + ) + try: event = Event.get(Event.id == id) except DoesNotExist: @@ -186,6 +195,10 @@ def send_to_plus(id): logger.error(message) return make_response(jsonify({"success": False, "message": message}), 404) + # events from before the conversion to relative dimensions cant include annotations + if any(d > 1 for d in event.box): + include_annotation = None + if event.end_time is None: logger.error(f"Unable to load clean png for in-progress event: {event.id}") return make_response( @@ -238,9 +251,96 @@ def send_to_plus(id): event.plus_id = plus_id event.save() + if not include_annotation is None: + region = event.region + box = event.box + + try: + current_app.plus_api.add_annotation( + event.plus_id, + box, + event.label, + ) + except Exception as ex: + logger.exception(ex) + return make_response( + jsonify({"success": False, "message": str(ex)}), + 400, + ) + return make_response(jsonify({"success": True, "plus_id": plus_id}), 200) +@bp.route("/events//false_positive", methods=("PUT",)) +def false_positive(id): + if not current_app.plus_api.is_active(): + message = "PLUS_API_KEY environment variable is not set" + logger.error(message) + return make_response( + jsonify( + { + "success": False, + "message": message, + } + ), + 400, + ) + + try: + event = Event.get(Event.id == id) + except DoesNotExist: + message = f"Event {id} not found" + logger.error(message) + return make_response(jsonify({"success": False, "message": message}), 404) + + # events from before the conversion to relative dimensions cant include annotations + if any(d > 1 for d in event.box): + message = f"Events prior to 0.13 cannot be submitted as false positives" + logger.error(message) + return make_response(jsonify({"success": False, "message": message}), 400) + + if event.false_positive: + message = f"False positive already submitted to Frigate+" + logger.error(message) + return make_response(jsonify({"success": False, "message": message}), 400) + + if not event.plus_id: + plus_response = send_to_plus(id) + if plus_response.status_code != 200: + return plus_response + # need to refetch the event now that it has a plus_id + event = Event.get(Event.id == id) + + region = event.region + box = event.box + + # provide top score if score is unavailable + score = event.top_score if event.score is None else event.score + + try: + current_app.plus_api.add_false_positive( + event.plus_id, + region, + box, + score, + event.label, + event.model_hash, + event.model_type, + event.detector_type, + ) + except Exception as ex: + logger.exception(ex) + return make_response( + jsonify({"success": False, "message": str(ex)}), + 400, + ) + + event.false_positive = True + event.save() + + return make_response(jsonify({"success": True, "plus_id": event.plus_id}), 200) + + @bp.route("/events//retain", methods=("DELETE",)) def delete_retain(id): try: @@ -654,6 +754,8 @@ def events(): Event.retain_indefinitely, Event.sub_label, Event.top_score, + Event.false_positive, + Event.box, ] if camera != "all": @@ -895,6 +997,14 @@ def mjpeg_feed(camera_name): return "Camera named {} not found".format(camera_name), 404 +@bp.route("//ptz/info") +def camera_ptz_info(camera_name): + if camera_name in current_app.frigate_config.cameras: + return jsonify(current_app.onvif.get_camera_info(camera_name)) + else: + return "Camera named {} not found".format(camera_name), 404 + + @bp.route("//latest.jpg") def latest_frame(camera_name): draw_options = { diff --git a/frigate/log.py b/frigate/log.py index a8041592f..67866942c 100644 --- a/frigate/log.py +++ b/frigate/log.py @@ -19,6 +19,10 @@ from frigate.util import clean_camera_user_pass def listener_configurer() -> None: root = logging.getLogger() + + if root.hasHandlers(): + root.handlers.clear() + console_handler = logging.StreamHandler() formatter = logging.Formatter( "[%(asctime)s] %(name)-30s %(levelname)-8s: %(message)s", "%Y-%m-%d %H:%M:%S" @@ -31,6 +35,10 @@ def listener_configurer() -> None: def root_configurer(queue: Queue) -> None: h = handlers.QueueHandler(queue) root = logging.getLogger() + + if root.hasHandlers(): + root.handlers.clear() + root.addHandler(h) root.setLevel(logging.INFO) diff --git a/frigate/models.py b/frigate/models.py index 9cfd89988..cc1a29f31 100644 --- a/frigate/models.py +++ b/frigate/models.py @@ -19,6 +19,7 @@ class Event(Model): # type: ignore[misc] start_time = DateTimeField() end_time = DateTimeField() top_score = FloatField() + score = FloatField() false_positive = BooleanField() zones = JSONField() thumbnail = TextField() @@ -30,6 +31,9 @@ class Event(Model): # type: ignore[misc] retain_indefinitely = BooleanField(default=False) ratio = FloatField(default=1.0) plus_id = CharField(max_length=30) + model_hash = CharField(max_length=32) + detector_type = CharField(max_length=32) + model_type = CharField(max_length=32) class Timeline(Model): # type: ignore[misc] diff --git a/frigate/object_processing.py b/frigate/object_processing.py index 97ef6f1ef..db2745309 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -185,7 +185,7 @@ class TrackedObject: "id": self.obj_data["id"], "camera": self.camera, "frame_time": self.obj_data["frame_time"], - "snapshot_time": snapshot_time, + "snapshot": self.thumbnail_data, "label": self.obj_data["label"], "sub_label": self.obj_data.get("sub_label"), "top_score": self.top_score, diff --git a/frigate/plus.py b/frigate/plus.py index faf70752e..4b59b961e 100644 --- a/frigate/plus.py +++ b/frigate/plus.py @@ -3,6 +3,7 @@ import json import logging import os import re +from typing import List import requests from frigate.const import PLUS_ENV_VAR, PLUS_API_HOST from requests.models import Response @@ -79,6 +80,13 @@ class PlusApi: json=data, ) + def _put(self, path: str, data: dict) -> Response: + return requests.put( + f"{self.host}/v1/{path}", + headers=self._get_authorization_header(), + json=data, + ) + def is_active(self) -> bool: return self._is_active @@ -124,3 +132,58 @@ class PlusApi: # return image id return str(presigned_urls.get("imageId")) + + def add_false_positive( + self, + plus_id: str, + region: List[float], + bbox: List[float], + score: float, + label: str, + model_hash: str, + model_type: str, + detector_type: str, + ) -> None: + r = self._put( + f"image/{plus_id}/false_positive", + { + "label": label, + "x": bbox[0], + "y": bbox[1], + "w": bbox[2], + "h": bbox[3], + "regionX": region[0], + "regionY": region[1], + "regionW": region[2], + "regionH": region[3], + "score": score, + "model_hash": model_hash, + "model_type": model_type, + "detector_type": detector_type, + }, + ) + + if not r.ok: + raise Exception(r.text) + + def add_annotation( + self, + plus_id: str, + bbox: List[float], + label: str, + difficult: bool = False, + ) -> None: + r = self._put( + f"image/{plus_id}/annotation", + { + "label": label, + "x": bbox[0], + "y": bbox[1], + "w": bbox[2], + "h": bbox[3], + "difficult": difficult, + }, + ) + + if not r.ok: + raise Exception(r.text) diff --git a/frigate/ptz.py b/frigate/ptz.py new file mode 100644 index 000000000..e2c21618e --- /dev/null +++ b/frigate/ptz.py @@ -0,0 +1,219 @@ +"""Configure and control camera via onvif.""" + +import logging +import site + +from enum import Enum +from onvif import ONVIFCamera, ONVIFError + +from frigate.config import FrigateConfig + + +logger = logging.getLogger(__name__) + + +class OnvifCommandEnum(str, Enum): + """Holds all possible move commands""" + + init = "init" + move_down = "move_down" + move_left = "move_left" + move_right = "move_right" + move_up = "move_up" + preset = "preset" + stop = "stop" + zoom_in = "zoom_in" + zoom_out = "zoom_out" + + +class OnvifController: + def __init__(self, config: FrigateConfig) -> None: + self.cams: dict[str, ONVIFCamera] = {} + + for cam_name, cam in config.cameras.items(): + if not cam.enabled: + continue + + if cam.onvif.host: + try: + self.cams[cam_name] = { + "onvif": ONVIFCamera( + cam.onvif.host, + cam.onvif.port, + cam.onvif.user, + cam.onvif.password, + wsdl_dir=site.getsitepackages()[0].replace( + "dist-packages", "site-packages" + ) + + "/wsdl", + ), + "init": False, + "active": False, + "presets": {}, + } + except ONVIFError as e: + logger.error(f"Onvif connection to {cam.name} failed: {e}") + + def _init_onvif(self, camera_name: str) -> bool: + onvif: ONVIFCamera = self.cams[camera_name]["onvif"] + + # create init services + media = onvif.create_media_service() + + try: + profile = media.GetProfiles()[0] + except ONVIFError as e: + logger.error(f"Unable to connect to camera: {camera_name}: {e}") + return False + + ptz = onvif.create_ptz_service() + request = ptz.create_type("GetConfigurationOptions") + request.ConfigurationToken = profile.PTZConfiguration.token + + # setup moving request + move_request = ptz.create_type("ContinuousMove") + move_request.ProfileToken = profile.token + self.cams[camera_name]["move_request"] = move_request + + # setup existing presets + try: + presets: list[dict] = ptz.GetPresets({"ProfileToken": profile.token}) + except ONVIFError as e: + logger.error(f"Unable to get presets from camera: {camera_name}: {e}") + return False + + for preset in presets: + self.cams[camera_name]["presets"][preset["Name"].lower()] = preset["token"] + + # get list of supported features + ptz_config = ptz.GetConfigurationOptions(request) + supported_features = [] + + if ptz_config.Spaces and ptz_config.Spaces.ContinuousPanTiltVelocitySpace: + supported_features.append("pt") + + if ptz_config.Spaces and ptz_config.Spaces.ContinuousZoomVelocitySpace: + supported_features.append("zoom") + + self.cams[camera_name]["features"] = supported_features + + self.cams[camera_name]["init"] = True + return True + + def _stop(self, camera_name: str) -> None: + onvif: ONVIFCamera = self.cams[camera_name]["onvif"] + move_request = self.cams[camera_name]["move_request"] + onvif.get_service("ptz").Stop( + { + "ProfileToken": move_request.ProfileToken, + "PanTilt": True, + "Zoom": True, + } + ) + self.cams[camera_name]["active"] = False + + def _move(self, camera_name: str, command: OnvifCommandEnum) -> None: + if self.cams[camera_name]["active"]: + logger.warning( + f"{camera_name} is already performing an action, stopping..." + ) + self._stop(camera_name) + + self.cams[camera_name]["active"] = True + onvif: ONVIFCamera = self.cams[camera_name]["onvif"] + move_request = self.cams[camera_name]["move_request"] + + if command == OnvifCommandEnum.move_left: + move_request.Velocity = {"PanTilt": {"x": -0.5, "y": 0}} + elif command == OnvifCommandEnum.move_right: + move_request.Velocity = {"PanTilt": {"x": 0.5, "y": 0}} + elif command == OnvifCommandEnum.move_up: + move_request.Velocity = { + "PanTilt": { + "x": 0, + "y": 0.5, + } + } + elif command == OnvifCommandEnum.move_down: + move_request.Velocity = { + "PanTilt": { + "x": 0, + "y": -0.5, + } + } + + onvif.get_service("ptz").ContinuousMove(move_request) + + def _move_to_preset(self, camera_name: str, preset: str) -> None: + if not preset in self.cams[camera_name]["presets"]: + logger.error(f"{preset} is not a valid preset for {camera_name}") + return + + self.cams[camera_name]["active"] = True + move_request = self.cams[camera_name]["move_request"] + onvif: ONVIFCamera = self.cams[camera_name]["onvif"] + preset_token = self.cams[camera_name]["presets"][preset] + onvif.get_service("ptz").GotoPreset( + { + "ProfileToken": move_request.ProfileToken, + "PresetToken": preset_token, + } + ) + self.cams[camera_name]["active"] = False + + def _zoom(self, camera_name: str, command: OnvifCommandEnum) -> None: + if self.cams[camera_name]["active"]: + logger.warning( + f"{camera_name} is already performing an action, stopping..." + ) + self._stop(camera_name) + + self.cams[camera_name]["active"] = True + onvif: ONVIFCamera = self.cams[camera_name]["onvif"] + move_request = self.cams[camera_name]["move_request"] + + if command == OnvifCommandEnum.zoom_in: + move_request.Velocity = {"Zoom": {"x": 0.5}} + elif command == OnvifCommandEnum.zoom_out: + move_request.Velocity = {"Zoom": {"x": -0.5}} + + onvif.get_service("ptz").ContinuousMove(move_request) + + def handle_command( + self, camera_name: str, command: OnvifCommandEnum, param: str = "" + ) -> None: + if camera_name not in self.cams.keys(): + logger.error(f"Onvif is not setup for {camera_name}") + return + + if not self.cams[camera_name]["init"]: + if not self._init_onvif(camera_name): + return + + if command == OnvifCommandEnum.init: + # already init + return + elif command == OnvifCommandEnum.stop: + self._stop(camera_name) + elif command == OnvifCommandEnum.preset: + self._move_to_preset(camera_name, param) + elif ( + command == OnvifCommandEnum.zoom_in or command == OnvifCommandEnum.zoom_out + ): + self._zoom(camera_name, command) + else: + self._move(camera_name, command) + + def get_camera_info(self, camera_name: str) -> dict[str, any]: + if camera_name not in self.cams.keys(): + logger.error(f"Onvif is not setup for {camera_name}") + return {} + + if not self.cams[camera_name]["init"]: + self._init_onvif(camera_name) + + return { + "name": camera_name, + "features": self.cams[camera_name]["features"], + "presets": list(self.cams[camera_name]["presets"].keys()), + } diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py index be9bdcdfd..b978453c1 100644 --- a/frigate/test/test_config.py +++ b/frigate/test/test_config.py @@ -54,7 +54,8 @@ class TestConfig(unittest.TestCase): "type": "openvino", }, }, - "model": {"path": "/default.tflite", "width": 512}, + # needs to be a file that will exist, doesnt matter what + "model": {"path": "/etc/hosts", "width": 512}, } frigate_config = FrigateConfig(**(deep_merge(config, self.minimal))) @@ -72,10 +73,10 @@ class TestConfig(unittest.TestCase): assert runtime_config.detectors["edgetpu"].device is None assert runtime_config.detectors["openvino"].device is None - assert runtime_config.model.path == "/default.tflite" + assert runtime_config.model.path == "/etc/hosts" assert runtime_config.detectors["cpu"].model.path == "/cpu_model.tflite" assert runtime_config.detectors["edgetpu"].model.path == "/edgetpu_model.tflite" - assert runtime_config.detectors["openvino"].model.path == "/default.tflite" + assert runtime_config.detectors["openvino"].model.path == "/etc/hosts" assert runtime_config.model.width == 512 assert runtime_config.detectors["cpu"].model.width == 512 diff --git a/frigate/test/test_http.py b/frigate/test/test_http.py index 12105926e..bc08ec010 100644 --- a/frigate/test/test_http.py +++ b/frigate/test/test_http.py @@ -114,7 +114,13 @@ class TestHttp(unittest.TestCase): def test_get_event_list(self): app = create_app( - FrigateConfig(**self.minimal_config), self.db, None, None, None, PlusApi() + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + PlusApi(), ) id = "123456.random" id2 = "7890.random" @@ -143,7 +149,13 @@ class TestHttp(unittest.TestCase): def test_get_good_event(self): app = create_app( - FrigateConfig(**self.minimal_config), self.db, None, None, None, PlusApi() + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + PlusApi(), ) id = "123456.random" @@ -157,7 +169,13 @@ class TestHttp(unittest.TestCase): def test_get_bad_event(self): app = create_app( - FrigateConfig(**self.minimal_config), self.db, None, None, None, PlusApi() + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + PlusApi(), ) id = "123456.random" bad_id = "654321.other" @@ -170,7 +188,13 @@ class TestHttp(unittest.TestCase): def test_delete_event(self): app = create_app( - FrigateConfig(**self.minimal_config), self.db, None, None, None, PlusApi() + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + PlusApi(), ) id = "123456.random" @@ -185,7 +209,13 @@ class TestHttp(unittest.TestCase): def test_event_retention(self): app = create_app( - FrigateConfig(**self.minimal_config), self.db, None, None, None, PlusApi() + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + PlusApi(), ) id = "123456.random" @@ -204,7 +234,13 @@ class TestHttp(unittest.TestCase): def test_set_delete_sub_label(self): app = create_app( - FrigateConfig(**self.minimal_config), self.db, None, None, None, PlusApi() + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + PlusApi(), ) id = "123456.random" sub_label = "sub" @@ -232,7 +268,13 @@ class TestHttp(unittest.TestCase): def test_sub_label_list(self): app = create_app( - FrigateConfig(**self.minimal_config), self.db, None, None, None, PlusApi() + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + PlusApi(), ) id = "123456.random" sub_label = "sub" @@ -255,6 +297,7 @@ class TestHttp(unittest.TestCase): None, None, None, + None, PlusApi(), ) @@ -270,6 +313,7 @@ class TestHttp(unittest.TestCase): None, None, None, + None, PlusApi(), ) id = "123456.random" @@ -288,6 +332,7 @@ class TestHttp(unittest.TestCase): None, None, None, + None, PlusApi(), ) mock_stats.return_value = self.test_stats diff --git a/frigate/timeline.py b/frigate/timeline.py index 5c3c11f3d..c351e3e68 100644 --- a/frigate/timeline.py +++ b/frigate/timeline.py @@ -12,6 +12,8 @@ from frigate.models import Timeline from multiprocessing.queues import Queue from multiprocessing.synchronize import Event as MpEvent +from frigate.util import to_relative_box + logger = logging.getLogger(__name__) @@ -64,77 +66,36 @@ class TimelineProcessor(threading.Thread): """Handle object detection.""" camera_config = self.config.cameras[camera] + timeline_entry = { + Timeline.timestamp: event_data["frame_time"], + Timeline.camera: camera, + Timeline.source: "tracked_object", + Timeline.source_id: event_data["id"], + Timeline.data: { + "box": to_relative_box( + camera_config.detect.width, + camera_config.detect.height, + event_data["box"], + ), + "label": event_data["label"], + "region": to_relative_box( + camera_config.detect.width, + camera_config.detect.height, + event_data["region"], + ), + }, + } if event_type == "start": - Timeline.insert( - timestamp=event_data["frame_time"], - camera=camera, - source="tracked_object", - source_id=event_data["id"], - class_type="visible", - data={ - "box": [ - event_data["box"][0] / camera_config.detect.width, - event_data["box"][1] / camera_config.detect.height, - event_data["box"][2] / camera_config.detect.width, - event_data["box"][3] / camera_config.detect.height, - ], - "label": event_data["label"], - "region": [ - event_data["region"][0] / camera_config.detect.width, - event_data["region"][1] / camera_config.detect.height, - event_data["region"][2] / camera_config.detect.width, - event_data["region"][3] / camera_config.detect.height, - ], - }, - ).execute() + timeline_entry[Timeline.class_type] = "visible" + Timeline.insert(timeline_entry).execute() elif ( event_type == "update" and prev_event_data["current_zones"] != event_data["current_zones"] and len(event_data["current_zones"]) > 0 ): - Timeline.insert( - timestamp=event_data["frame_time"], - camera=camera, - source="tracked_object", - source_id=event_data["id"], - class_type="entered_zone", - data={ - "box": [ - event_data["box"][0] / camera_config.detect.width, - event_data["box"][1] / camera_config.detect.height, - event_data["box"][2] / camera_config.detect.width, - event_data["box"][3] / camera_config.detect.height, - ], - "label": event_data["label"], - "region": [ - event_data["region"][0] / camera_config.detect.width, - event_data["region"][1] / camera_config.detect.height, - event_data["region"][2] / camera_config.detect.width, - event_data["region"][3] / camera_config.detect.height, - ], - "zones": event_data["current_zones"], - }, - ).execute() + timeline_entry[Timeline.class_type] = "entered_zone" + timeline_entry[Timeline.data]["zones"] = event_data["current_zones"] + Timeline.insert(timeline_entry).execute() elif event_type == "end": - Timeline.insert( - timestamp=event_data["frame_time"], - camera=camera, - source="tracked_object", - source_id=event_data["id"], - class_type="gone", - data={ - "box": [ - event_data["box"][0] / camera_config.detect.width, - event_data["box"][1] / camera_config.detect.height, - event_data["box"][2] / camera_config.detect.width, - event_data["box"][3] / camera_config.detect.height, - ], - "label": event_data["label"], - "region": [ - event_data["region"][0] / camera_config.detect.width, - event_data["region"][1] / camera_config.detect.height, - event_data["region"][2] / camera_config.detect.width, - event_data["region"][3] / camera_config.detect.height, - ], - }, - ).execute() + timeline_entry[Timeline.class_type] = "gone" + Timeline.insert(timeline_entry).execute() diff --git a/frigate/util.py b/frigate/util.py index 887baf8de..12139bf99 100755 --- a/frigate/util.py +++ b/frigate/util.py @@ -1065,3 +1065,14 @@ def get_tz_modifiers(tz_name: str) -> Tuple[str, str]: hour_modifier = f"{hours_offset} hour" minute_modifier = f"{minutes_offset} minute" return hour_modifier, minute_modifier + + +def to_relative_box( + width: int, height: int, box: Tuple[int, int, int, int] +) -> Tuple[int, int, int, int]: + return ( + box[0] / width, # x + box[1] / height, # y + (box[2] - box[0]) / width, # w + (box[3] - box[1]) / height, # h + ) diff --git a/migrations/014_event_updates_for_fp.py b/migrations/014_event_updates_for_fp.py new file mode 100644 index 000000000..5d075596b --- /dev/null +++ b/migrations/014_event_updates_for_fp.py @@ -0,0 +1,52 @@ +"""Peewee migrations + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.python(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import datetime as dt +import peewee as pw +from playhouse.sqlite_ext import * +from decimal import ROUND_HALF_EVEN +from frigate.models import Event + +try: + import playhouse.postgres_ext as pw_pext +except ImportError: + pass + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + score=pw.FloatField(null=True), + model_hash=pw.CharField(max_length=32, null=True), + detector_type=pw.CharField(max_length=32, null=True), + model_type=pw.CharField(max_length=32, null=True), + ) + + migrator.drop_not_null(Event, "area", "false_positive") + migrator.add_default(Event, "false_positive", 0) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/requirements-wheels.txt b/requirements-wheels.txt index b19c7947f..e8e92408b 100644 --- a/requirements-wheels.txt +++ b/requirements-wheels.txt @@ -1,18 +1,19 @@ click == 8.1.* Flask == 2.2.* imutils == 0.5.* -matplotlib == 3.6.* +matplotlib == 3.7.* mypy == 0.942 numpy == 1.23.* +onvif_zeep == 0.2.12 opencv-python-headless == 4.5.5.* paho-mqtt == 1.6.* peewee == 3.15.* -peewee_migrate == 1.6.* +peewee_migrate == 1.7.* psutil == 5.9.* pydantic == 1.10.* PyYAML == 6.0 pytz == 2023.3 -tzlocal == 4.2 +tzlocal == 4.3 types-PyYAML == 6.0.* requests == 2.28.* types-requests == 2.28.* diff --git a/requirements.txt b/requirements.txt index ac8407b6f..90780e2b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -scikit-build == 0.17.1 +scikit-build == 0.17.* nvidia-pyindex diff --git a/web/package-lock.json b/web/package-lock.json index 9a5952935..d3a42cb90 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "@cycjimmy/jsmpeg-player": "^6.0.5", - "axios": "^1.3.5", + "axios": "^1.3.6", "copy-to-clipboard": "3.3.3", "date-fns": "^2.29.3", "idb-keyval": "^6.2.0", @@ -32,23 +32,23 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/preact": "^3.2.3", "@testing-library/user-event": "^14.4.3", - "@typescript-eslint/eslint-plugin": "^5.58.0", - "@typescript-eslint/parser": "^5.58.0", + "@typescript-eslint/eslint-plugin": "^5.59.1", + "@typescript-eslint/parser": "^5.59.1", "@vitest/coverage-c8": "^0.30.1", "@vitest/ui": "^0.30.1", "autoprefixer": "^10.4.14", - "eslint": "^8.38.0", + "eslint": "^8.39.0", "eslint-config-preact": "^1.3.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-vitest-globals": "^1.3.1", "fake-indexeddb": "^4.0.1", "jsdom": "^21.1.1", "msw": "^1.2.1", - "postcss": "^8.4.19", - "prettier": "^2.8.7", - "tailwindcss": "^3.3.1", + "postcss": "^8.4.23", + "prettier": "^2.8.8", + "tailwindcss": "^3.3.2", "typescript": "^5.0.4", - "vite": "^4.2.1", + "vite": "^4.3.2", "vitest": "^0.30.1" } }, @@ -58,6 +58,18 @@ "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", "dev": true }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -946,9 +958,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz", - "integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1775,15 +1787,15 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.58.0.tgz", - "integrity": "sha512-vxHvLhH0qgBd3/tW6/VccptSfc8FxPQIkmNTVLWcCOVqSBvqpnKkBTYrhcGlXfSnd78azwe+PsjYFj0X34/njA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", + "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/type-utils": "5.58.0", - "@typescript-eslint/utils": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/type-utils": "5.59.1", + "@typescript-eslint/utils": "5.59.1", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -1809,13 +1821,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", - "integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0" + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1826,9 +1838,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", - "integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1839,13 +1851,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", - "integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1866,17 +1878,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz", - "integrity": "sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", + "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -1892,12 +1904,12 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", - "integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", + "@typescript-eslint/types": "5.59.1", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -1931,9 +1943,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1965,14 +1977,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.58.0.tgz", - "integrity": "sha512-ixaM3gRtlfrKzP8N6lRhBbjTow1t6ztfBvQNGuRM8qH1bjFFXIJ35XY+FC0RRBKn3C6cT+7VW1y8tNm7DwPHDQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", + "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "debug": "^4.3.4" }, "engines": { @@ -1992,13 +2004,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", - "integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0" + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2009,9 +2021,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", - "integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2022,13 +2034,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", - "integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2049,12 +2061,12 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", - "integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", + "@typescript-eslint/types": "5.59.1", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -2066,9 +2078,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2098,13 +2110,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.58.0.tgz", - "integrity": "sha512-FF5vP/SKAFJ+LmR9PENql7fQVVgGDOS+dq3j+cKl9iW/9VuZC/8CFmzIP0DLKXfWKpRHawJiG70rVH+xZZbp8w==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", + "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.58.0", - "@typescript-eslint/utils": "5.58.0", + "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/utils": "5.59.1", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -2125,13 +2137,13 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", - "integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0" + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2142,9 +2154,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", - "integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2155,13 +2167,13 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", - "integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2182,17 +2194,17 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz", - "integrity": "sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", + "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -2208,12 +2220,12 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", - "integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", + "@typescript-eslint/types": "5.59.1", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -2247,9 +2259,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2886,9 +2898,9 @@ } }, "node_modules/axios": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz", - "integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz", + "integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -4014,15 +4026,15 @@ } }, "node_modules/eslint": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz", - "integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.38.0", + "@eslint/js": "8.39.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -4032,7 +4044,7 @@ "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", + "eslint-scope": "^7.2.0", "eslint-visitor-keys": "^3.4.0", "espree": "^9.5.1", "esquery": "^1.4.2", @@ -4239,9 +4251,9 @@ "dev": true }, "node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -4249,6 +4261,9 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-utils": { @@ -6468,9 +6483,9 @@ } }, "node_modules/lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true, "engines": { "node": ">=10" @@ -6866,14 +6881,6 @@ "monaco-editor": ">=0.30" } }, - "node_modules/monaco-yaml/node_modules/yaml": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz", - "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==", - "engines": { - "node": ">= 14" - } - }, "node_modules/mpd-parser": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.1.1.tgz", @@ -7073,10 +7080,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -7615,9 +7628,9 @@ } }, "node_modules/postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", + "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", "dev": true, "funding": [ { @@ -7627,10 +7640,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -7639,9 +7656,9 @@ } }, "node_modules/postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, "dependencies": { "postcss-value-parser": "^4.0.0", @@ -7649,16 +7666,16 @@ "resolve": "^1.1.7" }, "engines": { - "node": ">=10.0.0" + "node": ">=14.0.0" }, "peerDependencies": { "postcss": "^8.0.0" } }, "node_modules/postcss-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", - "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "dev": true, "dependencies": { "camelcase-css": "^2.0.1" @@ -7671,20 +7688,20 @@ "url": "https://opencollective.com/postcss/" }, "peerDependencies": { - "postcss": "^8.3.3" + "postcss": "^8.4.21" } }, "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", "dev": true, "dependencies": { "lilconfig": "^2.0.5", - "yaml": "^1.10.2" + "yaml": "^2.1.1" }, "engines": { - "node": ">= 10" + "node": ">= 14" }, "funding": { "type": "opencollective", @@ -7704,12 +7721,12 @@ } }, "node_modules/postcss-nested": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", - "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", "dev": true, "dependencies": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^6.0.11" }, "engines": { "node": ">=12.0" @@ -7773,9 +7790,9 @@ } }, "node_modules/prettier": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "bin": { "prettier": "bin-prettier.js" }, @@ -7919,18 +7936,6 @@ } ] }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/react": { "name": "@preact/compat", "version": "17.1.2", @@ -8070,12 +8075,12 @@ "dev": true }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -8134,9 +8139,9 @@ } }, "node_modules/rollup": { - "version": "3.20.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz", - "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.0.tgz", + "integrity": "sha512-ANPhVcyeHvYdQMUyCbczy33nbLzI7RzrBje4uvNiTDJGIMtlKoOStmympwr9OtS1LZxiDmE2wvxHyVhoLtf1KQ==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -8648,53 +8653,43 @@ } }, "node_modules/tailwindcss": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz", - "integrity": "sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz", + "integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==", "dev": true, "dependencies": { + "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", - "color-name": "^1.1.4", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.17.2", - "lilconfig": "^2.0.6", + "jiti": "^1.18.2", + "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", - "postcss": "^8.0.9", - "postcss-import": "^14.1.0", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "6.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.1", - "sucrase": "^3.29.0" + "resolve": "^1.22.2", + "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" }, "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "postcss": "^8.0.9" + "node": ">=14.0.0" } }, - "node_modules/tailwindcss/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -9195,15 +9190,14 @@ } }, "node_modules/vite": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz", - "integrity": "sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==", "dev": true, "dependencies": { "esbuild": "^0.17.5", "postcss": "^8.4.21", - "resolve": "^1.22.1", - "rollup": "^3.18.0" + "rollup": "^3.21.0" }, "bin": { "vite": "bin/vite.js" @@ -9655,12 +9649,11 @@ "dev": true }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yargs": { @@ -9710,6 +9703,12 @@ "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", "dev": true }, + "@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true + }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -10254,9 +10253,9 @@ } }, "@eslint/js": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz", - "integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", "dev": true }, "@humanwhocodes/config-array": { @@ -10930,15 +10929,15 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.58.0.tgz", - "integrity": "sha512-vxHvLhH0qgBd3/tW6/VccptSfc8FxPQIkmNTVLWcCOVqSBvqpnKkBTYrhcGlXfSnd78azwe+PsjYFj0X34/njA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", + "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/type-utils": "5.58.0", - "@typescript-eslint/utils": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/type-utils": "5.59.1", + "@typescript-eslint/utils": "5.59.1", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -10948,29 +10947,29 @@ }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", - "integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0" + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" } }, "@typescript-eslint/types": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", - "integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", - "integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -10979,28 +10978,28 @@ } }, "@typescript-eslint/utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz", - "integrity": "sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", + "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", - "integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", + "@typescript-eslint/types": "5.59.1", "eslint-visitor-keys": "^3.3.0" } }, @@ -11021,9 +11020,9 @@ "dev": true }, "semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -11041,41 +11040,41 @@ } }, "@typescript-eslint/parser": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.58.0.tgz", - "integrity": "sha512-ixaM3gRtlfrKzP8N6lRhBbjTow1t6ztfBvQNGuRM8qH1bjFFXIJ35XY+FC0RRBKn3C6cT+7VW1y8tNm7DwPHDQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", + "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "debug": "^4.3.4" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", - "integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0" + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" } }, "@typescript-eslint/types": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", - "integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", - "integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -11084,19 +11083,19 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", - "integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", + "@typescript-eslint/types": "5.59.1", "eslint-visitor-keys": "^3.3.0" } }, "semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -11115,41 +11114,41 @@ } }, "@typescript-eslint/type-utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.58.0.tgz", - "integrity": "sha512-FF5vP/SKAFJ+LmR9PENql7fQVVgGDOS+dq3j+cKl9iW/9VuZC/8CFmzIP0DLKXfWKpRHawJiG70rVH+xZZbp8w==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", + "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.58.0", - "@typescript-eslint/utils": "5.58.0", + "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/utils": "5.59.1", "debug": "^4.3.4", "tsutils": "^3.21.0" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", - "integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0" + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" } }, "@typescript-eslint/types": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", - "integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", - "integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -11158,28 +11157,28 @@ } }, "@typescript-eslint/utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz", - "integrity": "sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", + "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", - "integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", + "@typescript-eslint/types": "5.59.1", "eslint-visitor-keys": "^3.3.0" } }, @@ -11200,9 +11199,9 @@ "dev": true }, "semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -11675,9 +11674,9 @@ "dev": true }, "axios": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz", - "integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz", + "integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==", "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -12531,15 +12530,15 @@ } }, "eslint": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz", - "integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.38.0", + "@eslint/js": "8.39.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -12549,7 +12548,7 @@ "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", + "eslint-scope": "^7.2.0", "eslint-visitor-keys": "^3.4.0", "espree": "^9.5.1", "esquery": "^1.4.2", @@ -12768,9 +12767,9 @@ "dev": true }, "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -14333,9 +14332,9 @@ } }, "lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true }, "lines-and-columns": { @@ -14629,13 +14628,6 @@ "vscode-languageserver-types": "^3.0.0", "vscode-uri": "^3.0.0", "yaml": "^2.0.0" - }, - "dependencies": { - "yaml": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz", - "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==" - } } }, "mpd-parser": { @@ -14784,9 +14776,9 @@ } }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "dev": true }, "natural-compare": { @@ -15176,20 +15168,20 @@ } }, "postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", + "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", "dev": true, "requires": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, "requires": { "postcss-value-parser": "^4.0.0", @@ -15198,31 +15190,31 @@ } }, "postcss-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", - "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "dev": true, "requires": { "camelcase-css": "^2.0.1" } }, "postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", "dev": true, "requires": { "lilconfig": "^2.0.5", - "yaml": "^1.10.2" + "yaml": "^2.1.1" } }, "postcss-nested": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", - "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", "dev": true, "requires": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^6.0.11" } }, "postcss-selector-parser": { @@ -15264,9 +15256,9 @@ "dev": true }, "prettier": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==" + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==" }, "pretty-format": { "version": "27.5.1", @@ -15368,12 +15360,6 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, - "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true - }, "react": { "version": "npm:@preact/compat@17.1.2", "resolved": "https://registry.npmjs.org/@preact/compat/-/compat-17.1.2.tgz", @@ -15494,12 +15480,12 @@ "dev": true }, "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "requires": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -15536,9 +15522,9 @@ } }, "rollup": { - "version": "3.20.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz", - "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.0.tgz", + "integrity": "sha512-ANPhVcyeHvYdQMUyCbczy33nbLzI7RzrBje4uvNiTDJGIMtlKoOStmympwr9OtS1LZxiDmE2wvxHyVhoLtf1KQ==", "dev": true, "requires": { "fsevents": "~2.3.2" @@ -15934,43 +15920,34 @@ } }, "tailwindcss": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz", - "integrity": "sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz", + "integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==", "dev": true, "requires": { + "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", - "color-name": "^1.1.4", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.17.2", - "lilconfig": "^2.0.6", + "jiti": "^1.18.2", + "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", - "postcss": "^8.0.9", - "postcss-import": "^14.1.0", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "6.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.1", - "sucrase": "^3.29.0" - }, - "dependencies": { - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } + "resolve": "^1.22.2", + "sucrase": "^3.32.0" } }, "test-exclude": { @@ -16371,16 +16348,15 @@ } }, "vite": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz", - "integrity": "sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==", "dev": true, "requires": { "esbuild": "^0.17.5", "fsevents": "~2.3.2", "postcss": "^8.4.21", - "resolve": "^1.22.1", - "rollup": "^3.18.0" + "rollup": "^3.21.0" } }, "vite-node": { @@ -16664,10 +16640,9 @@ "dev": true }, "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==" }, "yargs": { "version": "17.6.2", diff --git a/web/package.json b/web/package.json index 91292a6aa..cd32ce888 100644 --- a/web/package.json +++ b/web/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@cycjimmy/jsmpeg-player": "^6.0.5", - "axios": "^1.3.5", + "axios": "^1.3.6", "copy-to-clipboard": "3.3.3", "date-fns": "^2.29.3", "idb-keyval": "^6.2.0", @@ -36,23 +36,23 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/preact": "^3.2.3", "@testing-library/user-event": "^14.4.3", - "@typescript-eslint/eslint-plugin": "^5.58.0", - "@typescript-eslint/parser": "^5.58.0", + "@typescript-eslint/eslint-plugin": "^5.59.1", + "@typescript-eslint/parser": "^5.59.1", "@vitest/coverage-c8": "^0.30.1", "@vitest/ui": "^0.30.1", "autoprefixer": "^10.4.14", - "eslint": "^8.38.0", + "eslint": "^8.39.0", "eslint-config-preact": "^1.3.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-vitest-globals": "^1.3.1", "fake-indexeddb": "^4.0.1", "jsdom": "^21.1.1", "msw": "^1.2.1", - "postcss": "^8.4.19", - "prettier": "^2.8.7", - "tailwindcss": "^3.3.1", + "postcss": "^8.4.23", + "prettier": "^2.8.8", + "tailwindcss": "^3.3.2", "typescript": "^5.0.4", - "vite": "^4.2.1", + "vite": "^4.3.2", "vitest": "^0.30.1" } } diff --git a/web/src/api/ws.jsx b/web/src/api/ws.jsx index 734200215..8995a065b 100644 --- a/web/src/api/ws.jsx +++ b/web/src/api/ws.jsx @@ -120,6 +120,15 @@ export function useSnapshotsState(camera) { return { payload, send, connected }; } +export function usePtzCommand(camera) { + const { + value: { payload }, + send, + connected, + } = useWs(`${camera}/ptz`, `${camera}/ptz`); + return { payload, send, connected }; +} + export function useRestart() { const { value: { payload }, diff --git a/web/src/components/CameraControlPanel.jsx b/web/src/components/CameraControlPanel.jsx new file mode 100644 index 000000000..90bf3ef27 --- /dev/null +++ b/web/src/components/CameraControlPanel.jsx @@ -0,0 +1,248 @@ +import { h } from 'preact'; +import { useState } from 'preact/hooks'; +import useSWR from 'swr'; +import { usePtzCommand } from '../api/ws'; +import ActivityIndicator from './ActivityIndicator'; +import ArrowRightDouble from '../icons/ArrowRightDouble'; +import ArrowUpDouble from '../icons/ArrowUpDouble'; +import ArrowDownDouble from '../icons/ArrowDownDouble'; +import ArrowLeftDouble from '../icons/ArrowLeftDouble'; +import Button from './Button'; +import Heading from './Heading'; + +export default function CameraControlPanel({ camera = '' }) { + const { data: ptz } = useSWR(`${camera}/ptz/info`); + const [currentPreset, setCurrentPreset] = useState(''); + + const { payload: _, send: sendPtz } = usePtzCommand(camera); + + const onSetPreview = async (e) => { + e.stopPropagation(); + + if (currentPreset == 'none') { + return; + } + + sendPtz(`preset-${currentPreset}`); + setCurrentPreset(''); + }; + + const onSetMove = async (e, dir) => { + e.stopPropagation(); + sendPtz(`MOVE_${dir}`); + setCurrentPreset(''); + }; + + const onSetZoom = async (e, dir) => { + e.stopPropagation(); + sendPtz(`ZOOM_${dir}`); + setCurrentPreset(''); + }; + + const onSetStop = async (e) => { + e.stopPropagation(); + sendPtz('STOP'); + }; + + if (!ptz) { + return ; + } + + document.addEventListener('keydown', (e) => { + if (!e) { + return; + } + + if (e.repeat) { + e.preventDefault(); + return; + } + + if (ptz.features.includes('pt')) { + if (e.key === 'ArrowLeft') { + e.preventDefault(); + onSetMove(e, 'LEFT'); + } else if (e.key === 'ArrowRight') { + e.preventDefault(); + onSetMove(e, 'RIGHT'); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + onSetMove(e, 'UP'); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + onSetMove(e, 'DOWN'); + } + + if (ptz.features.includes('zoom')) { + if (e.key == '+') { + e.preventDefault(); + onSetZoom(e, 'IN'); + } else if (e.key == '-') { + e.preventDefault(); + onSetZoom(e, 'OUT'); + } + } + } + }); + + document.addEventListener('keyup', (e) => { + if (!e || e.repeat) { + return; + } + + if ( + e.key === 'ArrowLeft' || + e.key === 'ArrowRight' || + e.key === 'ArrowUp' || + e.key === 'ArrowDown' || + e.key === '+' || + e.key === '-' + ) { + e.preventDefault(); + onSetStop(e); + } + }); + + return ( +
+ {ptz.features.includes('pt') && ( +
+
+ + Pan / Tilt + +
+ +
+
+ + +
+
+ +
+
+
+ )} + + {ptz.features.includes('zoom') && ( +
+ + Zoom + +
+ +
+
+
+ +
+
+ )} + + {(ptz.presets || []).length > 0 && ( +
+ + Presets + +
+ +
+ + +
+ )} +
+ ); +} diff --git a/web/src/components/TimelineSummary.jsx b/web/src/components/TimelineSummary.jsx index edad60255..8e5af9654 100644 --- a/web/src/components/TimelineSummary.jsx +++ b/web/src/components/TimelineSummary.jsx @@ -20,15 +20,50 @@ export default function TimelineSummary({ event, onFrameSelected }) { const [timeIndex, setTimeIndex] = useState(-1); + const recordingParams = { + before: event.end_time || Date.now(), + after: event.start_time, + }; + const { data: recordings } = useSWR([`${event.camera}/recordings`, recordingParams], { revalidateOnFocus: false }); + + // calculates the seek seconds by adding up all the seconds in the segments prior to the playback time + const getSeekSeconds = (seekUnix) => { + if (!recordings) { + return 0; + } + + let seekSeconds = 0; + recordings.every((segment) => { + // if the next segment is past the desired time, stop calculating + if (segment.start_time > seekUnix) { + return false; + } + + if (segment.end_time < seekUnix) { + seekSeconds += segment.end_time - segment.start_time; + return true; + } + + seekSeconds += segment.end_time - segment.start_time - (segment.end_time - seekUnix); + return true; + }); + + return seekSeconds; + }; + const onSelectMoment = async (index) => { setTimeIndex(index); - onFrameSelected(eventTimeline[index]); + onFrameSelected(eventTimeline[index], getSeekSeconds(eventTimeline[index].timestamp)); }; if (!eventTimeline || !config) { return ; } + if (eventTimeline.length == 0) { + return
; + } + return (
diff --git a/web/src/icons/ArrowDownDouble.jsx b/web/src/icons/ArrowDownDouble.jsx new file mode 100644 index 000000000..7685542e9 --- /dev/null +++ b/web/src/icons/ArrowDownDouble.jsx @@ -0,0 +1,19 @@ +import { h } from 'preact'; +import { memo } from 'preact/compat'; + +export function ArrowDownDouble({ className = '' }) { + return ( + + + + ); +} + +export default memo(ArrowDownDouble); diff --git a/web/src/icons/ArrowLeftDouble.jsx b/web/src/icons/ArrowLeftDouble.jsx new file mode 100644 index 000000000..eaa25395c --- /dev/null +++ b/web/src/icons/ArrowLeftDouble.jsx @@ -0,0 +1,19 @@ +import { h } from 'preact'; +import { memo } from 'preact/compat'; + +export function ArrowLeftDouble({ className = '' }) { + return ( + + + + ); +} + +export default memo(ArrowLeftDouble); diff --git a/web/src/icons/ArrowRightDouble.jsx b/web/src/icons/ArrowRightDouble.jsx index 7487a4d5c..fc1960836 100644 --- a/web/src/icons/ArrowRightDouble.jsx +++ b/web/src/icons/ArrowRightDouble.jsx @@ -3,8 +3,15 @@ import { memo } from 'preact/compat'; export function ArrowRightDouble({ className = '' }) { return ( - - + + ); } diff --git a/web/src/icons/ArrowUpDouble.jsx b/web/src/icons/ArrowUpDouble.jsx new file mode 100644 index 000000000..7468a2b91 --- /dev/null +++ b/web/src/icons/ArrowUpDouble.jsx @@ -0,0 +1,19 @@ +import { h } from 'preact'; +import { memo } from 'preact/compat'; + +export function ArrowUpDouble({ className = '' }) { + return ( + + + + ); +} + +export default memo(ArrowUpDouble); diff --git a/web/src/routes/Birdseye.jsx b/web/src/routes/Birdseye.jsx index 91c97fcb9..c23713bb9 100644 --- a/web/src/routes/Birdseye.jsx +++ b/web/src/routes/Birdseye.jsx @@ -6,6 +6,8 @@ import Heading from '../components/Heading'; import WebRtcPlayer from '../components/WebRtcPlayer'; import MsePlayer from '../components/MsePlayer'; import useSWR from 'swr'; +import { useMemo } from 'preact/hooks'; +import CameraControlPanel from '../components/CameraControlPanel'; export default function Birdseye() { const { data: config } = useSWR('config'); @@ -16,6 +18,16 @@ export default function Birdseye() { ); const sourceValues = ['mse', 'webrtc', 'jsmpeg']; + const ptzCameras = useMemo(() => { + if (!config) { + return []; + } + + return Object.entries(config.cameras) + .filter(([_, conf]) => conf.onvif?.host) + .map(([_, camera]) => camera.name); + }, [config]); + if (!config || !sourceIsLoaded) { return ; } @@ -25,7 +37,7 @@ export default function Birdseye() { if ('MediaSource' in window) { player = ( -
+
@@ -42,7 +54,7 @@ export default function Birdseye() { } else if (viewSource == 'webrtc' && config.birdseye.restream) { player = ( -
+
@@ -50,7 +62,7 @@ export default function Birdseye() { } else { player = ( -
+
@@ -79,7 +91,21 @@ export default function Birdseye() { )}
- {player} +
+ {player} + + {ptzCameras && ( +
+ Control Panel + {ptzCameras.map((camera) => ( +
+ {camera.replaceAll('_', ' ')} + +
+ ))} +
+ )} +
); } diff --git a/web/src/routes/Camera.jsx b/web/src/routes/Camera.jsx index 7a50d530a..4a415e32d 100644 --- a/web/src/routes/Camera.jsx +++ b/web/src/routes/Camera.jsx @@ -15,6 +15,7 @@ import { useApiHost } from '../api'; import useSWR from 'swr'; import WebRtcPlayer from '../components/WebRtcPlayer'; import MsePlayer from '../components/MsePlayer'; +import CameraControlPanel from '../components/CameraControlPanel'; const emptyObject = Object.freeze({}); @@ -188,6 +189,13 @@ export default function Camera({ camera }) { {player} + {cameraConfig?.onvif?.host && ( +
+ Control Panel + +
+ )} +
Tracked objects
diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx index c676cb108..af0af5b67 100644 --- a/web/src/routes/Events.jsx +++ b/web/src/routes/Events.jsx @@ -3,6 +3,7 @@ import { route } from 'preact-router'; import ActivityIndicator from '../components/ActivityIndicator'; import Heading from '../components/Heading'; import { Tabs, TextTab } from '../components/Tabs'; +import Link from '../components/Link'; import { useApiHost } from '../api'; import useSWR from 'swr'; import useSWRInfinite from 'swr/infinite'; @@ -57,7 +58,12 @@ export default function Events({ path, ...props }) { showDownloadMenu: false, showDatePicker: false, showCalendar: false, - showPlusConfig: false, + showPlusSubmit: false, + }); + const [plusSubmitEvent, setPlusSubmitEvent] = useState({ + id: null, + label: null, + validBox: null, }); const [uploading, setUploading] = useState([]); const [viewEvent, setViewEvent] = useState(); @@ -65,6 +71,8 @@ export default function Events({ path, ...props }) { const [eventDetailType, setEventDetailType] = useState('clip'); const [downloadEvent, setDownloadEvent] = useState({ id: null, + label: null, + box: null, has_clip: false, has_snapshot: false, plus_id: undefined, @@ -182,14 +190,10 @@ export default function Events({ path, ...props }) { onFilter(name, items); }; - const onEventFrameSelected = (event, frame) => { - const eventDuration = event.end_time - event.start_time; - + const onEventFrameSelected = (event, frame, seekSeconds) => { if (this.player) { this.player.pause(); - const videoOffset = this.player.duration() - eventDuration; - const startTime = videoOffset + (frame.timestamp - event.start_time); - this.player.currentTime(startTime); + this.player.currentTime(seekSeconds); setEventOverlay(frame); } }; @@ -202,6 +206,8 @@ export default function Events({ path, ...props }) { e.stopPropagation(); setDownloadEvent((_prev) => ({ id: event.id, + box: event.box, + label: event.label, has_clip: event.has_clip, has_snapshot: event.has_snapshot, plus_id: event.plus_id, @@ -211,6 +217,16 @@ export default function Events({ path, ...props }) { setState({ ...state, showDownloadMenu: true }); }; + const showSubmitToPlus = (event_id, label, box, e) => { + if (e) { + e.stopPropagation(); + } + // if any of the box coordinates are > 1, then the box data is from an older version + // and not valid to submit to plus with the snapshot image + setPlusSubmitEvent({ id: event_id, label, validBox: !box.some((d) => d > 1) }); + setState({ ...state, showDownloadMenu: false, showPlusSubmit: true }); + }; + const handleSelectDateRange = useCallback( (dates) => { setSearchParams({ ...searchParams, before: dates.before, after: dates.after }); @@ -255,23 +271,16 @@ export default function Events({ path, ...props }) { [size, setSize, isValidating, isDone] ); - const onSendToPlus = async (id, e) => { - if (e) { - e.stopPropagation(); - } - + const onSendToPlus = async (id, false_positive, validBox) => { if (uploading.includes(id)) { return; } - if (!config.plus.enabled) { - setState({ ...state, showDownloadMenu: false, showPlusConfig: true }); - return; - } - setUploading((prev) => [...prev, id]); - const response = await axios.post(`events/${id}/plus`); + const response = false_positive + ? await axios.put(`events/${id}/false_positive`) + : await axios.post(`events/${id}/plus`, validBox ? { include_annotation: 1 } : {}); if (response.status === 200) { mutate( @@ -293,6 +302,8 @@ export default function Events({ path, ...props }) { if (state.showDownloadMenu && downloadEvent.id === id) { setState({ ...state, showDownloadMenu: false }); } + + setState({ ...state, showPlusSubmit: false }); }; const handleEventDetailTabChange = (index) => { @@ -379,12 +390,12 @@ export default function Events({ path, ...props }) { download /> )} - {(downloadEvent.end_time && downloadEvent.has_snapshot && !downloadEvent.plus_id) && ( + {downloadEvent.end_time && downloadEvent.has_snapshot && !downloadEvent.plus_id && ( onSendToPlus(downloadEvent.id)} + onSelect={() => showSubmitToPlus(downloadEvent.id, downloadEvent.label, downloadEvent.box)} /> )} {downloadEvent.plus_id && ( @@ -439,25 +450,96 @@ export default function Events({ path, ...props }) { /> )} - {state.showPlusConfig && ( + {state.showPlusSubmit && ( -
- Setup a Frigate+ Account -

In order to submit images to Frigate+, you first need to setup an account.

- - https://plus.frigate.video - -
-
- -
+ {config.plus.enabled ? ( + <> +
+ Submit to Frigate+ + + {`${plusSubmitEvent.label}`} + + {plusSubmitEvent.validBox ? ( +

+ Objects in locations you want to avoid are not false positives. Submitting them as false positives + will confuse the model. +

+ ) : ( +

+ Events prior to version 0.13 can only be submitted to Frigate+ without annotations. +

+ )} +
+ {plusSubmitEvent.validBox ? ( +
+ + + +
+ ) : ( +
+ + +
+ )} + + ) : ( + <> +
+ Setup a Frigate+ Account +

In order to submit images to Frigate+, you first need to setup an account.

+ + https://plus.frigate.video + +
+
+ +
+ + )}
)} {deleteFavoriteState.showDeleteFavorite && ( @@ -543,12 +625,20 @@ export default function Events({ path, ...props }) { {event.end_time && event.has_snapshot && ( {event.plus_id ? ( -
Sent to Frigate+
+
+ + Edit in Frigate+ + +
) : ( @@ -590,7 +680,7 @@ export default function Events({ path, ...props }) {
onEventFrameSelected(event, frame)} + onFrameSelected={(frame, seekSeconds) => onEventFrameSelected(event, frame, seekSeconds)} />
{eventOverlay.class_type == 'entered_zone' ? (