diff --git a/axis/door_control.py b/axis/door_control.py new file mode 100644 index 00000000..7914faf7 --- /dev/null +++ b/axis/door_control.py @@ -0,0 +1,246 @@ +"""Door Control API. + +The Axis Door control API makes it possible to control the behavior and functionality +of physical access controls in the Axis devices (e.g. A1001, A1601) +""" + +from .api import APIItem, APIItems + +URL = "/vapix/doorcontrol" + +API_DISCOVERY_ID = "door-control" + +CAPABILITY_ACCESS = "Access" +CAPABILITY_LOCK = "Lock" +CAPABILITY_UNLOCK = "Unlock" +CAPABILITY_BLOCK = "Block" +CAPABILITY_DOUBLE_LOCK = "DoubleLock" +CAPABILITY_LOCK_DOWN = "LockDown" +CAPABILITY_LOCK_OPEN = "LockOpen" +CAPABILITY_DOOR_MONITOR = "DoorMonitor" +CAPABILITY_LOCK_MONITOR = "LockMonitor" +CAPABILITY_DOUBLE_LOCK_MONITOR = "DoubleLockMonitor" +CAPABILITY_ALARM = "Alarm" +CAPABILITY_TAMPER = "Tamper" +CAPABILITY_WARNING = "Warning" +CAPABILITY_CONFIGURABLE = "Configurable" + +SUPPORTED_CAPABILITIES = ( + CAPABILITY_ACCESS, + CAPABILITY_LOCK, + CAPABILITY_UNLOCK, + CAPABILITY_BLOCK, + CAPABILITY_DOUBLE_LOCK, + CAPABILITY_LOCK_DOWN, + CAPABILITY_LOCK_OPEN, + CAPABILITY_DOOR_MONITOR, + CAPABILITY_LOCK_MONITOR, + CAPABILITY_DOUBLE_LOCK_MONITOR, + CAPABILITY_ALARM, + CAPABILITY_TAMPER, + CAPABILITY_WARNING, + CAPABILITY_CONFIGURABLE +) + + +class DoorControl(APIItems): + """Door control for Axis devices.""" + + def __init__(self, request: object) -> None: + """Initialize door control manager.""" + super().__init__({}, request, URL, Door) + + # TODO: Question: Is this used to get status information for the door? Or to update the object properties? + async def update(self) -> None: + """Refresh data.""" + raw = await self.get_door_info_list() + self.process_raw(raw) + + @staticmethod + def pre_process_raw(raw: dict) -> dict: + """Return a dictionary of doors.""" + door_control_data = raw.get("DoorInfo", {}) + return {api["token"]: api for api in door_control_data} + + async def get_service_capabilities(self) -> dict: + """List the capabilities of the door controller.""" + return await self._request( + "post", + URL, + json={"tdc:GetServiceCapabilities": {}}, + ) + + async def get_door_info_list(self) -> dict: + """List the doors.""" + return await self._request( + "post", + URL, + json={"tdc:GetDoorInfoList": {}}, + ) + + async def get_door_info(self, door_tokens: list) -> dict: + """List the door information.""" + return await self._request( + "post", + URL, + json={"tdc:GetDoorInfo": {"Token": door_tokens}} + ) + + async def get_door_state(self, door_token: str) -> dict: + """List the door information.""" + return await self._request( + "post", + URL, + json={"tdc:GetDoorState": {"Token": door_token}} + ) + + # region Door Actions + async def access_door(self, door_token: str) -> None: + """Access a Door. + + It invokes the functionality typically used when a card holder presents a card to a card reader at the door and is granted access. + The DoorMode shall change to Accessed. + The Door shall remain accessible for the defined time as configured in the device. + When the time span elapses, the DoorMode shall change back to its previous state. + A device must have the Lock capability to utilize this method. + """ + await self._request( + "post", + URL, + json={"tdc:AccessDoor": {"Token": door_token}} + ) + + async def lock_door(self, door_token: str) -> None: + """Lock a Door. + + The DoorMode shall change to Locked. + A device must have the Lock capability to utilize this method. + """ + await self._request( + "post", + URL, + json={"tdc:LockDoor": {"Token": door_token}} + ) + + async def unlock_door(self, door_token: str) -> None: + """Unlock a Door until it is explicitly locked again. + + The DoorMode shall change to Unlocked. + A device must have the Unlock capability to utilize this method. + """ + await self._request( + "post", + URL, + json={"tdc:UnlockDoor": {"Token": door_token}} + ) + + async def block_door(self, door_token: str) -> None: + """Block a Door and prevent momentary access (AccessDoor command). + + The DoorMode shall change to Blocked. + A device must have the Block capability to utilize this method. + """ + await self._request( + "post", + URL, + json={"tdc:BlockDoor": {"Token": door_token}} + ) + + async def lock_down_door(self, door_token: str) -> None: + """Locks down a door and prevents other actions until a LockDownReleaseDoor command is invoked. + + The DoorMode shall change to LockedDown. + The device shall ignore other door control commands until a LockDownReleaseDoor command is performed. + A device must have the LockDown capability to utilize this method. + """ + await self._request( + "post", + URL, + json={"tdc:LockDownDoor": {"Token": door_token}} + ) + + async def lock_down_release_door(self, door_token: str) -> None: + """Releases the LockedDown state of a Door. + + The DoorMode shall change back to its previous/next state. + It is not defined what the previous/next state shall be, but typically - Locked. + This method will only succeed if the current DoorMode is LockedDown. + """ + await self._request( + "post", + URL, + json={"tdc:LockDownReleaseDoor": {"Token": door_token}} + ) + + async def lock_open_door(self, door_token: str) -> None: + """Unlocks a Door and prevents other actions until LockOpenReleaseDoor method is invoked. + + The DoorMode shall change to LockedOpen. + The device shall ignore other door control commands until a LockOpenReleaseDoor command is performed. + A device must have the LockOpen capability to utilize this method. + """ + await self._request( + "post", + URL, + json={"tdc:LockOpenDoor": {"Token": door_token}} + ) + + async def lock_open_release_door(self, door_token: str) -> None: + """Releases the LockedOpen state of a Door. + + The DoorMode shall change state from the LockedOpen state back to its previous/next state. + It is not defined what the previous/next state shall be, but typically - Unlocked. + This method shall only succeed if the current DoorMode is LockedOpen. + """ + await self._request( + "post", + URL, + json={"tdc:LockOpenReleaseDoor": {"Token": door_token}} + ) + + async def double_lock_door(self, door_token: str) -> None: + """Lock the door with a double lock.""" + await self._request( + "post", + URL, + json={"tdc:DoubleLockDoor": {"Token": door_token}} + ) + + async def release_door(self, door_token: str) -> None: + """Release the door from a priority level.""" + await self._request( + "post", + URL, + json={"axtdc:ReleaseDoor": {"Token": door_token}} + ) + # endregion + + +class Door(APIItem): + """API Discovery item.""" + + @property + def door_token(self) -> str: + """Token of Door.""" + return self.raw["token"] + + @property + def door_name(self) -> str: + """Name of Door.""" + return self.raw["Name"] + + @property + def door_description(self) -> str: + """Door Description.""" + return self.raw["Description"] + + @property + def door_capabilities(self) -> dict: + """Capabilities of Door.""" + return self.raw["Capabilities"] + + async def is_capable_of(self, capability: str) -> bool: + """Retrieve whether door has the specified capability.""" + if capability not in SUPPORTED_CAPABILITIES: + return False + return self.raw["Capabilities"][capability] diff --git a/axis/event_stream.py b/axis/event_stream.py index 195a2c0c..fdb2927c 100644 --- a/axis/event_stream.py +++ b/axis/event_stream.py @@ -9,6 +9,7 @@ LOGGER = logging.getLogger(__name__) +CLASS_DOOR = "door" CLASS_INPUT = "input" CLASS_LIGHT = "light" CLASS_MOTION = "motion" @@ -188,6 +189,70 @@ class DayNight(AxisBinaryEvent): TYPE = "DayNight" +class DoorAlarm(AxisBinaryEvent): + """Door Alarm Changes.""" + + TOPIC = "tns1:Door/State/DoorAlarm" + CLASS = CLASS_DOOR + TYPE = "Door Alarm" + + +class DoorFault(AxisBinaryEvent): + """Door Fault Changes.""" + + TOPIC = "tns1:Door/State/DoorFault" + CLASS = CLASS_DOOR + TYPE = "Door Fault" + + +class DoorLockPhysical(AxisBinaryEvent): + """Door Lock State Changes.""" + + TOPIC = "tns1:Door/State/LockPhysicalState" + CLASS = CLASS_DOOR + TYPE = "Door Lock Physical State" + + +class DoorMode(AxisBinaryEvent): + """Door Mode Changes.""" + + TOPIC = "tns1:Door/State/DoorMode" + CLASS = CLASS_DOOR + TYPE = "Door Mode" + + +class DoorPhysical(AxisBinaryEvent): + """Door Physical State Changes.""" + + TOPIC = "tns1:Door/State/DoorPhysicalState" + CLASS = CLASS_DOOR + TYPE = "Door Physical State" + + +class DoorTamper(AxisBinaryEvent): + """Door Tamper State Changes.""" + + TOPIC = "tns1:Door/State/DoorTamper" + CLASS = CLASS_DOOR + TYPE = "Door Tamper State" + + +class DoorWarning(AxisBinaryEvent): + """Door Warning State Changes.""" + + TOPIC = "tns1:Door/State/DoorWarning" + CLASS = CLASS_DOOR + TYPE = "Door Warning State" + + +class DoorDoubleLockPhysical(AxisBinaryEvent): + """Door Double Lock State Changes.""" + + TOPIC = "tns1:Door/State/DoubleLockPhysicalState" + CLASS = CLASS_DOOR + TYPE = "Door Double Lock Physical State" + + class FenceGuard(AxisBinaryEvent): """Fence Guard trigger event.""" @@ -338,6 +403,14 @@ def id(self) -> str: EVENT_CLASSES = ( Audio, DayNight, + DoorAlarm, + DoorDoubleLockPhysical, + DoorFault, + DoorLockPhysical, + DoorMode, + DoorPhysical, + DoorWarning, + DoorTamper, FenceGuard, Input, Light, diff --git a/axis/vapix.py b/axis/vapix.py index a51e61b6..202cb9ec 100644 --- a/axis/vapix.py +++ b/axis/vapix.py @@ -21,6 +21,7 @@ from .applications.vmd4 import Vmd4 from .basic_device_info import API_DISCOVERY_ID as BASIC_DEVICE_INFO_ID, BasicDeviceInfo from .configuration import Configuration +from .door_control import DoorControl, API_DISCOVERY_ID as DOOR_CONTROL_ID from .errors import PathNotFound, RequestError, Unauthorized, raise_error from .event_instances import EventInstances from .light_control import API_DISCOVERY_ID as LIGHT_CONTROL_ID, LightControl @@ -51,6 +52,7 @@ def __init__(self, config: Configuration) -> None: self.applications: Optional[Applications] = None self.basic_device_info: Optional[BasicDeviceInfo] = None self.event_instances: Optional[EventInstances] = None + self.door_control: Optional[DoorControl] = None self.fence_guard: Optional[FenceGuard] = None self.light_control: Optional[LightControl] = None self.loitering_guard: Optional[LoiteringGuard] = None @@ -139,6 +141,7 @@ async def initialize_api_discovery(self) -> None: for api_id, api_class, api_attr in ( (BASIC_DEVICE_INFO_ID, BasicDeviceInfo, "basic_device_info"), (IO_PORT_MANAGEMENT_ID, IoPortManagement, "ports"), + (DOOR_CONTROL_ID, DoorControl, "door_control"), (LIGHT_CONTROL_ID, LightControl, "light_control"), (MQTT_ID, MqttClient, "mqtt"), (STREAM_PROFILES_ID, StreamProfiles, "stream_profiles"), diff --git a/tests/event_fixtures.py b/tests/event_fixtures.py index 55a9cf65..6658f05b 100644 --- a/tests/event_fixtures.py +++ b/tests/event_fixtures.py @@ -4,6 +4,8 @@ DAYNIGHT_INIT = b'\n\ntns1:VideoSource/tnsaxis:DayNightVisionuri://1c8ae81b-3b00-46cf-bf76-79cc3fa533dc/ProducerReference\n' +DOOR_MODE_INIT = b'\n\n tns1:Door/State/DoorModeuri://755cc9bb-cf3a-410b-bd1b-0ec97c6d6256/ProducerReference\n' + FENCE_GUARD_INIT = b'\n\ntnsaxis:CameraApplicationPlatform/FenceGuard/Camera1Profile1uri://755cc9bb-cf3a-410b-bd1b-0ec97c6d6256/ProducerReference\n' GLOBAL_SCENE_CHANGE = b'\n\ntns1:VideoSource/GlobalSceneChange/ImagingServiceuri://755cc9bb-cf3a-410b-bd1b-0ec97c6d6256/ProducerReference\n' diff --git a/tests/test_door_control.py b/tests/test_door_control.py new file mode 100644 index 00000000..41746716 --- /dev/null +++ b/tests/test_door_control.py @@ -0,0 +1,330 @@ +"""Test Axis Door Control API. + +pytest --cov-report term-missing --cov=axis.light_control tests/test_light_control.py +""" + +import json +import pytest + +import respx + +from axis.door_control import DoorControl +from .conftest import HOST + + +@pytest.fixture +def door_control(axis_device) -> DoorControl: + """Returns the door_control mock object.""" + return DoorControl(axis_device.vapix.request) + + +@respx.mock +async def test_update(door_control): + """Test update method.""" + route = respx.post(f"http://{HOST}:80/vapix/doorcontrol").respond( + json={"DoorInfo": [ + { + "token": "Axis-5fba94a4-8601-4627-bdda-cc408f69e026", + "Name": "Test Door 1", + "Description": "Test Door 1 Description", + "Capabilities": { + "Access": True, + "Lock": True, + "Unlock": True, + "Block": True, + "DoubleLock": True, + "LockDown": True, + "LockOpen": True, + "DoorMonitor": True, + "LockMonitor": False, + "DoubleLockMonitor": False, + "Alarm": True, + "Tamper": False, + "Warning": True, + "Configurable": True + } + }, + { + "token": "Axis-c2987ee0-28d5-4b53-8493-52977af927cf", + "Name": "Test Door 2", + "Description": "Test Door 2 Description", + "Capabilities": {} + } + ] + } + ) + + await door_control.update() + + assert route.called + assert route.calls.last.request.method == "POST" + assert route.calls.last.request.url.path == "/vapix/doorcontrol" + assert json.loads(route.calls.last.request.content) == {"tdc:GetDoorInfoList": {}} + + assert len(door_control.values()) == 2 + + item = door_control["Axis-5fba94a4-8601-4627-bdda-cc408f69e026"] + assert item.id == "Axis-5fba94a4-8601-4627-bdda-cc408f69e026" + assert item.door_token == "Axis-5fba94a4-8601-4627-bdda-cc408f69e026" + assert item.door_name == "Test Door 1" + assert item.door_description == "Test Door 1 Description" + + # TODO: Check to see that comparing dict using "==" actually does what I want it to do + assert item.door_capabilities == { + "Access": True, + "Lock": True, + "Unlock": True, + "Block": True, + "DoubleLock": True, + "LockDown": True, + "LockOpen": True, + "DoorMonitor": True, + "LockMonitor": False, + "DoubleLockMonitor": False, + "Alarm": True, + "Tamper": False, + "Warning": True, + "Configurable": True + } + + item2 = door_control["Axis-c2987ee0-28d5-4b53-8493-52977af927cf"] + assert item2.id == "Axis-c2987ee0-28d5-4b53-8493-52977af927cf" + assert item2.door_token == "Axis-c2987ee0-28d5-4b53-8493-52977af927cf" + assert item2.door_name == "Test Door 2" + assert item2.door_description == "Test Door 2 Description" + # TODO: Check to see that comparing dict using "==" actually does what I want it to do + assert item2.door_capabilities == {} + + +@respx.mock +async def test_get_service_capabilities(door_control): + """Test get service capabilities API.""" + route = respx.post(f"http://{HOST}:80/vapix/doorcontrol").respond( + json={ + "Capabilities": { + "MaxLimit": 100, + "GetDoorSupported": True, + "SetDoorSupported": True, + "PriorityConfigurationSupported": True + } + }, + ) + + response = await door_control.get_service_capabilities() + + assert route.called + assert route.calls.last.request.method == "POST" + assert route.calls.last.request.url.path == "/vapix/doorcontrol" + assert json.loads(route.calls.last.request.content) == {"tdc:GetServiceCapabilities": {}} + + assert response["Capabilities"] == { + "MaxLimit": 100, + "GetDoorSupported": True, + "SetDoorSupported": True, + "PriorityConfigurationSupported": True + } + + +@respx.mock +async def test_get_door_info_list(door_control): + """Test get door list.""" + route = respx.post(f"http://{HOST}:80/vapix/doorcontrol").respond( + json={ + "DoorInfo": [ + { + "token": "Axis-5fba94a4-8601-4627-bdda-cc408f69e026", + "Name": "Test Door 1", + "Description": "", + "Capabilities": { + "Access": True, + "Lock": True, + "Unlock": True, + "Block": True, + "DoubleLock": True, + "LockDown": True, + "LockOpen": True, + "DoorMonitor": True, + "LockMonitor": False, + "DoubleLockMonitor": False, + "Alarm": True, + "Tamper": False, + "Warning": True, + "Configurable": True + } + }, + { + "token": "Axis-c2987ee0-28d5-4b53-8493-52977af927cf", + "Name": "Test Door 2", + "Description": "", + "Capabilities": {} + } + ] + } + ) + + response = await door_control.get_door_info_list() + + assert route.called + assert route.calls.last.request.method == "POST" + assert route.calls.last.request.url.path == "/vapix/doorcontrol" + assert json.loads(route.calls.last.request.content) == {"tdc:GetDoorInfoList": {}} + + assert response["DoorInfo"] == [ + { + "token": "Axis-5fba94a4-8601-4627-bdda-cc408f69e026", + "Name": "Test Door 1", + "Description": "", + "Capabilities": { + "Access": True, + "Lock": True, + "Unlock": True, + "Block": True, + "DoubleLock": True, + "LockDown": True, + "LockOpen": True, + "DoorMonitor": True, + "LockMonitor": False, + "DoubleLockMonitor": False, + "Alarm": True, + "Tamper": False, + "Warning": True, + "Configurable": True + } + }, + { + "token": "Axis-c2987ee0-28d5-4b53-8493-52977af927cf", + "Name": "Test Door 2", + "Description": "", + "Capabilities": {} + } + ] + + +@respx.mock +async def test_get_door_info(door_control): + """Test get door info from token(s).""" + route = respx.post(f"http://{HOST}:80/vapix/doorcontrol").respond( + json={ + "DoorInfo": [ + { + "token": "Axis-5fba94a4-8601-4627-bdda-cc408f69e026", + "Name": "Test Door 1", + "Description": "", + "Capabilities": { + "Access": True, + "Lock": True, + "Unlock": True, + "Block": True, + "DoubleLock": True, + "LockDown": True, + "LockOpen": True, + "DoorMonitor": True, + "LockMonitor": False, + "DoubleLockMonitor": False, + "Alarm": True, + "Tamper": False, + "Warning": True, + "Configurable": True + } + } + ] + } + ) + + tokens = ["Axis-5fba94a4-8601-4627-bdda-cc408f69e026"] + + response = await door_control.get_door_info(tokens) + + assert route.called + assert route.calls.last.request.method == "POST" + assert route.calls.last.request.url.path == "/vapix/doorcontrol" + assert json.loads(route.calls.last.request.content) == { + "tdc:GetDoorInfo": {"Token": tokens} + } + + assert response["DoorInfo"] == [ + { + "token": "Axis-5fba94a4-8601-4627-bdda-cc408f69e026", + "Name": "Test Door 1", + "Description": "", + "Capabilities": { + "Access": True, + "Lock": True, + "Unlock": True, + "Block": True, + "DoubleLock": True, + "LockDown": True, + "LockOpen": True, + "DoorMonitor": True, + "LockMonitor": False, + "DoubleLockMonitor": False, + "Alarm": True, + "Tamper": False, + "Warning": True, + "Configurable": True + } + } + ] + + +@respx.mock +async def test_get_door_state(door_control): + """Test get door state(s).""" + route = respx.post(f"http://{HOST}:80/vapix/doorcontrol").respond( + json={ + "DoorState": { + "DoorPhysicalState": "Closed", + "Alarm": "Normal", + "DoorMode": "Locked" + } + } + ) + + token = "Axis-5fba94a4-8601-4627-bdda-cc408f69e026" + + response = await door_control.get_door_state(token) + + assert route.called + assert route.calls.last.request.method == "POST" + assert route.calls.last.request.url.path == "/vapix/doorcontrol" + assert json.loads(route.calls.last.request.content) == { + "tdc:GetDoorState": {"Token": token} + } + + assert response["DoorState"] == { + "DoorPhysicalState": "Closed", + "Alarm": "Normal", + "DoorMode": "Locked" + } + + +@pytest.mark.parametrize( + "input,expected", [ + ({"api_function": "tdc:AccessDoor", "method_name": "access_door"}, None), + ({"api_function": "tdc:LockDoor", "method_name": "lock_door"}, None), + ({"api_function": "tdc:UnlockDoor", "method_name": "unlock_door"}, None), + ({"api_function": "tdc:BlockDoor", "method_name": "block_door"}, None), + ({"api_function": "tdc:LockDownDoor", "method_name": "lock_down_door"}, None), + ({"api_function": "tdc:LockDownReleaseDoor", "method_name": "lock_down_release_door"}, None), + ({"api_function": "tdc:LockOpenDoor", "method_name": "lock_open_door"}, None), + ({"api_function": "tdc:LockOpenReleaseDoor", "method_name": "lock_open_release_door"}, None), + ({"api_function": "tdc:DoubleLockDoor", "method_name": "double_lock_door"}, None), + ({"api_function": "axtdc:ReleaseDoor", "method_name": "release_door"}, None), + ] +) +@respx.mock +async def test_door_requests(door_control, input: dict, expected: str): + route = respx.post(f"http://{HOST}:80/vapix/doorcontrol").respond( + json={} + ) + + token = "Axis-5fba94a4-8601-4627-bdda-cc408f69e026" + + response = await getattr(door_control, input["method_name"])(token) + + assert route.called + assert route.calls.last.request.method == "POST" + assert route.calls.last.request.url.path == "/vapix/doorcontrol" + assert json.loads(route.calls.last.request.content) == {input["api_function"]: {"Token": token}} + + assert response == expected diff --git a/tests/test_event_stream.py b/tests/test_event_stream.py index 9517278e..24b3cef3 100644 --- a/tests/test_event_stream.py +++ b/tests/test_event_stream.py @@ -12,6 +12,7 @@ FIRST_MESSAGE, AUDIO_INIT, DAYNIGHT_INIT, + DOOR_MODE_INIT, FENCE_GUARD_INIT, GLOBAL_SCENE_CHANGE, LIGHT_STATUS_INIT, @@ -55,66 +56,66 @@ def event_manager(axis_device) -> EventManager: [ (FIRST_MESSAGE, {}), ( - PIR_INIT, - { - "operation": "Initialized", - "topic": "tns1:Device/tnsaxis:Sensor/PIR", - "source": "sensor", - "source_idx": "0", - "type": "state", - "value": "0", - }, + PIR_INIT, + { + "operation": "Initialized", + "topic": "tns1:Device/tnsaxis:Sensor/PIR", + "source": "sensor", + "source_idx": "0", + "type": "state", + "value": "0", + }, ), ( - PIR_CHANGE, - { - "operation": "Changed", - "topic": "tns1:Device/tnsaxis:Sensor/PIR", - "source": "sensor", - "source_idx": "0", - "type": "state", - "value": "1", - }, + PIR_CHANGE, + { + "operation": "Changed", + "topic": "tns1:Device/tnsaxis:Sensor/PIR", + "source": "sensor", + "source_idx": "0", + "type": "state", + "value": "1", + }, ), ( - RULE_ENGINE_REGION_DETECTOR_INIT, - { - "operation": "Initialized", - "source": "VideoSource", - "source_idx": "0", - "topic": "tns1:RuleEngine/MotionRegionDetector/Motion", - "type": "State", - "value": "0", - }, + RULE_ENGINE_REGION_DETECTOR_INIT, + { + "operation": "Initialized", + "source": "VideoSource", + "source_idx": "0", + "topic": "tns1:RuleEngine/MotionRegionDetector/Motion", + "type": "State", + "value": "0", + }, ), ( - STORAGE_ALERT_INIT, - { - "operation": "Initialized", - "source": "disk_id", - "source_idx": "NetworkShare", - "topic": "tnsaxis:Storage/Alert", - "type": "overall_health", - "value": "-3", - }, + STORAGE_ALERT_INIT, + { + "operation": "Initialized", + "source": "disk_id", + "source_idx": "NetworkShare", + "topic": "tnsaxis:Storage/Alert", + "type": "overall_health", + "value": "-3", + }, ), ( - VMD4_ANY_INIT, - { - "operation": "Initialized", - "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1ProfileANY", - "type": "active", - "value": "0", - }, + VMD4_ANY_INIT, + { + "operation": "Initialized", + "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1ProfileANY", + "type": "active", + "value": "0", + }, ), ( - VMD4_ANY_CHANGE, - { - "operation": "Changed", - "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1ProfileANY", - "type": "active", - "value": "1", - }, + VMD4_ANY_CHANGE, + { + "operation": "Changed", + "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1ProfileANY", + "type": "active", + "value": "1", + }, ), ], ) @@ -127,184 +128,196 @@ def test_parse_event_xml(event_manager, input: bytes, expected: dict): "input,expected", [ ( - AUDIO_INIT, - { - "topic": "tns1:AudioSource/tnsaxis:TriggerLevel", - "source": "channel", - "source_idx": "1", - "class": "sound", - "type": "Sound", - "state": "0", - "tripped": False, - }, + AUDIO_INIT, + { + "topic": "tns1:AudioSource/tnsaxis:TriggerLevel", + "source": "channel", + "source_idx": "1", + "class": "sound", + "type": "Sound", + "state": "0", + "tripped": False, + }, ), ( - DAYNIGHT_INIT, - { - "topic": "tns1:VideoSource/tnsaxis:DayNightVision", - "source": "VideoSourceConfigurationToken", - "source_idx": "1", - "class": "light", - "type": "DayNight", - "state": "1", - "tripped": True, - }, + DAYNIGHT_INIT, + { + "topic": "tns1:VideoSource/tnsaxis:DayNightVision", + "source": "VideoSourceConfigurationToken", + "source_idx": "1", + "class": "light", + "type": "DayNight", + "state": "1", + "tripped": True, + }, ), ( - FENCE_GUARD_INIT, - { - "topic": "tnsaxis:CameraApplicationPlatform/FenceGuard/Camera1Profile1", - "source": "", - "source_idx": "Camera1Profile1", - "class": "motion", - "type": "Fence Guard", - "state": "0", - "tripped": False, - }, + DOOR_MODE_INIT, + { + "topic": "tns1:Door/State/DoorMode", + "source": "DoorToken", + "source_idx": "Axis-5fba94a4-8601-4627-bdda-cc408f69e026", + "class": "door", + "type": "Door Mode", + "state": "Accessed", + "tripped": False, + }, ), ( - LIGHT_STATUS_INIT, - { - "topic": "tns1:Device/tnsaxis:Light/Status", - "source": "id", - "source_idx": "0", - "class": "light", - "type": "Light", - "state": "OFF", - "tripped": False, - }, + FENCE_GUARD_INIT, + { + "topic": "tnsaxis:CameraApplicationPlatform/FenceGuard/Camera1Profile1", + "source": "", + "source_idx": "Camera1Profile1", + "class": "motion", + "type": "Fence Guard", + "state": "0", + "tripped": False, + }, ), ( - LOITERING_GUARD_INIT, - { - "topic": "tnsaxis:CameraApplicationPlatform/LoiteringGuard/Camera1Profile1", - "source": "", - "source_idx": "Camera1Profile1", - "class": "motion", - "type": "Loitering Guard", - "state": "0", - "tripped": False, - }, + LIGHT_STATUS_INIT, + { + "topic": "tns1:Device/tnsaxis:Light/Status", + "source": "id", + "source_idx": "0", + "class": "light", + "type": "Light", + "state": "OFF", + "tripped": False, + }, ), ( - MOTION_GUARD_INIT, - { - "topic": "tnsaxis:CameraApplicationPlatform/MotionGuard/Camera1ProfileANY", - "source": "", - "source_idx": "Camera1ProfileANY", - "class": "motion", - "type": "Motion Guard", - "state": "0", - "tripped": False, - }, + LOITERING_GUARD_INIT, + { + "topic": "tnsaxis:CameraApplicationPlatform/LoiteringGuard/Camera1Profile1", + "source": "", + "source_idx": "Camera1Profile1", + "class": "motion", + "type": "Loitering Guard", + "state": "0", + "tripped": False, + }, ), ( - OBJECT_ANALYTICS_INIT, - { - "topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1", - "source": "", - "source_idx": "Device1Scenario1", - "class": "motion", - "type": "Object Analytics", - "state": "0", - "tripped": False, - }, + MOTION_GUARD_INIT, + { + "topic": "tnsaxis:CameraApplicationPlatform/MotionGuard/Camera1ProfileANY", + "source": "", + "source_idx": "Camera1ProfileANY", + "class": "motion", + "type": "Motion Guard", + "state": "0", + "tripped": False, + }, ), ( - PIR_INIT, - { - "topic": "tns1:Device/tnsaxis:Sensor/PIR", - "source": "sensor", - "source_idx": "0", - "class": "motion", - "type": "PIR", - "state": "0", - "tripped": False, - }, + OBJECT_ANALYTICS_INIT, + { + "topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1", + "source": "", + "source_idx": "Device1Scenario1", + "class": "motion", + "type": "Object Analytics", + "state": "0", + "tripped": False, + }, ), ( - PORT_0_INIT, - { - "topic": "tns1:Device/tnsaxis:IO/Port", - "source": "port", - "source_idx": "1", - "class": "input", - "type": "Input", - "state": "0", - "tripped": False, - }, + PIR_INIT, + { + "topic": "tns1:Device/tnsaxis:Sensor/PIR", + "source": "sensor", + "source_idx": "0", + "class": "motion", + "type": "PIR", + "state": "0", + "tripped": False, + }, ), ( - PORT_ANY_INIT, - { - "topic": "tns1:Device/tnsaxis:IO/Port", - "source": "port", - "source_idx": "", - "class": "input", - "type": "Input", - "state": "0", - "tripped": False, - }, + PORT_0_INIT, + { + "topic": "tns1:Device/tnsaxis:IO/Port", + "source": "port", + "source_idx": "1", + "class": "input", + "type": "Input", + "state": "0", + "tripped": False, + }, ), ( - PTZ_MOVE_INIT, - { - "topic": "tns1:PTZController/tnsaxis:Move/Channel_1", - "source": "PTZConfigurationToken", - "source_idx": "1", - "class": "ptz", - "type": "is_moving", - "state": "0", - "tripped": False, - }, + PORT_ANY_INIT, + { + "topic": "tns1:Device/tnsaxis:IO/Port", + "source": "port", + "source_idx": "", + "class": "input", + "type": "Input", + "state": "0", + "tripped": False, + }, ), ( - PTZ_PRESET_INIT_1, - { - "topic": "tns1:PTZController/tnsaxis:PTZPresets/Channel_1", - "source": "PresetToken", - "source_idx": "1", - "class": "ptz", - "type": "on_preset", - "state": "1", - "tripped": True, - }, + PTZ_MOVE_INIT, + { + "topic": "tns1:PTZController/tnsaxis:Move/Channel_1", + "source": "PTZConfigurationToken", + "source_idx": "1", + "class": "ptz", + "type": "is_moving", + "state": "0", + "tripped": False, + }, ), ( - RELAY_INIT, - { - "topic": "tns1:Device/Trigger/Relay", - "source": "RelayToken", - "source_idx": "3", - "class": "output", - "type": "Relay", - "state": "inactive", - "tripped": False, - }, + PTZ_PRESET_INIT_1, + { + "topic": "tns1:PTZController/tnsaxis:PTZPresets/Channel_1", + "source": "PresetToken", + "source_idx": "1", + "class": "ptz", + "type": "on_preset", + "state": "1", + "tripped": True, + }, ), ( - VMD3_INIT, - { - "topic": "tns1:RuleEngine/tnsaxis:VMD3/vmd3_video_1", - "source": "areaid", - "source_idx": "0", - "class": "motion", - "type": "VMD3", - "state": "0", - "tripped": False, - }, + RELAY_INIT, + { + "topic": "tns1:Device/Trigger/Relay", + "source": "RelayToken", + "source_idx": "3", + "class": "output", + "type": "Relay", + "state": "inactive", + "tripped": False, + }, ), ( - VMD4_ANY_INIT, - { - "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1ProfileANY", - "source": "", - "source_idx": "Camera1ProfileANY", - "class": "motion", - "type": "VMD4", - "state": "0", - "tripped": False, - }, + VMD3_INIT, + { + "topic": "tns1:RuleEngine/tnsaxis:VMD3/vmd3_video_1", + "source": "areaid", + "source_idx": "0", + "class": "motion", + "type": "VMD3", + "state": "0", + "tripped": False, + }, + ), + ( + VMD4_ANY_INIT, + { + "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1ProfileANY", + "source": "", + "source_idx": "Camera1ProfileANY", + "class": "motion", + "type": "VMD4", + "state": "0", + "tripped": False, + }, ), ], )