Skip to content

Commit a716971

Browse files
committed
Add a service to cancle a VTO call
1 parent 2d3807c commit a716971

File tree

7 files changed

+66
-54
lines changed

7 files changed

+66
-54
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,8 @@ Service | Parameters | Description
195195
`dahua.set_record_mode` | `target`: camera.cam13_main <br /> `mode`: Auto, On, Off | Sets the record mode. On is always on recording. Off is always off. Auto based on motion settings, etc.
196196
`dahua.enable_all_ivs_rules` | `target`: camera.cam13_main <br /> `channel`: The camera channel, e.g.: 0 <br /> `enabled`: True to enable all IVS rules, False to disable all IVS rules | Enables or disables all IVS rules
197197
`dahua.enable_ivs_rule` | `target`: camera.cam13_main <br /> `channel`: The camera channel, e.g.: 0 <br /> `index`: The rule index <br /> enabled`: True to enable the IVS rule, False to disable the IVS rule | Enable or disable an IVS rule
198-
`dahua.vto_open_door` | `target`: camera.cam13_main <br /> `door_id`: The door ID to open, e.g.: 1 <br /> | Opens a door via a VTO
198+
`dahua.vto_open_door` | `target`: camera.cam13_main <br /> `door_id`: The door ID to open, e.g.: 1 <br /> Opens a door via a VTO
199+
`dahua.vto_cancel_call` | `target`: camera.cam13_main <br />Cancels a call on a VTO device (Doorbell)
199200
`dahua.set_video_in_day_night_mode` | `target`: camera.cam13_main <br /> `config_type`: The config type: general, day, night <br /> `mode`: The mode: Auto, Color, BlackWhite. Note Auto is also known as Brightness by Dahua|Set the camera's Day/Night Mode. For example, Color, BlackWhite, or Auto
200201

201202

custom_components/dahua/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
STARTUP_MESSAGE,
3535
CONF_CHANNEL,
3636
)
37+
from .vto import DahuaVTOClient
3738

3839
SCAN_INTERVAL_SECONDS = timedelta(seconds=30)
3940

@@ -605,6 +606,13 @@ def supports_smart_motion_detection(self) -> bool:
605606
""" True if smart motion detection is supported"""
606607
return self._supports_smart_motion_detection
607608

609+
def get_vto_client(self) -> DahuaVTOClient:
610+
"""
611+
Returns an instance of the connected VTO client if this is a VTO device. We need this because there's different
612+
ways to call a VTO device and the VTO client will handle that. For example, to hang up a call
613+
"""
614+
return self.dahua_vto_event_thread.vto_client
615+
608616

609617
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
610618
"""Handle removal of an entry."""

custom_components/dahua/button.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
Button entity platform for Dahua.
3+
https://developers.home-assistant.io/docs/core/entity/button
4+
Buttons require HomeAssistant 2021.12 or greater
5+
"""
6+
from homeassistant.core import HomeAssistant
7+
8+
9+
async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices):
10+
"""Setup the button platform."""
11+
# TODO: Add some buttons. This requires a pretty recent version of HomeAssistant so I'm waiting a bit longer
12+
# before adding buttons

custom_components/dahua/camera.py

+11
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
SERVICE_ENABLE_ALL_IVS_RULES = "enable_all_ivs_rules"
3333
SERVICE_ENABLE_IVS_RULE = "enable_ivs_rule"
3434
SERVICE_VTO_OPEN_DOOR = "vto_open_door"
35+
SERVICE_VTO_CANCEL_CALL = "vto_cancel_call"
3536
SERVICE_SET_DAY_NIGHT_MODE = "set_video_in_day_night_mode"
3637

3738

@@ -130,6 +131,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entitie
130131
"async_vto_open_door"
131132
)
132133

134+
platform.async_register_entity_service(
135+
SERVICE_VTO_CANCEL_CALL,
136+
{},
137+
"async_vto_cancel_call"
138+
)
139+
133140
platform.async_register_entity_service(
134141
SERVICE_SET_CHANNEL_TITLE,
135142
{
@@ -317,6 +324,10 @@ async def async_vto_open_door(self, door_id: int):
317324
""" Handles the service call from SERVICE_VTO_OPEN_DOOR """
318325
await self._coordinator.client.async_access_control_open_door(door_id)
319326

327+
async def async_vto_cancel_call(self):
328+
""" Handles the service call from SERVICE_VTO_CANCEL_CALL to cancel VTO calls """
329+
await self._coordinator.get_vto_client().cancel_call()
330+
320331
async def async_set_service_set_channel_title(self, text1: str, text2: str):
321332
""" Handles the service call from SERVICE_SET_CHANNEL_TITLE to set profile mode to day/night """
322333
channel = self._coordinator.get_channel()

custom_components/dahua/services.yaml

+9
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,12 @@ vto_open_door:
369369
mode: box
370370
min: 0
371371
max: 100
372+
373+
vto_cancel_call:
374+
name: Cancel VTO call
375+
description: Cancels a VTO call
376+
target:
377+
entity:
378+
integration: dahua
379+
domain: camera
380+

custom_components/dahua/thread.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def __init__(self, hass: HomeAssistant, client: DahuaClient, on_receive_vto_even
8686
self._username = username
8787
self._password = password
8888
self._is_ssl = False
89+
self.vto_client = None
8990

9091
def run(self):
9192
"""Fetch VTO events"""
@@ -104,9 +105,19 @@ def run(self):
104105
# how well do we know when we are shutting down HA?
105106
loop = asyncio.new_event_loop()
106107

107-
client = loop.create_connection(
108-
lambda: DahuaVTOClient(self._host, self._username, self._password, self._is_ssl,
109-
self.on_receive_vto_event), host=self._host, port=self._port)
108+
def vto_client_lambda():
109+
# Notice how we set vto_client client here. This is so nasty, I'm embarrassed to put this into the
110+
# code, but I'm not a python expert and it works well enough and this is just a spare time project
111+
# so here it is. We need to capture an instance of the DahuaVTOClient so we can use it later on
112+
# in switches to execute commands on the VTO. We need the client connected to the event loop
113+
# which is done through loop.create_connection. This makes it awkward to capture... which is why
114+
# I've done this. I'm sure there's a better way :)
115+
self.vto_client = DahuaVTOClient(self._host, self._username, self._password, self._is_ssl,
116+
self.on_receive_vto_event)
117+
return self.vto_client
118+
119+
client = loop.create_connection(vto_client_lambda, host=self._host, port=self._port)
120+
110121
loop.run_until_complete(client)
111122
loop.run_forever()
112123
loop.close()

custom_components/dahua/vto.py

+10-50
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@
3737
DAHUA_SERIAL_NUMBER
3838
]
3939

40-
ENDPOINT_ACCESS_CONTROL = "accessControl.cgi?action=openDoor&UserID=101&Type=Remote&channel="
41-
4240

4341
class DahuaVTOClient(asyncio.Protocol):
4442
requestId: int
@@ -76,11 +74,10 @@ def __init__(self, host: str, username: str, password: str, is_ssl: bool, on_rec
7674

7775
# This is the hook back into HA
7876
self.on_receive_vto_event = on_receive_vto_event
79-
8077
self._loop = asyncio.get_event_loop()
8178

8279
def connection_made(self, transport):
83-
_LOGGER.debug("Connection established")
80+
_LOGGER.debug("VTO connection established")
8481

8582
try:
8683
self.transport = transport
@@ -266,6 +263,15 @@ def handle_access_control(message):
266263

267264
self.send(DAHUA_CONFIG_MANAGER_GETCONFIG, handle_access_control, request_data)
268265

266+
async def cancel_call(self):
267+
_LOGGER.info("Cancelling call on VTO")
268+
269+
def cancel(message):
270+
_LOGGER.info(f"Got cancel call response: {message}")
271+
272+
self.send("console.runCmd", cancel, {"command": "hc"})
273+
return True
274+
269275
def load_version(self):
270276
_LOGGER.info("Get version")
271277

@@ -326,52 +332,6 @@ def handle_keep_alive(message):
326332

327333
self.send(DAHUA_GLOBAL_KEEPALIVE, handle_keep_alive, request_data)
328334

329-
def access_control_open_door(self, door_id: int = 1):
330-
is_locked = self.lock_status.get(door_id, False)
331-
should_unlock = False
332-
333-
try:
334-
if is_locked:
335-
_LOGGER.info(f"Access Control - Door #{door_id} is already unlocked, ignoring request")
336-
337-
else:
338-
is_locked = True
339-
should_unlock = True
340-
341-
self.lock_status[door_id] = is_locked
342-
self.publish_lock_state(door_id, False)
343-
344-
url = f"{self.base_url}{ENDPOINT_ACCESS_CONTROL}{door_id}"
345-
346-
response = requests.get(url, verify=False, auth=self.auth)
347-
348-
response.raise_for_status()
349-
350-
except Exception as ex:
351-
exc_type, exc_obj, exc_tb = sys.exc_info()
352-
353-
_LOGGER.error(f"Failed to open door, error: {ex}, Line: {exc_tb.tb_lineno}")
354-
355-
if should_unlock and is_locked:
356-
Timer(float(self.hold_time), self.magnetic_unlock, (self, door_id)).start()
357-
358-
@staticmethod
359-
def magnetic_unlock(self, door_id):
360-
self.lock_status[door_id] = False
361-
self.publish_lock_state(door_id, True)
362-
363-
def publish_lock_state(self, door_id: int, is_locked: bool):
364-
state = "Locked" if is_locked else "Unlocked"
365-
366-
_LOGGER.info(f"Access Control - {state} magnetic lock #{door_id}")
367-
368-
message = {
369-
"door": door_id,
370-
"isLocked": is_locked
371-
}
372-
373-
_LOGGER.info("locked %s", json.dumps(message, indent=4))
374-
375335
@staticmethod
376336
def parse_response(response):
377337
result = []

0 commit comments

Comments
 (0)