Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4e8c390
feat/ctronics-new-adapter-fix: add new CTronics adapter and test file
romainantes Aug 18, 2026
1ef7a5a
update uv.lock
romainantes Aug 18, 2026
a98223f
fix indentation error
romainantes Aug 18, 2026
9f934d2
fix compil with mypy and format check
romainantes Aug 18, 2026
fb1806b
ignore error for fake test
romainantes Aug 18, 2026
79e0943
fix ruff format
romainantes Aug 18, 2026
925b9e7
feat/ctronics-new-adapter : fix start_zoom_focus and add test
romainantes Sep 11, 2026
0314b4a
feat/ctronics-new-adapter : add test test_real_ctronics_zoom
romainantes Sep 11, 2026
5c15b89
feat/ctronics-new-adapter-fix : set focus_auth for CTronicsCamera
romainantes Sep 11, 2026
2823b0e
feat/ctronics-new-adapter-fix : fix set_manual_focus unclamped values
romainantes Sep 11, 2026
c5b3692
feat/ctronics-new-adapter-fix : add default preset_move_hold_s to 5.0
romainantes Sep 11, 2026
9b334ec
feat/ctronics-new-adapter-fix : When no matching token is found, the …
romainantes Sep 11, 2026
118e01b
feat/ctronics-new-adapter-fix : RTSP paths are now normalized
romainantes Sep 11, 2026
e1f0a28
feat/ctronics-new-adapter-fix : removed unused configurations
romainantes Sep 11, 2026
4019af0
feat/ctronics-new-adapter-fix : Handle CTronics ONVIF focus errors an…
romainantes Sep 11, 2026
16a531a
feat/ctronics-new-adapter-fix : kept ONVIF Imaging as the single focu…
romainantes Sep 11, 2026
517b85b
feat/ctronics-new-adapter-fix : added a preset-token cache initialize…
romainantes Sep 11, 2026
5d87213
feat/ctronics-new-adapter-fix : fix test for test_real_ctronics_prese…
romainantes Sep 11, 2026
687cdfb
feat/ctronics-new-adapter-fix : fix test for test_real_ctronics_prese…
romainantes Sep 11, 2026
487424a
feat/ctronics-new-adapter-fix : Apply PR review comments
Sep 18, 2026
854b792
fix
Sep 18, 2026
702d46c
style: fix ruff suppressions and sync camera api lockfile
MateoLostanlen Oct 8, 2026
1f9d8b2
fix(ctronics): satisfy mypy on optional preset caches
MateoLostanlen Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ The API supports multiple camera adapters through a common abstraction:
* `rtsp` adapter, for RTSP streams
* `url` adapter, for HTTP snapshot URLs
* `rest` adapter, for HTTP APIs needing auth headers or returning a JSON-wrapped image
* `ctronics` adapter, for CTronics cameras exposing an authenticated HTTP snapshot endpoint and optional ONVIF PTZ/focus
* `mock` adapter, for development and tests

Each adapter has its own class and inherits from the same base interface. The system selects the correct implementation at runtime based on the `adapter` field in `credentials.json`. The API routes are the same for all camera types. PTZ cameras use pose and movement, other cameras ignore pose parameters without failing.
Expand Down Expand Up @@ -218,6 +219,8 @@ Below is one generic example for each adapter: `url`, `rtsp`, `reolink` static,

These examples are only for illustration. Real deployments use real URLs and real tokens generated by the alert API. For Linovision/Hikvision, `username`/`password` override the global `CAM_USER`/`CAM_PWD`, `azimuth_offset_deg` aligns camera coordinates to real azimuths, and `snapshot_channel`/`ptz_channel` let you pick the right ISAPI channels.

For CTronics, use `adapter: "ctronics"` with `ip_address` (or the configuration key), optional `username`/`password`, and optionally `port`, `protocol`, `snapshot_path`, `snapshot_command`, and `timeout`. Set `type` to `"ptz"` to enable ONVIF PTZ and focus controls; `onvif_port` defaults to `8080` and can be configured per camera. `onvif_wsdl_dir`, `onvif_profile_token`, `focus_min`, and `focus_max` are also supported for ONVIF setup and focus calibration. The tested default snapshot endpoint is `/tmpfs/snap.jpg?usr=...&pwd=...`; verify the endpoint on other models before deployment.

For the full list of adapters with their required fields, and for adding support for a camera none of them handles, see the [camera adapter guide](pyro_camera_api/pyro_camera_api/camera/adapters/README.md).

