diff --git a/axis/vapix/interfaces/param_cgi.py b/axis/vapix/interfaces/param_cgi.py index eb0fdd05..1d756166 100644 --- a/axis/vapix/interfaces/param_cgi.py +++ b/axis/vapix/interfaces/param_cgi.py @@ -14,7 +14,6 @@ ImageParam, Param, PropertyParam, - PTZParam, StreamProfileParam, ) from ..models.port_cgi import GetPortsRequest, ListInputRequest, ListOutputRequest @@ -373,156 +372,15 @@ async def update_ptz(self) -> None: await self.update(PTZ) @property - def ptz_params(self) -> PTZParam: - """Provide property parameters.""" - return PTZParam.decode(self[PTZ].raw) - - @property - def ptz_camera_default(self) -> int: - """PTZ default video channel. - - When camera parameter is omitted in HTTP requests. - """ - return int(self[PTZ]["CameraDefault"]) # type: ignore - - @property - def ptz_number_of_cameras(self) -> int: - """Amount of video channels.""" - return int(self[PTZ]["NbrOfCameras"]) # type: ignore - - @property - def ptz_number_of_serial_ports(self) -> int: - """Amount of serial ports.""" - return int(self[PTZ]["NbrOfSerPorts"]) # type: ignore - - @property - def ptz_limits(self) -> dict: - """PTZ.Limit.L# are populated when a driver is installed on a video channel. - - Index # is the video channel number, starting on 1. - When it is possible to obtain the current position from the driver, - for example the current pan position, it is possible to apply limit restrictions - to the requested operation. For instance, if an absolute pan to position 150 - is requested, but the upper limit is set to 140, the new pan position will be 140. - This is the purpose of all but MinFieldAngle and MaxFieldAngle in this group. - The purpose of those two parameters is to calibrate image centering. - """ - attributes = ( - "MaxBrightness", - "MaxFieldAngle", - "MaxFocus", - "MaxIris", - "MaxPan", - "MaxTilt", - "MaxZoom", - "MinBrightness", - "MinFieldAngle", - "MinFocus", - "MinIris", - "MinPan", - "MinTilt", - "MinZoom", - ) - return self.process_dynamic_group( - self[PTZ], # type: ignore[arg-type] - "Limit.L", - attributes, - range(1, self.ptz_number_of_cameras + 1), - ) - - @property - def ptz_support(self) -> dict: - """PTZ.Support.S# are populated when a driver is installed on a video channel. - - A parameter in the group has the value true if the corresponding capability - is supported by the driver. The index # is the video channel number which starts from 1. - An absolute operation means moving to a certain position, - a relative operation means moving relative to the current position. - Arguments referred to apply to PTZ control. - """ - attributes = ( - "AbsoluteBrightness", - "AbsoluteFocus", - "AbsoluteIris", - "AbsolutePan", - "AbsoluteTilt", - "AbsoluteZoom", - "ActionNotification", - "AreaZoom", - "AutoFocus", - "AutoIrCutFilter", - "AutoIris", - "Auxiliary", - "BackLight", - "ContinuousBrightness", - "ContinuousFocus", - "ContinuousIris", - "ContinuousPan", - "ContinuousTilt", - "ContinuousZoom", - "DevicePreset", - "DigitalZoom", - "GenericHTTP", - "IrCutFilter", - "JoyStickEmulation", - "LensOffset", - "OSDMenu", - "ProportionalSpeed", - "RelativeBrightness", - "RelativeFocus", - "RelativeIris", - "RelativePan", - "RelativeTilt", - "RelativeZoom", - "ServerPreset", - "SpeedCtl", - ) - return self.process_dynamic_group( - self[PTZ], # type: ignore[arg-type] - "Support.S", - attributes, - range(1, self.ptz_number_of_cameras + 1), - ) - - @property - def ptz_various(self) -> dict: - """PTZ.Various.V# are populated when a driver is installed on a video channel. + def ptz_data(self) -> bytes: + """Create a smaller dictionary containing all PTZ information.""" + if PTZ not in self: + return b"" - The index # is the video channel number which starts from 1. - The group consists of several different types of parameters for the video channel. - To distinguish the parameter types, the group is presented as - three different categories below. The Enabled parameters determine - if a specific feature can be controlled using ptz.cgi (see section PTZ control). - """ - attributes = ( - "AutoFocus", - "AutoIris", - "BackLight", - "BackLightEnabled", - "BrightnessEnabled", - "CtlQueueing", - "CtlQueueLimit", - "CtlQueuePollTime", - "FocusEnabled", - "HomePresetSet", - "IrCutFilter", - "IrCutFilterEnabled", - "IrisEnabled", - "MaxProportionalSpeed", - "PanEnabled", - "ProportionalSpeedEnabled", - "PTZCounter", - "ReturnToOverview", - "SpeedCtlEnabled", - "TiltEnabled", - "ZoomEnabled", - ) - return self.process_dynamic_group( - self[PTZ], # type: ignore[arg-type] - "Various.V", - attributes, - range(1, self.ptz_number_of_cameras + 1), - ) + ptz = "" + for k, v in self[PTZ].raw.items(): + ptz += f"root.PTZ.{k}={v}\n" + return ptz.encode() # Stream profiles diff --git a/axis/vapix/interfaces/port_cgi.py b/axis/vapix/interfaces/port_cgi.py index 011e666f..442f7003 100644 --- a/axis/vapix/interfaces/port_cgi.py +++ b/axis/vapix/interfaces/port_cgi.py @@ -35,7 +35,6 @@ async def get_ports(self) -> dict[str, Port]: def process_ports(self) -> dict[str, Port]: """Process ports.""" - assert self.vapix.params is not None return GetPortsResponse.decode(self.vapix.params.ports).data async def action(self, id: str, action: PortAction) -> None: diff --git a/axis/vapix/interfaces/ptz.py b/axis/vapix/interfaces/ptz.py index 358fa5f0..5fdc841e 100644 --- a/axis/vapix/interfaces/ptz.py +++ b/axis/vapix/interfaces/ptz.py @@ -7,22 +7,35 @@ from ..models.ptz_cgi import ( DeviceDriverRequest, + GetPtzParamsRequest, + GetPtzResponse, PtzCommandRequest, PtzControlRequest, + PtzItem, PtzMove, PtzQuery, PtzRotation, PtzState, QueryRequest, ) +from .api_handler import ApiHandler -class PtzControl: +class PtzControl(ApiHandler[PtzItem]): """Configure and control the PTZ functionality.""" - def __init__(self, vapix) -> None: - """Initialize PTZ control.""" - self.vapix = vapix + async def _api_request(self) -> dict[str, PtzItem]: + """Get API data method defined by subclass.""" + return await self.get_ptz() + + async def get_ptz(self) -> dict[str, PtzItem]: + """Retrieve privilege rights for current user.""" + bytes_data = await self.vapix.new_request(GetPtzParamsRequest()) + return GetPtzResponse.decode(bytes_data).data + + def process_ptz(self) -> dict[str, PtzItem]: + """Process ports.""" + return GetPtzResponse.decode(self.vapix.params.ptz_data).data async def control( self, @@ -60,11 +73,8 @@ async def control( ircutfilter: PtzState | None = None, backlight: bool | None = None, ) -> None: - """Control the pan, tilt and zoom behavior of a PTZ unit. - move= Absolute:Moves the image 25 % of the image field width in the specified direction. - Relative: Moves the device approx. 50-90 degrees in the specified direction. - """ - return await self.vapix.new_request( + """Control the pan, tilt and zoom behavior of a PTZ unit.""" + await self.vapix.new_request( PtzControlRequest( camera, center, @@ -102,14 +112,14 @@ async def control( ) ) - async def query(self, query: PtzQuery) -> str: + async def query(self, query: PtzQuery) -> bytes: """Retrieve current status.""" return await self.vapix.new_request(QueryRequest(query)) - async def configured_device_driver(self) -> str: + async def configured_device_driver(self) -> bytes: """Name of the system-configured device driver.""" return await self.vapix.new_request(DeviceDriverRequest()) - async def available_ptz_commands(self) -> str: + async def available_ptz_commands(self) -> bytes: """Available PTZ commands.""" return await self.vapix.new_request(PtzCommandRequest()) diff --git a/axis/vapix/models/param_cgi.py b/axis/vapix/models/param_cgi.py index 9cf3031c..cf521df7 100644 --- a/axis/vapix/models/param_cgi.py +++ b/axis/vapix/models/param_cgi.py @@ -339,151 +339,6 @@ def decode(cls, data: dict[str, str]) -> Self: ) -@dataclass -class PTZParam(ApiItem): - """Stream profile parameters.""" - - camera_default: int - """PTZ default video channel. - - When camera parameter is omitted in HTTP requests. - """ - - number_of_cameras: int - """Amount of video channels.""" - - number_of_serial_ports: int - """Amount of serial ports.""" - - limits: dict - """PTZ.Limit.L# are populated when a driver is installed on a video channel. - - Index # is the video channel number, starting on 1. - When it is possible to obtain the current position from the driver, - for example the current pan position, it is possible to apply limit restrictions - to the requested operation. For instance, if an absolute pan to position 150 - is requested, but the upper limit is set to 140, the new pan position will be 140. - This is the purpose of all but MinFieldAngle and MaxFieldAngle in this group. - The purpose of those two parameters is to calibrate image centering. - """ - - support: dict - """PTZ.Support.S# are populated when a driver is installed on a video channel. - - A parameter in the group has the value true if the corresponding capability - is supported by the driver. The index # is the video channel number which starts from 1. - An absolute operation means moving to a certain position, - a relative operation means moving relative to the current position. - Arguments referred to apply to PTZ control. - """ - - various: dict - """PTZ.Various.V# are populated when a driver is installed on a video channel. - - The index # is the video channel number which starts from 1. - The group consists of several different types of parameters for the video channel. - To distinguish the parameter types, the group is presented as - three different categories below. The Enabled parameters determine - if a specific feature can be controlled using ptz.cgi (see section PTZ control). - """ - - @classmethod - def decode(cls, data: dict[str, str]) -> Self: - """Decode dictionary to class object.""" - number_of_cameras = int(data["NbrOfCameras"]) - limit_attributes = ( - "MaxBrightness", - "MaxFieldAngle", - "MaxFocus", - "MaxIris", - "MaxPan", - "MaxTilt", - "MaxZoom", - "MinBrightness", - "MinFieldAngle", - "MinFocus", - "MinIris", - "MinPan", - "MinTilt", - "MinZoom", - ) - support_attributes = ( - "AbsoluteBrightness", - "AbsoluteFocus", - "AbsoluteIris", - "AbsolutePan", - "AbsoluteTilt", - "AbsoluteZoom", - "ActionNotification", - "AreaZoom", - "AutoFocus", - "AutoIrCutFilter", - "AutoIris", - "Auxiliary", - "BackLight", - "ContinuousBrightness", - "ContinuousFocus", - "ContinuousIris", - "ContinuousPan", - "ContinuousTilt", - "ContinuousZoom", - "DevicePreset", - "DigitalZoom", - "GenericHTTP", - "IrCutFilter", - "JoyStickEmulation", - "LensOffset", - "OSDMenu", - "ProportionalSpeed", - "RelativeBrightness", - "RelativeFocus", - "RelativeIris", - "RelativePan", - "RelativeTilt", - "RelativeZoom", - "ServerPreset", - "SpeedCtl", - ) - various_attributes = ( - "AutoFocus", - "AutoIris", - "BackLight", - "BackLightEnabled", - "BrightnessEnabled", - "CtlQueueing", - "CtlQueueLimit", - "CtlQueuePollTime", - "FocusEnabled", - "HomePresetSet", - "IrCutFilter", - "IrCutFilterEnabled", - "IrisEnabled", - "MaxProportionalSpeed", - "PanEnabled", - "ProportionalSpeedEnabled", - "PTZCounter", - "ReturnToOverview", - "SpeedCtlEnabled", - "TiltEnabled", - "ZoomEnabled", - ) - return cls( - id="ptz", - camera_default=int(data["CameraDefault"]), - number_of_cameras=int(data["NbrOfCameras"]), - number_of_serial_ports=int(data["NbrOfSerPorts"]), - limits=process_dynamic_group( - data, "Limit.L", limit_attributes, range(1, number_of_cameras + 1) - ), - support=process_dynamic_group( - data, "Support.S", support_attributes, range(1, number_of_cameras + 1) - ), - various=process_dynamic_group( - data, "Various.V", various_attributes, range(1, number_of_cameras + 1) - ), - ) - - @dataclass class StreamProfileParam(ApiItem): """Stream profile parameters.""" diff --git a/axis/vapix/models/ptz_cgi.py b/axis/vapix/models/ptz_cgi.py index 54d29ee9..0fefefff 100644 --- a/axis/vapix/models/ptz_cgi.py +++ b/axis/vapix/models/ptz_cgi.py @@ -7,13 +7,181 @@ from dataclasses import dataclass from enum import Enum -from typing import TypeVar +from typing import Any, Mapping, TypeVar, cast + +from typing_extensions import NotRequired, Self, TypedDict from .api import ApiItem, ApiRequest, ApiResponse +from .param_cgi import process_dynamic_group + + +class ImageSourceT(TypedDict): + """PTZ image source description.""" + + PTZEnabled: bool + + +class LimitT(TypedDict): + """PTZ limit data description.""" + + MaxBrightness: NotRequired[int] + MaxFieldAngle: int + MaxFocus: NotRequired[int] + MaxIris: NotRequired[int] + MaxPan: int + MaxTilt: int + MaxZoom: int + MinBrightness: NotRequired[int] + MinFieldAngle: int + MinFocus: NotRequired[int] + MinIris: NotRequired[int] + MinPan: int + MinTilt: int + MinZoom: int + + +class PresetPositionT(TypedDict): + """PTZ preset position data description.""" + + data: tuple[float, float, float] + name: str | None + + +class PresetT(TypedDict): + """PTZ preset data description.""" + + HomePosition: int + ImageSource: int + Name: str | None + Position: dict[str, PresetPositionT] + + +class SupportT(TypedDict): + """PTZ support data description.""" + + AbsoluteBrightness: bool + AbsoluteFocus: bool + AbsoluteIris: bool + AbsolutePan: bool + AbsoluteTilt: bool + AbsoluteZoom: bool + ActionNotification: bool + AreaZoom: bool + AutoFocus: bool + AutoIrCutFilter: bool + AutoIris: bool + Auxiliary: bool + BackLight: bool + ContinuousBrightness: bool + ContinuousFocus: bool + ContinuousIris: bool + ContinuousPan: bool + ContinuousTilt: bool + ContinuousZoom: bool + DevicePreset: bool + DigitalZoom: bool + GenericHTTP: bool + IrCutFilter: bool + JoyStickEmulation: bool + LensOffset: bool + OSDMenu: bool + ProportionalSpeed: bool + RelativeBrightness: bool + RelativeFocus: bool + RelativeIris: bool + RelativePan: bool + RelativeTilt: bool + RelativeZoom: bool + ServerPreset: bool + SpeedCtl: bool + + +class UserAdvT(TypedDict): + """PTZ user adv data description.""" + + MoveSpeed: int + + +class UserCtlQueueT(TypedDict): + """PTZ user control queue data description.""" + + Priority: int + TimeoutTime: int + TimeoutType: str + UseCookie: str + UserGroup: str + + +class VariousT(TypedDict): + """PTZ various configurations data description.""" + + CtlQueueing: bool + CtlQueueLimit: int + CtlQueuePollTime: int + HomePresetSet: bool + Locked: NotRequired[bool] + MaxProportionalSpeed: int + PanEnabled: bool + ProportionalSpeedEnabled: bool + ReturnToOverview: int + SpeedCtlEnabled: bool + TiltEnabled: bool + ZoomEnabled: bool + + +class PtzItemT(TypedDict): + """PTZ representation.""" + + BoaProtPTZOperator: str + CameraDefault: int + NbrOfCameras: int + NbrOfSerPorts: int + CamPorts: dict[str, int] + ImageSource: dict[str, ImageSourceT] + Limit: dict[str, LimitT] + Preset: dict[str, PresetT] + PTZDriverStatuses: dict[str, int] + Support: dict[str, SupportT] + UserAdv: dict[str, UserAdvT] + UserCtlQueue: dict[str, UserCtlQueueT] + Various: dict[str, VariousT] + + +def params_to_dict(params: str, starts_with: str | None = None) -> dict[str, Any]: + """Convert params to dictionary.""" + + def convert(value: str) -> bool | int | str: + """Convert value to Python type.""" + if value in ("true", "false", "yes", "no"): # Boolean values + return value in ("true", "yes") + if value.lstrip("-").isdigit(): # Positive/negative values + return int(value) + return value + + def travel(store: dict[str, Any], keys: str, v: bool | int | str) -> None: + """Travel through store and add new keys and finally value to store. + + travel({}, "root.IOPort.I1.Output.Active", "closed") + {'root': {'IOPort': {'I1': {'Output': {'Active': 'closed'}}}}} + """ + k, _, keys = keys.partition(".") # "root", ".", "IOPort.I1.Output.Active" + travel(store.setdefault(k, {}), keys, v) if keys else store.update({k: v}) + + param_dict: dict[str, Any] = {} + for line in params.splitlines(): + if starts_with is not None and not line.startswith(starts_with): + continue + keys, _, value = line.partition("=") + travel(param_dict, keys, convert(value)) + return param_dict class PtzMove(Enum): - """Supported PTZ moves.""" + """Supported PTZ moves. + + Absolute:Moves the image 25 % of the image field width in the specified direction. + Relative: Moves the device approx. 50-90 degrees in the specified direction. + """ HOME = "home" """Moves the image to the home position.""" @@ -96,6 +264,288 @@ def limit(num: _T, minimum: float, maximum: float) -> _T: return max(min(num, maximum), minimum) +@dataclass +class PtzImageSource: + """Image source""" + + ptz_enabled: bool + + @classmethod + def decode(cls, data: ImageSourceT) -> Self: + """Decode dictionary to class object.""" + return cls(ptz_enabled=data["PTZEnabled"]) + + @classmethod + def from_dict(cls, data: dict[str, ImageSourceT]) -> dict[str, Self]: + """Create objects from dict.""" + return {k[1:]: cls.decode(v) for k, v in data.items()} + + +@dataclass +class PtzLimit: + """PTZ.Limit.L# are populated when a driver is installed on a video channel. + + Index # is the video channel number, starting on 1. + When it is possible to obtain the current position from the driver, + for example the current pan position, it is possible to apply limit restrictions + to the requested operation. For instance, if an absolute pan to position 150 + is requested, but the upper limit is set to 140, the new pan position will be 140. + This is the purpose of all but MinFieldAngle and MaxFieldAngle in this group. + The purpose of those two parameters is to calibrate image centering. + """ + + max_field_angle: int + min_field_angle: int + max_pan: int + min_pan: int + max_tilt: int + min_tilt: int + max_zoom: int + min_zoom: int + max_brightness: int | None + min_brightness: int | None + max_focus: int | None + min_focus: int | None + max_iris: int | None + min_iris: int | None + + @classmethod + def decode(cls, data: LimitT) -> Self: + """Decode dictionary to class object.""" + return cls( + max_field_angle=data["MaxFieldAngle"], + min_field_angle=data["MinFieldAngle"], + max_pan=data["MaxPan"], + min_pan=data["MinPan"], + max_tilt=data["MaxTilt"], + min_tilt=data["MinTilt"], + max_zoom=data["MaxZoom"], + min_zoom=data["MinZoom"], + max_brightness=data.get("MaxBrightness"), + min_brightness=data.get("MinBrightness"), + max_focus=data.get("MaxFocus"), + min_focus=data.get("MinFocus"), + max_iris=data.get("MaxIris"), + min_iris=data.get("MinIris"), + ) + + @classmethod + def from_dict(cls, data: dict[str, LimitT]) -> dict[str, Self]: + """Create objects from dict.""" + return {k[1:]: cls.decode(v) for k, v in data.items()} + + +@dataclass +class PtzSupport: + """PTZ.Support.S# are populated when a driver is installed on a video channel. + + A parameter in the group has the value true if the corresponding capability + is supported by the driver. The index # is the video channel number + which starts from 1. + An absolute operation means moving to a certain position, + a relative operation means moving relative to the current position. + Arguments referred to apply to PTZ control. + """ + + absolute_brightness: bool + absolute_focus: bool + absolute_iris: bool + absolute_pan: bool + absolute_tilt: bool + absolute_zoom: bool + action_notification: bool + area_zoom: bool + auto_focus: bool + auto_ir_cut_filter: bool + auto_iris: bool + auxiliary: bool + backLight: bool + continuous_brightness: bool + continuous_focus: bool + continuous_iris: bool + continuous_pan: bool + continuous_tilt: bool + continuousZoom: bool + device_preset: bool + digital_zoom: bool + generic_http: bool + ir_cut_filter: bool + joystick_emulation: bool + lens_offset: bool + osd_menu: bool + proportional_speed: bool + relative_brightness: bool + relative_focus: bool + relative_iris: bool + relative_pan: bool + relative_tilt: bool + relative_zoom: bool + server_preset: bool + speed_control: bool + + @classmethod + def decode(cls, data: SupportT) -> Self: + """Decode dictionary to class object.""" + return cls( + absolute_brightness=data["AbsoluteBrightness"], + absolute_focus=data["AbsoluteFocus"], + absolute_iris=data["AbsoluteIris"], + absolute_pan=data["AbsolutePan"], + absolute_tilt=data["AbsoluteTilt"], + absolute_zoom=data["AbsoluteZoom"], + action_notification=data["ActionNotification"], + area_zoom=data["AreaZoom"], + auto_focus=data["AutoFocus"], + auto_ir_cut_filter=data["AutoIrCutFilter"], + auto_iris=data["AutoIris"], + auxiliary=data["Auxiliary"], + backLight=data["BackLight"], + continuous_brightness=data["ContinuousBrightness"], + continuous_focus=data["ContinuousFocus"], + continuous_iris=data["ContinuousIris"], + continuous_pan=data["ContinuousPan"], + continuous_tilt=data["ContinuousTilt"], + continuousZoom=data["ContinuousZoom"], + device_preset=data["DevicePreset"], + digital_zoom=data["DigitalZoom"], + generic_http=data["GenericHTTP"], + ir_cut_filter=data["IrCutFilter"], + joystick_emulation=data["JoyStickEmulation"], + lens_offset=data["LensOffset"], + osd_menu=data["OSDMenu"], + proportional_speed=data["ProportionalSpeed"], + relative_brightness=data["RelativeBrightness"], + relative_focus=data["RelativeFocus"], + relative_iris=data["RelativeIris"], + relative_pan=data["RelativePan"], + relative_tilt=data["RelativeTilt"], + relative_zoom=data["RelativeZoom"], + server_preset=data["ServerPreset"], + speed_control=data["SpeedCtl"], + ) + + @classmethod + def from_dict(cls, data: dict[str, SupportT]) -> dict[str, Self]: + """Create objects from dict.""" + return {k[1:]: cls.decode(v) for k, v in data.items()} + + +@dataclass +class PtzVarious: + """PTZ.Various.V# are populated when a driver is installed on a video channel. + + The index # is the video channel number which starts from 1. + The group consists of several different types of parameters for the video channel. + To distinguish the parameter types, the group is presented as + three different categories below. The Enabled parameters determine + if a specific feature can be controlled using ptz.cgi (see section PTZ control). + """ + + control_queueing: bool + control_queue_limit: int + control_queue_poll_time: int + home_preset_set: bool + locked: bool + max_proportional_speed: int + pan_enabled: bool + proportional_speed_enabled: bool + return_to_overview: int + speed_control_enabled: bool + tilt_enabled: bool + zoom_enabled: bool + + @classmethod + def decode(cls, data: VariousT) -> Self: + """Decode dictionary to class object.""" + return cls( + control_queueing=data["CtlQueueing"], + control_queue_limit=data["CtlQueueLimit"], + control_queue_poll_time=data["CtlQueuePollTime"], + home_preset_set=data["HomePresetSet"], + locked=data.get("Locked", False), + max_proportional_speed=data["MaxProportionalSpeed"], + pan_enabled=data["PanEnabled"], + proportional_speed_enabled=data["ProportionalSpeedEnabled"], + return_to_overview=data["ReturnToOverview"], + speed_control_enabled=data["SpeedCtlEnabled"], + tilt_enabled=data["TiltEnabled"], + zoom_enabled=data["ZoomEnabled"], + ) + + @classmethod + def from_dict(cls, data: dict[str, VariousT]) -> dict[str, Self]: + """Create objects from dict.""" + return {k[1:]: cls.decode(v) for k, v in data.items()} + + +@dataclass +class PtzItem(ApiItem): + """PTZ parameters.""" + + camera_default: int + """PTZ default video channel. + + When camera parameter is omitted in HTTP requests. + """ + + number_of_cameras: int + """Amount of video channels.""" + + number_of_serial_ports: int + """Amount of serial ports.""" + + cam_ports: dict[str, int] + + image_source: dict[str, PtzImageSource] + + limits: dict[str, PtzLimit] + """PTZ.Limit.L# are populated when a driver is installed on a video channel.""" + + support: dict[str, PtzSupport] + """PTZ.Support.S# are populated when a driver is installed on a video channel.""" + + various: dict[str, PtzVarious] + """PTZ.Various.V# are populated when a driver is installed on a video channel.""" + + @classmethod + def decode(cls, data: PtzItemT) -> Self: + """Decode dictionary to class object.""" + return cls( + id="ptz", + camera_default=int(data["CameraDefault"]), + number_of_cameras=int(data["NbrOfCameras"]), + number_of_serial_ports=int(data["NbrOfSerPorts"]), + cam_ports=data["CamPorts"], + image_source=PtzImageSource.from_dict(data["ImageSource"]), + limits=PtzLimit.from_dict(data["Limit"]), + support=PtzSupport.from_dict(data["Support"]), + various=PtzVarious.from_dict(data["Various"]), + ) + + +@dataclass +class GetPtzParamsRequest(ApiRequest): + """Request object for listing PTZ parameters.""" + + method = "get" + path = "/axis-cgi/param.cgi?action=list&group=root.PTZ" + content_type = "text/plain" + + +@dataclass +class GetPtzResponse(ApiResponse[dict[str, PtzItem]]): + """Response object for listing ports.""" + + @classmethod + def decode(cls, bytes_data: bytes) -> Self: + """Prepare API description dictionary.""" + data = bytes_data.decode() + ptz_params: PtzItemT = ( + params_to_dict(data, "root.PTZ").get("root", {}).get("PTZ", {}) + ) + return cls({"0": PtzItem.decode(ptz_params)}) + + @dataclass class PtzControlRequest(ApiRequest): """Control pan, tilt and zoom behavior of a PTZ unit.""" @@ -138,6 +588,11 @@ class PtzControlRequest(ApiRequest): which is product-specific. """ move: PtzMove | None = None + """Supported PTZ moves. + + Absolute:Moves the image 25 % of the image field width in the specified direction. + Relative: Moves the device approx. 50-90 degrees in the specified direction. + """ pan: float | None = None """-180.0 ... 180.0 Pans the device to the specified absolute coordinates.""" diff --git a/axis/vapix/vapix.py b/axis/vapix/vapix.py index 61244401..961efe30 100644 --- a/axis/vapix/vapix.py +++ b/axis/vapix/vapix.py @@ -59,8 +59,6 @@ def __init__(self, device: "AxisDevice") -> None: self.loitering_guard: LoiteringGuard | None = None self.motion_guard: MotionGuard | None = None self.object_analytics: ObjectAnalytics | None = None - self.params: Params | None = None - self.ptz: PtzControl | None = None self.vmd4: Vmd4 | None = None self.users = Users(self) @@ -75,7 +73,9 @@ def __init__(self, device: "AxisDevice") -> None: self.stream_profiles = StreamProfilesHandler(self) self.view_areas = ViewAreaHandler(self) + self.params = Params(self) self.port_cgi = Ports(self) + self.ptz = PtzControl(self) @property def firmware_version(self) -> str: @@ -182,8 +182,6 @@ async def do_api_request(api: ApiHandler) -> None: async def initialize_param_cgi(self, preload_data: bool = True) -> None: """Load data from param.cgi.""" - self.params = Params(self) - tasks = [] if preload_data: @@ -216,8 +214,8 @@ async def initialize_param_cgi(self, preload_data: bool = True) -> None: if not self.io_port_management.supported(): self.port_cgi._items = self.port_cgi.process_ports() - if not self.ptz and self.params.ptz: - self.ptz = PtzControl(self) + if self.params.ptz: + self.ptz._items = self.ptz.process_ptz() async def initialize_applications(self) -> None: """Load data for applications on device.""" diff --git a/tests/test_param_cgi.py b/tests/test_param_cgi.py index a3f2dc84..efb8a423 100644 --- a/tests/test_param_cgi.py +++ b/tests/test_param_cgi.py @@ -448,126 +448,6 @@ async def test_update_properties(params: Params): assert params.system_serialnumber == "ACCC12345678" -@respx.mock -async def test_update_ptz(params: Params): - """Verify that update ptz works.""" - route = respx.get( - f"http://{HOST}:80/axis-cgi/param.cgi?action=list&group=root.PTZ" - ).respond( - text=response_param_cgi_ptz, - headers={"Content-Type": "text/plain"}, - ) - - await params.update_ptz() - - assert route.called - assert route.calls.last.request.method == "GET" - assert route.calls.last.request.url.path == "/axis-cgi/param.cgi" - - ptz_param = params.ptz_params - - assert params.ptz_camera_default == 1 - assert params.ptz_number_of_cameras == 1 - assert params.ptz_number_of_serial_ports == 1 - assert ptz_param.camera_default == 1 - assert ptz_param.number_of_cameras == 1 - assert ptz_param.number_of_serial_ports == 1 - assert ( - ptz_param.limits - == params.ptz_limits - == { - 1: { - "MaxBrightness": 9999, - "MaxFieldAngle": 623, - "MaxFocus": 9999, - "MaxIris": 9999, - "MaxPan": 170, - "MaxTilt": 90, - "MaxZoom": 9999, - "MinBrightness": 1, - "MinFieldAngle": 22, - "MinFocus": 770, - "MinIris": 1, - "MinPan": -170, - "MinTilt": -20, - "MinZoom": 1, - } - } - ) - assert ( - ptz_param.support - == params.ptz_support - == { - 1: { - "AbsoluteBrightness": True, - "AbsoluteFocus": True, - "AbsoluteIris": True, - "AbsolutePan": True, - "AbsoluteTilt": True, - "AbsoluteZoom": True, - "ActionNotification": True, - "AreaZoom": True, - "AutoFocus": True, - "AutoIrCutFilter": True, - "AutoIris": True, - "Auxiliary": True, - "BackLight": True, - "ContinuousBrightness": False, - "ContinuousFocus": True, - "ContinuousIris": False, - "ContinuousPan": True, - "ContinuousTilt": True, - "ContinuousZoom": True, - "DevicePreset": False, - "DigitalZoom": True, - "GenericHTTP": False, - "IrCutFilter": True, - "JoyStickEmulation": True, - "LensOffset": False, - "OSDMenu": False, - "ProportionalSpeed": True, - "RelativeBrightness": True, - "RelativeFocus": True, - "RelativeIris": True, - "RelativePan": True, - "RelativeTilt": True, - "RelativeZoom": True, - "ServerPreset": True, - "SpeedCtl": True, - } - } - ) - assert ( - ptz_param.various - == params.ptz_various - == { - 1: { - "AutoFocus": True, - "AutoIris": True, - "BackLight": False, - "BackLightEnabled": True, - "BrightnessEnabled": True, - "CtlQueueing": False, - "CtlQueueLimit": 20, - "CtlQueuePollTime": 20, - "FocusEnabled": True, - "HomePresetSet": True, - "IrCutFilter": "auto", - "IrCutFilterEnabled": True, - "IrisEnabled": True, - "MaxProportionalSpeed": 200, - "PanEnabled": True, - "ProportionalSpeedEnabled": True, - "PTZCounter": 8, - "ReturnToOverview": 0, - "SpeedCtlEnabled": True, - "TiltEnabled": True, - "ZoomEnabled": True, - } - } - ) - - @respx.mock async def test_update_stream_profiles(params: Params): """Verify that update properties works.""" diff --git a/tests/test_ptz.py b/tests/test_ptz.py index 4ae61160..5a734305 100644 --- a/tests/test_ptz.py +++ b/tests/test_ptz.py @@ -8,18 +8,20 @@ import pytest import respx +from axis.device import AxisDevice from axis.vapix.interfaces.ptz import PtzControl from axis.vapix.models.ptz_cgi import PtzMove, PtzQuery, PtzRotation, PtzState, limit from .conftest import HOST +from .test_param_cgi import response_param_cgi_ptz UNSUPPORTED_COMMAND = "unsupported" @pytest.fixture -def ptz_control(axis_device) -> PtzControl: +def ptz_control(axis_device: AxisDevice) -> PtzControl: """Return the PTZ control mock object.""" - return PtzControl(axis_device.vapix) + return axis_device.vapix.ptz def test_limit(): @@ -32,7 +34,100 @@ def test_limit(): @respx.mock -async def test_ptz_control_no_input(ptz_control): +async def test_update_ptz(ptz_control: PtzControl): + """Verify that update ptz works.""" + route = respx.get( + f"http://{HOST}:80/axis-cgi/param.cgi?action=list&group=root.PTZ" + ).respond( + text=response_param_cgi_ptz, + headers={"Content-Type": "text/plain"}, + ) + + await ptz_control.update() + + assert route.called + assert route.calls.last.request.method == "GET" + assert route.calls.last.request.url.path == "/axis-cgi/param.cgi" + + ptz = ptz_control["0"] + assert ptz.camera_default == 1 + assert ptz.number_of_cameras == 1 + assert ptz.number_of_serial_ports == 1 + assert ptz.cam_ports == {"Cam1Port": 1} + + assert len(ptz.limits) == 1 + limit = ptz.limits["1"] + assert limit.max_brightness == 9999 + assert limit.min_brightness == 1 + assert limit.max_field_angle == 623 + assert limit.min_field_angle == 22 + assert limit.max_focus == 9999 + assert limit.min_focus == 770 + assert limit.max_iris == 9999 + assert limit.min_iris == 1 + assert limit.max_pan == 170 + assert limit.min_pan == -170 + assert limit.max_tilt == 90 + assert limit.min_tilt == -20 + assert limit.max_zoom == 9999 + assert limit.min_zoom == 1 + + assert len(ptz.support) == 1 + support = ptz.support["1"] + assert support.absolute_brightness + assert support.absolute_focus + assert support.absolute_iris + assert support.absolute_pan + assert support.absolute_tilt + assert support.absolute_zoom + assert support.action_notification + assert support.area_zoom + assert support.auto_focus + assert support.auto_ir_cut_filter + assert support.auto_iris + assert support.auxiliary + assert support.backLight + assert support.continuous_brightness is False + assert support.continuous_focus + assert support.continuous_iris is False + assert support.continuous_pan + assert support.continuous_tilt + assert support.continuousZoom + assert support.device_preset is False + assert support.digital_zoom + assert support.generic_http is False + assert support.ir_cut_filter + assert support.joystick_emulation + assert support.lens_offset is False + assert support.osd_menu is False + assert support.proportional_speed + assert support.relative_brightness + assert support.relative_focus + assert support.relative_iris + assert support.relative_pan + assert support.relative_tilt + assert support.relative_zoom + assert support.server_preset + assert support.speed_control + + assert len(ptz.various) == 1 + various = ptz.various["1"] + assert various.control_queueing is False + assert various.control_queue_limit + assert various.control_queue_poll_time == 20 + assert various.home_preset_set + assert various.locked is False + assert various.max_proportional_speed == 200 + assert various.pan_enabled + assert various.proportional_speed_enabled + assert various.return_to_overview == 0 + assert various.speed_control_enabled + assert various.tilt_enabled + assert various.zoom_enabled + + +@respx.mock +async def test_ptz_control_no_input(ptz_control: PtzControl): """Verify that PTZ control without input doesn't send out anything.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") await ptz_control.control() @@ -41,7 +136,7 @@ async def test_ptz_control_no_input(ptz_control): @respx.mock -async def test_ptz_control_camera_no_output(ptz_control): +async def test_ptz_control_camera_no_output(ptz_control: PtzControl): """Verify that PTZ control does not send out camera input without additional commands.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") await ptz_control.control(camera=1) @@ -50,7 +145,7 @@ async def test_ptz_control_camera_no_output(ptz_control): @respx.mock -async def test_ptz_control_camera_with_move(ptz_control): +async def test_ptz_control_camera_with_move(ptz_control: PtzControl): """Verify that PTZ control send out camera input with additional commands.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -66,7 +161,7 @@ async def test_ptz_control_camera_with_move(ptz_control): @respx.mock -async def test_ptz_control_center(ptz_control): +async def test_ptz_control_center(ptz_control: PtzControl): """Verify that PTZ control can send out center input.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") await ptz_control.control(center=(30, 60)) @@ -74,7 +169,7 @@ async def test_ptz_control_center(ptz_control): @respx.mock -async def test_ptz_control_center_with_imagewidth(ptz_control): +async def test_ptz_control_center_with_imagewidth(ptz_control: PtzControl): """Verify that PTZ control can send out center together with imagewidth.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") await ptz_control.control(center=(30, 60), imagewidth=120) @@ -85,7 +180,7 @@ async def test_ptz_control_center_with_imagewidth(ptz_control): @respx.mock -async def test_ptz_control_areazoom(ptz_control): +async def test_ptz_control_areazoom(ptz_control: PtzControl): """Verify that PTZ control can send out areazoom input.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") await ptz_control.control(areazoom=(30, 60, 90)) @@ -95,7 +190,7 @@ async def test_ptz_control_areazoom(ptz_control): @respx.mock -async def test_ptz_control_areazoom_too_little_zoom(ptz_control): +async def test_ptz_control_areazoom_too_little_zoom(ptz_control: PtzControl): """Verify that PTZ control can send out areazoom input.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") await ptz_control.control(areazoom=(30, 60, 0)) @@ -105,7 +200,7 @@ async def test_ptz_control_areazoom_too_little_zoom(ptz_control): @respx.mock -async def test_ptz_control_areazoom_with_imageheight(ptz_control): +async def test_ptz_control_areazoom_with_imageheight(ptz_control: PtzControl): """Verify that PTZ control can send out areazoom with imageheight.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") await ptz_control.control(areazoom=(30, 60, 90), imageheight=120) @@ -116,7 +211,7 @@ async def test_ptz_control_areazoom_with_imageheight(ptz_control): @respx.mock -async def test_ptz_control_pan(ptz_control): +async def test_ptz_control_pan(ptz_control: PtzControl): """Verify that PTZ control can send out pan and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -131,7 +226,7 @@ async def test_ptz_control_pan(ptz_control): @respx.mock -async def test_ptz_control_tilt(ptz_control): +async def test_ptz_control_tilt(ptz_control: PtzControl): """Verify that PTZ control can send out tilt and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -146,7 +241,7 @@ async def test_ptz_control_tilt(ptz_control): @respx.mock -async def test_ptz_control_zoom(ptz_control): +async def test_ptz_control_zoom(ptz_control: PtzControl): """Verify that PTZ control can send out zoom and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -161,7 +256,7 @@ async def test_ptz_control_zoom(ptz_control): @respx.mock -async def test_ptz_control_focus(ptz_control): +async def test_ptz_control_focus(ptz_control: PtzControl): """Verify that PTZ control can send out focus and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -176,7 +271,7 @@ async def test_ptz_control_focus(ptz_control): @respx.mock -async def test_ptz_control_iris(ptz_control): +async def test_ptz_control_iris(ptz_control: PtzControl): """Verify that PTZ control can send out iris and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -191,7 +286,7 @@ async def test_ptz_control_iris(ptz_control): @respx.mock -async def test_ptz_control_brightness(ptz_control): +async def test_ptz_control_brightness(ptz_control: PtzControl): """Verify that PTZ control can send out brightness and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -206,7 +301,7 @@ async def test_ptz_control_brightness(ptz_control): @respx.mock -async def test_ptz_control_rpan(ptz_control): +async def test_ptz_control_rpan(ptz_control: PtzControl): """Verify that PTZ control can send out rpan and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -221,7 +316,7 @@ async def test_ptz_control_rpan(ptz_control): @respx.mock -async def test_ptz_control_rtilt(ptz_control): +async def test_ptz_control_rtilt(ptz_control: PtzControl): """Verify that PTZ control can send out rtilt and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -236,7 +331,7 @@ async def test_ptz_control_rtilt(ptz_control): @respx.mock -async def test_ptz_control_rzoom(ptz_control): +async def test_ptz_control_rzoom(ptz_control: PtzControl): """Verify that PTZ control can send out rzoom and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -251,7 +346,7 @@ async def test_ptz_control_rzoom(ptz_control): @respx.mock -async def test_ptz_control_rfocus(ptz_control): +async def test_ptz_control_rfocus(ptz_control: PtzControl): """Verify that PTZ control can send out rfocus and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -266,7 +361,7 @@ async def test_ptz_control_rfocus(ptz_control): @respx.mock -async def test_ptz_control_riris(ptz_control): +async def test_ptz_control_riris(ptz_control: PtzControl): """Verify that PTZ control can send out riris and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -281,7 +376,7 @@ async def test_ptz_control_riris(ptz_control): @respx.mock -async def test_ptz_control_rbrightness(ptz_control): +async def test_ptz_control_rbrightness(ptz_control: PtzControl): """Verify that PTZ control can send out rbrightness and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -298,7 +393,7 @@ async def test_ptz_control_rbrightness(ptz_control): @respx.mock -async def test_ptz_control_continuouszoommove(ptz_control): +async def test_ptz_control_continuouszoommove(ptz_control: PtzControl): """Verify that PTZ control can send out continuouszoommove and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -322,7 +417,7 @@ async def test_ptz_control_continuouszoommove(ptz_control): @respx.mock -async def test_ptz_control_continuousfocusmove(ptz_control): +async def test_ptz_control_continuousfocusmove(ptz_control: PtzControl): """Verify that PTZ control can send out continuousfocusmove and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -346,7 +441,7 @@ async def test_ptz_control_continuousfocusmove(ptz_control): @respx.mock -async def test_ptz_control_continuousirismove(ptz_control): +async def test_ptz_control_continuousirismove(ptz_control: PtzControl): """Verify that PTZ control can send out continuousirismove and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -370,7 +465,7 @@ async def test_ptz_control_continuousirismove(ptz_control): @respx.mock -async def test_ptz_control_continuousbrightnessmove(ptz_control): +async def test_ptz_control_continuousbrightnessmove(ptz_control: PtzControl): """Verify that PTZ control can send out continuousbrightnessmove and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -394,7 +489,7 @@ async def test_ptz_control_continuousbrightnessmove(ptz_control): @respx.mock -async def test_ptz_control_speed(ptz_control): +async def test_ptz_control_speed(ptz_control: PtzControl): """Verify that PTZ control can send out speed and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -409,7 +504,7 @@ async def test_ptz_control_speed(ptz_control): @respx.mock -async def test_ptz_control_autofocus(ptz_control): +async def test_ptz_control_autofocus(ptz_control: PtzControl): """Verify that PTZ control can send out autofocus.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -418,7 +513,7 @@ async def test_ptz_control_autofocus(ptz_control): @respx.mock -async def test_ptz_control_autoiris(ptz_control): +async def test_ptz_control_autoiris(ptz_control: PtzControl): """Verify that PTZ control can send out autoiris.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -427,7 +522,7 @@ async def test_ptz_control_autoiris(ptz_control): @respx.mock -async def test_ptz_control_backlight(ptz_control): +async def test_ptz_control_backlight(ptz_control: PtzControl): """Verify that PTZ control can send out backlight.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -436,7 +531,7 @@ async def test_ptz_control_backlight(ptz_control): @respx.mock -async def test_ptz_control_ircutfilter(ptz_control): +async def test_ptz_control_ircutfilter(ptz_control: PtzControl): """Verify that PTZ control can send out ircutfilter.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -447,7 +542,7 @@ async def test_ptz_control_ircutfilter(ptz_control): @respx.mock -async def test_ptz_control_imagerotation(ptz_control): +async def test_ptz_control_imagerotation(ptz_control: PtzControl): """Verify that PTZ control can send out imagerotation.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -458,7 +553,7 @@ async def test_ptz_control_imagerotation(ptz_control): @respx.mock -async def test_ptz_control_continuouspantiltmove(ptz_control): +async def test_ptz_control_continuouspantiltmove(ptz_control: PtzControl): """Verify that PTZ control can send out continuouspantiltmove and its limits.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") @@ -482,7 +577,7 @@ async def test_ptz_control_continuouspantiltmove(ptz_control): @respx.mock -async def test_ptz_control_auxiliary(ptz_control): +async def test_ptz_control_auxiliary(ptz_control: PtzControl): """Verify that PTZ control can send out auxiliary.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") await ptz_control.control(auxiliary="any") @@ -490,7 +585,7 @@ async def test_ptz_control_auxiliary(ptz_control): @respx.mock -async def test_ptz_control_gotoserverpresetname(ptz_control): +async def test_ptz_control_gotoserverpresetname(ptz_control: PtzControl): """Verify that PTZ control can send out gotoserverpresetname.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") await ptz_control.control(gotoserverpresetname="any") @@ -501,24 +596,23 @@ async def test_ptz_control_gotoserverpresetname(ptz_control): @respx.mock -async def test_ptz_control_gotoserverpresetno(ptz_control): +async def test_ptz_control_gotoserverpresetno(ptz_control: PtzControl): """Verify that PTZ control can send out gotoserverpresetno.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") - await ptz_control.control(gotoserverpresetno="any") + await ptz_control.control(gotoserverpresetno=1) assert ( route.calls.last.request.content - == urlencode({"gotoserverpresetno": "any"}).encode() + == urlencode({"gotoserverpresetno": 1}).encode() ) @respx.mock -async def test_ptz_control_gotodevicepreset(ptz_control): +async def test_ptz_control_gotodevicepreset(ptz_control: PtzControl): """Verify that PTZ control can send out gotodevicepreset.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi") - await ptz_control.control(gotodevicepreset="any") + await ptz_control.control(gotodevicepreset=2) assert ( - route.calls.last.request.content - == urlencode({"gotodevicepreset": "any"}).encode() + route.calls.last.request.content == urlencode({"gotodevicepreset": 2}).encode() ) @@ -588,7 +682,7 @@ async def test_query_limit(ptz_control, input, output): @respx.mock -async def test_get_configured_device_driver(ptz_control): +async def test_get_configured_device_driver(ptz_control: PtzControl): """Verify listing configured device driver.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi").respond( text="Sony_camblock", @@ -606,7 +700,7 @@ async def test_get_configured_device_driver(ptz_control): @respx.mock -async def test_get_available_ptz_commands(ptz_control): +async def test_get_available_ptz_commands(ptz_control: PtzControl): """Verify listing configured device driver.""" route = respx.post(f"http://{HOST}:80/axis-cgi/com/ptz.cgi").respond( text="""Available commands