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,
+ },
),
],
)