---
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies = [
"pyroclient @ git+https://github.com/pyronear/pyro-api.git@main#subdirectory=client",
"pyro_camera_api_client",
"pyro_predictor",
"onvif-zeep>=0.2.12",
]

[project.urls]
Expand Down Expand Up @@ -144,6 +145,7 @@ known-third-party = ["pillow", "tqdm", "onnxruntime", "huggingface_hub"]
".github/**.py" = ["D", "T201", "ANN", "S", "PYI024"]
"tests/**.py" = ["D103", "CPY001", "S101", "T201", "ANN001", "ANN201", "ANN202", "ARG001", "S113"]
"pyro_camera_api/**.py" = ["D", "T201", "S101", "ANN", "BLE001", "S113", "S501", "S404", "S603", "S405", "S314", "E402", "RUF029"]
"pyro_camera_api/tests/**.py" = ["N802", "S105", "S106", "S108"]
"pyroengine/core.py" = ["BLE001"]
"pyroengine/sensors.py" = ["S113", "S501", "ANN"]
"watchdog/**.py" = ["CPY001", "S108", "S310", "S404", "S603", "S607", "BLE001", "LOG015"]
Expand Down
1 change: 1 addition & 0 deletions pyro_camera_api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies = [
"opencv-python-headless>=4.9.0,<5",
"pillow>=10.2.0,<11",
"requests>=2.31.0,<3",
"onvif-zeep>=0.2.12,<1",
"numpy>=1.26.4,<2",
"urllib3>=2.2.1,<3",
"ncnn==1.0.20240410",
Expand Down
12 changes: 10 additions & 2 deletions pyro_camera_api/pyro_camera_api/api/routes_focus.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ def manual_focus(camera_ip: str, position: int):
if not isinstance(cam, FocusMixin):
raise HTTPException(status_code=400, detail="Camera does not support manual focus")

result = cam.set_manual_focus(position)
try:
result = cam.set_manual_focus(position)
except RuntimeError as exc:
logger.warning("Manual focus unavailable for %s: %s", camera_ip, exc)
raise HTTPException(status_code=503, detail="Focus service unavailable") from exc

return {
"status": "manual_focus",
Expand Down Expand Up @@ -92,7 +96,11 @@ def get_focus_status(camera_ip: str):
if not hasattr(cam, "get_focus_level"):
raise HTTPException(status_code=400, detail="Camera does not expose focus status")

data = cam.get_focus_level()
try:
data = cam.get_focus_level()
except RuntimeError as exc:
logger.warning("Focus status unavailable for %s: %s", camera_ip, exc)
raise HTTPException(status_code=503, detail="Focus service unavailable") from exc
if not data:
raise HTTPException(status_code=500, detail="Could not retrieve focus level")

Expand Down
1 change: 1 addition & 0 deletions pyro_camera_api/pyro_camera_api/camera/adapters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ contract is `camera/base.py`, the dispatch `camera/registry.py`, the implementat
| `reolink-823S2`, `reolink-823A16` | `ReolinkCamera` | yes | yes | yes | `type`, `ip_address`, `poses`, `azimuths` |
| `reolink` (any string containing it)| `ReolinkCamera` | yes | yes | yes | same, but see Pitfalls: PTZ speeds fall back to the 823S2 tables |
| `linovision` (alias `hikvision`) | `LinovisionCamera` | yes | yes | yes | `type`, `ip_address`, `poses`, `azimuths` |
| `ctronics` (alias `ctronic`) | `CTronicsCamera` | yes | yes | yes | `type`, `ip_address`, `poses`, `azimuths` |
| `rtsp` | `RTSPCamera` | yes | no | no | `rtsp_url` (**required**) |
| `url` (alias `http`, `https`) | `URLCamera` | yes | no | no | `url` (**required**) with embedded credentials |
| `rest` (alias `api`) | `RestSnapshotCamera` | yes | no | no | `url` (**required**), `headers`, `response`, `json_path`, `encoding` |
Expand Down
3 changes: 2 additions & 1 deletion pyro_camera_api/pyro_camera_api/camera/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# pyro_camera_api/camera/adapters/__init__.py
from .ctronics import CTronicsCamera
from .reolink import ReolinkCamera
from .rest import RestSnapshotCamera
from .rtsp import RTSPCamera
from .url import URLCamera

__all__ = ["RTSPCamera", "ReolinkCamera", "RestSnapshotCamera", "URLCamera"]
__all__ = ["CTronicsCamera", "RTSPCamera", "ReolinkCamera", "RestSnapshotCamera", "URLCamera"]
Loading
Loading