Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 246 additions & 0 deletions axis/door_control.py
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove device examples, access control is enough, I think video door stations might be applicable here as well and I don't want to keep updating the list.

"""

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?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Time to remove it? ;)

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:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whats the reasoning behind this? When will this be used? It doesnt seem to have test coverage when looking for the method

"""Retrieve whether door has the specified capability."""
if capability not in SUPPORTED_CAPABILITIES:
return False
return self.raw["Capabilities"][capability]
73 changes: 73 additions & 0 deletions axis/event_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

LOGGER = logging.getLogger(__name__)

CLASS_DOOR = "door"
CLASS_INPUT = "input"
CLASS_LIGHT = "light"
CLASS_MOTION = "motion"
Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -338,6 +403,14 @@ def id(self) -> str:
EVENT_CLASSES = (
Audio,
DayNight,
DoorAlarm,
DoorDoubleLockPhysical,
DoorFault,
DoorLockPhysical,
DoorMode,
DoorPhysical,
DoorWarning,
DoorTamper,
FenceGuard,
Input,
Light,
Expand Down
3 changes: 3 additions & 0 deletions axis/vapix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"),
Expand Down
2 changes: 2 additions & 0 deletions tests/event_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

DAYNIGHT_INIT = b'<?xml version="1.0" encoding="UTF-8"?>\n<tt:MetadataStream xmlns:tt="http://www.onvif.org/ver10/schema">\n<tt:Event><wsnt:NotificationMessage xmlns:tns1="http://www.onvif.org/ver10/topics" xmlns:tnsaxis="http://www.axis.com/2009/event/topics" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:wsa5="http://www.w3.org/2005/08/addressing"><wsnt:Topic Dialect="http://docs.oasis-open.org/wsn/t-1/TopicExpression/Simple">tns1:VideoSource/tnsaxis:DayNightVision</wsnt:Topic><wsnt:ProducerReference><wsa5:Address>uri://1c8ae81b-3b00-46cf-bf76-79cc3fa533dc/ProducerReference</wsa5:Address></wsnt:ProducerReference><wsnt:Message><tt:Message UtcTime="2019-02-06T18:58:51.007104Z" PropertyOperation="Initialized"><tt:Source><tt:SimpleItem Name="VideoSourceConfigurationToken" Value="1"/></tt:Source><tt:Key></tt:Key><tt:Data><tt:SimpleItem Name="day" Value="1"/></tt:Data></tt:Message></wsnt:Message></wsnt:NotificationMessage></tt:Event></tt:MetadataStream>\n'

DOOR_MODE_INIT = b'<?xml version="1.0" encoding="UTF-8"?>\n<tt:MetadataStream xmlns:tt="http://www.onvif.org/ver10/schema">\n <tt:Event><wsnt:NotificationMessage xmlns:tns1="http://www.onvif.org/ver10/topics" xmlns:tnsaxis="http://www.axis.com/2009/event/topics" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:wsa5="http://www.w3.org/2005/08/addressing"><wsnt:Topic Dialect="http://docs.oasis-open.org/wsn/t-1/TopicExpression/Simple">tns1:Door/State/DoorMode</wsnt:Topic><wsnt:ProducerReference><wsa5:Address>uri://755cc9bb-cf3a-410b-bd1b-0ec97c6d6256/ProducerReference</wsa5:Address></wsnt:ProducerReference><wsnt:Message><tt:Message UtcTime="2020-09-05T04:25:51.692744Z" PropertyOperation="Initialized"><tt:Source><tt:SimpleItem Name="DoorToken" Value="Axis-5fba94a4-8601-4627-bdda-cc408f69e026"/></tt:Source><tt:Key></tt:Key><tt:Data><tt:SimpleItem Name="state" Value="Accessed"/></tt:Data></tt:Message></wsnt:Message></wsnt:NotificationMessage></tt:Event></tt:MetadataStream>\n'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you generate additional events? The better coverage and examples the easier it will be to maintain


FENCE_GUARD_INIT = b'<?xml version="1.0" encoding="UTF-8"?>\n<tt:MetadataStream xmlns:tt="http://www.onvif.org/ver10/schema">\n<tt:Event><wsnt:NotificationMessage xmlns:tns1="http://www.onvif.org/ver10/topics" xmlns:tnsaxis="http://www.axis.com/2009/event/topics" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:wsa5="http://www.w3.org/2005/08/addressing"><wsnt:Topic Dialect="http://docs.oasis-open.org/wsn/t-1/TopicExpression/Simple">tnsaxis:CameraApplicationPlatform/FenceGuard/Camera1Profile1</wsnt:Topic><wsnt:ProducerReference><wsa5:Address>uri://755cc9bb-cf3a-410b-bd1b-0ec97c6d6256/ProducerReference</wsa5:Address></wsnt:ProducerReference><wsnt:Message><tt:Message UtcTime="2020-09-04T20:08:34.701060Z" PropertyOperation="Initialized"><tt:Source></tt:Source><tt:Key></tt:Key><tt:Data><tt:SimpleItem Name="active" Value="0"/></tt:Data></tt:Message></wsnt:Message></wsnt:NotificationMessage></tt:Event></tt:MetadataStream>\n'

GLOBAL_SCENE_CHANGE = b'<?xml version="1.0" encoding="UTF-8"?>\n<tt:MetadataStream xmlns:tt="http://www.onvif.org/ver10/schema">\n<tt:Event><wsnt:NotificationMessage xmlns:tns1="http://www.onvif.org/ver10/topics" xmlns:tnsaxis="http://www.axis.com/2009/event/topics" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:wsa5="http://www.w3.org/2005/08/addressing"><wsnt:Topic Dialect="http://docs.oasis-open.org/wsn/t-1/TopicExpression/Simple">tns1:VideoSource/GlobalSceneChange/ImagingService</wsnt:Topic><wsnt:ProducerReference><wsa5:Address>uri://755cc9bb-cf3a-410b-bd1b-0ec97c6d6256/ProducerReference</wsa5:Address></wsnt:ProducerReference><wsnt:Message><tt:Message UtcTime="2020-09-05T04:45:30.199233Z" PropertyOperation="Initialized"><tt:Source><tt:SimpleItem Name="Source" Value="0"/></tt:Source><tt:Key></tt:Key><tt:Data><tt:SimpleItem Name="State" Value="0"/></tt:Data></tt:Message></wsnt:Message></wsnt:NotificationMessage></tt:Event></tt:MetadataStream>\n'
Expand Down
Loading