diff --git a/axis/vapix/interfaces/param_cgi.py b/axis/vapix/interfaces/param_cgi.py index 55b6e5e1..b0b42a15 100644 --- a/axis/vapix/interfaces/param_cgi.py +++ b/axis/vapix/interfaces/param_cgi.py @@ -14,6 +14,7 @@ ImageParam, Param, PropertyParam, + PTZParam, StreamProfileParam, ) from ..models.stream_profile import StreamProfile @@ -386,6 +387,11 @@ async def update_ptz(self) -> None: """Update PTZ group of parameters.""" 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. diff --git a/axis/vapix/models/param_cgi.py b/axis/vapix/models/param_cgi.py index cf521df7..9cf3031c 100644 --- a/axis/vapix/models/param_cgi.py +++ b/axis/vapix/models/param_cgi.py @@ -339,6 +339,151 @@ 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/tests/test_param_cgi.py b/tests/test_param_cgi.py index 5db4a1ef..915bc4c0 100644 --- a/tests/test_param_cgi.py +++ b/tests/test_param_cgi.py @@ -18,8 +18,7 @@ def params(axis_device) -> Params: @respx.mock -@pytest.mark.asyncio -async def test_params(params): +async def test_params(params: Params): """Verify that you can list parameters.""" route = respx.get(f"http://{HOST}:80/axis-cgi/param.cgi?action=list").respond( text=response_param_cgi, @@ -200,8 +199,7 @@ async def test_params(params): assert params.system_serialnumber == "ACCC12345678" -@pytest.mark.asyncio -async def test_params_empty_raw(params): +async def test_params_empty_raw(params: Params): """Verify that params can take an empty raw on creation.""" assert len(params) == 0 @@ -209,8 +207,7 @@ async def test_params_empty_raw(params): @respx.mock -@pytest.mark.asyncio -async def test_update_brand(params): +async def test_update_brand(params: Params): """Verify that update brand works.""" route = respx.get( f"http://{HOST}:80/axis-cgi/param.cgi?action=list&group=root.Brand" @@ -234,8 +231,7 @@ async def test_update_brand(params): @respx.mock -@pytest.mark.asyncio -async def test_update_image(params): +async def test_update_image(params: Params): """Verify that update brand works.""" route = respx.get( f"http://{HOST}:80/axis-cgi/param.cgi?action=list&group=root.Image" @@ -348,8 +344,7 @@ async def test_update_image(params): @respx.mock -@pytest.mark.asyncio -async def test_update_ports(params): +async def test_update_ports(params: Params): """Verify that update brand works.""" input_route = respx.get( f"http://{HOST}:80/axis-cgi/param.cgi?action=list&group=root.Input" @@ -401,8 +396,7 @@ async def test_update_ports(params): @respx.mock -@pytest.mark.asyncio -async def test_update_properties(params): +async def test_update_properties(params: Params): """Verify that update properties works.""" route = respx.get( f"http://{HOST}:80/axis-cgi/param.cgi?action=list&group=root.Properties" @@ -459,8 +453,7 @@ async def test_update_properties(params): @respx.mock -@pytest.mark.asyncio -async def test_update_ptz(params): +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" @@ -475,96 +468,112 @@ async def test_update_ptz(params): 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 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.ptz_camera_default == 1 + assert ptz_param.ptz_number_of_cameras == 1 + assert ptz_param.ptz_number_of_serial_ports == 1 + assert ( + ptz_param.ptz_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 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.ptz_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 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, + ) + assert ( + ptz_param.ptz_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 -@pytest.mark.asyncio -async def test_update_stream_profiles(params): +async def test_update_stream_profiles(params: Params): """Verify that update properties works.""" route = respx.get( f"http://{HOST}:80/axis-cgi/param.cgi?action=list&group=root.StreamProfile" @@ -601,8 +610,7 @@ async def test_update_stream_profiles(params): @respx.mock -@pytest.mark.asyncio -async def test_stream_profiles_empty_response(params): +async def test_stream_profiles_empty_response(params: Params): """Verify that update properties works.""" respx.get( f"http://{HOST}:80/axis-cgi/param.cgi?action=list&group=root.StreamProfile"