Skip to content

Commit

Permalink
webcam: Made webcams addressable via uuid
Browse files Browse the repository at this point in the history
Signed-off-by: Patrick Schmidt <[email protected]>
  • Loading branch information
Clon1998 committed Sep 21, 2023
1 parent 3d9052d commit 43e2616
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 37 deletions.
39 changes: 31 additions & 8 deletions docs/web_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3745,6 +3745,7 @@ A list of configured webcams:
{
"webcams": [
{
"id": "59b7d256-0db9-47bf-bf5f-b9e4a6f61488",
"name": "testcam3",
"location": "door",
"service": "mjpegstreamer",
Expand All @@ -3762,6 +3763,7 @@ A list of configured webcams:
"source": "config"
},
{
"id": "2986b491-2bc8-4cad-9dae-c5a500ba6511",
"name": "tc2",
"location": "printer",
"service": "mjpegstreamer",
Expand All @@ -3779,6 +3781,7 @@ A list of configured webcams:
"source": "database"
},
{
"id": "cd68e494-b7c4-4ef0-a54e-61b8661bc9b6",
"name": "TestCam",
"location": "printer",
"service": "mjpegstreamer",
Expand All @@ -3804,14 +3807,16 @@ A list of configured webcams:
HTTP request:
```http
GET /server/webcams/item?name=cam_name
GET /server/webcams/item?uuid=cd68e494-b7c4-4ef0-a54e-61b8661bc9b6
```
JSON-RPC request:
```json
{
"jsonrpc": "2.0",
"method": "server.webcams.get_item",
"params": {
"name": "cam_name"
"name": "cam_name",
"uuid": "cd68e494-b7c4-4ef0-a54e-61b8661bc9b6"
},
"id": 4654
}
Expand All @@ -3820,8 +3825,13 @@ JSON-RPC request:
Parameters:

- `name`: The name of the camera to request information for. If the named
camera is not available the request will return with an error. This
parameter must be provided.
camera is not available the request will return with an error. This parameter is optional.

- `uuid`: The id of the camera to request information for. If the
camera is not available the request will return with an error. This parameter is optional.

!!! Warning
For every request either the `name` or `uuid` parameter must be specified. If both parameters are specified, the `name` parameter will be used.

Returns:

Expand All @@ -3830,6 +3840,7 @@ The full configuration for the requested webcam:
```json
{
"webcam": {
"id": "cd68e494-b7c4-4ef0-a54e-61b8661bc9b6",
"name": "TestCam",
"location": "printer",
"service": "mjpegstreamer",
Expand Down Expand Up @@ -3885,8 +3896,10 @@ JSON-RPC request:

Parameters:

- `uuid`: The id of the camera update. This parameter must
be provided to update a cam and must be omitted to add a new one.
- `name`: The name of the camera to add or update. This parameter must
be provided for new entries.
be provided for new entries and can also be used to update exisiting once if the `uuid` os omitted.
- `location`: A description of the webcam location, ie: what the webcam is
observing. The default is `printer` for new entries.
- `icon`: The name of the icon to use for the camera. The default is `mdiWebcam`
Expand Down Expand Up @@ -3954,6 +3967,7 @@ The full configuration for the added/updated webcam:
HTTP request:
```http
DELETE /server/webcams/item?name=cam_name
DELETE /server/webcams/item?uuid=cd68e494-b7c4-4ef0-a54e-61b8661bc9b6
```
JSON-RPC request:
```json
Expand All @@ -3970,8 +3984,12 @@ JSON-RPC request:
Parameters:

- `name`: The name of the camera to delete. If the named camera is not
available the request will return with an error. This parameter must
be provided.
available the request will return with an error. This parameter is optional.
- `uuid`: The id of the camera to delete. If the camera is not
available the request will return with an error. This parameter is optional.

!!! Warning
For every request either the `name` or `uuid` parameter must be specified. If both parameters are specified, the `name` parameter will be used.

Returns:

Expand Down Expand Up @@ -4003,6 +4021,7 @@ reachable.
HTTP request:
```http
POST /server/webcams/test?name=cam_name
POST /server/webcams/test?uuid=cd68e494-b7c4-4ef0-a54e-61b8661bc9b6
```
JSON-RPC request:
```json
Expand All @@ -4019,8 +4038,12 @@ JSON-RPC request:
Parameters:

- `name`: The name of the camera to test. If the named camera is not
available the request will return with an error. This parameter must
be provided.
available the request will return with an error. This parameter is optional.
- `uuid`: The id of the camera to test. If the camera is not
available the request will return with an error. This parameter is optional.

!!! Warning
For every request either the `name` or `uuid` parameter must be specified. If both parameters are specified, the `name` parameter will be used.

Returns: Test results in the following format

Expand Down
90 changes: 61 additions & 29 deletions moonraker/components/webcam.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def __init__(self, config: ConfigHelper) -> None:
for section in prefix_sections:
cam_cfg = config[section]
webcam = WebCam.from_config(cam_cfg)
self.webcams[webcam.name] = webcam
self.webcams[webcam.id] = webcam

self.server.register_endpoint(
"/server/webcams/list", ["GET"], self._handle_webcam_list
Expand All @@ -70,12 +70,12 @@ async def component_init(self) -> None:
self._set_default_host_ip(machine.public_ip)
db: MoonrakerDatabase = self.server.lookup_component("database")
saved_cams: Dict[str, Any] = await db.get_item("webcams", default={})
for cam_data in saved_cams.values():
for uid, cam_data in saved_cams.items():
try:
webcam = WebCam.from_database(self.server, cam_data)
if webcam.name in self.webcams:
webcam = WebCam.from_database(self.server, uid, cam_data)
if webcam.id in self.webcams:
continue
self.webcams[webcam.name] = webcam
self.webcams[webcam.id] = webcam
except Exception:
logging.exception("Failed to process webcam from db")
continue
Expand All @@ -101,7 +101,7 @@ def get_webcams(self) -> Dict[str, WebCam]:
def _list_webcams(self) -> List[Dict[str, Any]]:
return [wc.as_dict() for wc in self.webcams.values()]

async def _find_dbcam_by_uuid(
async def _find_dbcam_by_name(
self, name: str
) -> Tuple[str, Dict[str, Any]]:
db: MoonrakerDatabase = self.server.lookup_component("database")
Expand All @@ -111,21 +111,28 @@ async def _find_dbcam_by_uuid(
if name == cam_data["name"]:
return uid, cam_data
return "", {}
async def _find_dbcam_by_uuid(
self, uid: str
) -> Tuple[str, Dict[str, Any]]:
db: MoonrakerDatabase = self.server.lookup_component("database")
found_cam: Dict[str, Any]
found_cam = await db.get_item("webcams", uid, default={})
if found_cam:
return uid, found_cam
return "", {}

async def _save_cam(self, webcam: WebCam) -> None:
uid, cam_data = await self._find_dbcam_by_uuid(webcam.name)
if not uid:
uid = str(uuid.uuid4())
_, cam_data = await self._find_dbcam_by_uuid(webcam.id)
for mfield, dbfield in CAM_FIELDS.items():
cam_data[dbfield] = getattr(webcam, mfield)
cam_data["location"] = webcam.location
cam_data["rotation"] = webcam.rotation
cam_data["extra_data"] = webcam.extra_data
db: MoonrakerDatabase = self.server.lookup_component("database")
db.insert_item("webcams", uid, cam_data)
db.insert_item("webcams", webcam.id, cam_data)

async def _delete_cam(self, webcam: WebCam) -> None:
uid, cam = await self._find_dbcam_by_uuid(webcam.name)
uid, _ = await self._find_dbcam_by_uuid(webcam.id)
if not uid:
return
db: MoonrakerDatabase = self.server.lookup_component("database")
Expand All @@ -135,35 +142,42 @@ async def _handle_webcam_request(
self, web_request: WebRequest
) -> Dict[str, Any]:
action = web_request.get_action()
name = web_request.get_str("name")
name: Optional[str] = web_request.get_str("name", default=None)
cam_id: str

if name:
cam_id, _ = await self._find_dbcam_by_name(name)
else:
cam_id = web_request.get_str("uuid")

webcam_data: Dict[str, Any] = {}
if action == "GET":
if name not in self.webcams:
raise self.server.error(f"Webcam {name} not found", 404)
webcam_data = self.webcams[name].as_dict()
if not cam_id or cam_id not in self.webcams:
raise self.server.error(f"Webcam {cam_id} not found", 404)
webcam_data = self.webcams[cam_id].as_dict()
elif action == "POST":
webcam = self.webcams.get(name, None)
webcam = self.webcams.get(cam_id, None)
if webcam is not None:
if webcam.source == "config":
raise self.server.error(
f"Cannot overwrite webcam '{name}' sourced from "
f"Cannot overwrite webcam '{cam_id}' sourced from "
"Moonraker configuration"
)
webcam.update(web_request)
else:
webcam = WebCam.from_web_request(self.server, web_request)
self.webcams[name] = webcam
self.webcams[webcam.id] = webcam
webcam_data = webcam.as_dict()
await self._save_cam(webcam)
elif action == "DELETE":
if name not in self.webcams:
raise self.server.error(f"Webcam {name} not found", 404)
elif self.webcams[name].source == "config":
if cam_id not in self.webcams:
raise self.server.error(f"Webcam {cam_id} not found", 404)
elif self.webcams[cam_id].source == "config":
raise self.server.error(
f"Cannot delete webcam '{name}' sourced from "
f"Cannot delete webcam '{cam_id}' sourced from "
"Moonraker configuration"
)
webcam = self.webcams.pop(name)
webcam = self.webcams.pop(cam_id)
webcam_data = webcam.as_dict()
await self._delete_cam(webcam)
if action != "GET":
Expand All @@ -180,13 +194,20 @@ async def _handle_webcam_list(
async def _handle_webcam_test(
self, web_request: WebRequest
) -> Dict[str, Any]:
name = web_request.get_str("name")
if name not in self.webcams:
raise self.server.error(f"Webcam '{name}' not found", 404)
name: Optional[str] = web_request.get_str("name", default=None)
cam_id: str
if name:
cam_id, _ = await self._find_dbcam_by_name(name)
else:
cam_id = web_request.get_str("uuid")


if cam_id not in self.webcams:
raise self.server.error(f"Webcam '{cam_id}' not found", 404)
client: HttpClient = self.server.lookup_component("http_client")
cam = self.webcams[name]
cam = self.webcams[cam_id]
result: Dict[str, Any] = {
"name": name,
"uuid": cam_id,
"snapshot_reachable": False
}
for img_type in ["snapshot", "stream"]:
Expand All @@ -207,6 +228,7 @@ class WebCam:
_default_host: str = "http://127.0.0.1"
def __init__(self, server: Server, **kwargs) -> None:
self._server = server
self.id: str = kwargs["id"]
self.enabled: bool = kwargs["enabled"]
self.icon: str = kwargs["icon"]
self.aspect_ratio: str = kwargs["aspect_ratio"]
Expand Down Expand Up @@ -332,6 +354,8 @@ async def convert_local(self, url: str) -> str:
def update(self, web_request: WebRequest) -> None:
for field in web_request.get_args().keys():
try:
if field == "id":
continue
attr = getattr(self, field)
except AttributeError:
continue
Expand All @@ -354,6 +378,7 @@ def set_default_host(host: str) -> None:
@classmethod
def from_config(cls, config: ConfigHelper) -> WebCam:
webcam: Dict[str, Any] = {}
webcam["id"] = str(uuid.uuid4())
webcam["name"] = config.get_name().split(maxsplit=1)[-1]
webcam["enabled"] = config.getboolean("enabled", True)
webcam["icon"] = config.get("icon", "mdiWebcam")
Expand All @@ -379,6 +404,7 @@ def from_web_request(
cls, server: Server, web_request: WebRequest
) -> WebCam:
webcam: Dict[str, Any] = {}
webcam["id"] = str(uuid.uuid4())
webcam["name"] = web_request.get_str("name")
webcam["enabled"] = web_request.get_boolean("enabled", True)
webcam["icon"] = web_request.get_str("icon", "mdiWebcam")
Expand All @@ -397,8 +423,14 @@ def from_web_request(
return cls(server, **webcam)

@classmethod
def from_database(cls, server: Server, cam_data: Dict[str, Any]) -> WebCam:
def from_database(
cls,
server: Server,
cam_uuid: str,
cam_data: Dict[str, Any]
) -> WebCam:
webcam: Dict[str, Any] = {}
webcam["id"] = cam_uuid
webcam["name"] = str(cam_data["name"])
webcam["enabled"] = bool(cam_data.get("enabled", True))
webcam["icon"] = str(cam_data.get("icon", "mdiWebcam"))
Expand Down

0 comments on commit 43e2616

Please sign in to comment.