Skip to content

Commit 322d6f3

Browse files
authored
PTZ Support (#487)
Support added for PTZ cameras. There is a new entity that allows you to move the camera to a preset position.
1 parent d463a89 commit 322d6f3

File tree

6 files changed

+128
-0
lines changed

6 files changed

+128
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ Service | Parameters | Description
229229
`camera.enable_motion_detection` | | Enables motion detection
230230
`camera.disable_motion_detection` | | Disabled motion detection
231231
`dahua.set_infrared_mode` | `target`: camera.cam13_main <br /> `mode`: Auto, On, Off <br /> `brightness`: 0 - 100 inclusive| Sets the infrared mode. Useful to set the mode back to Auto
232+
`dahua.goto_preset_position` | `target`: camera.cam13_main <br /> `position`: 1 - 10 inclusive| Go to a preset position
232233
`dahua.set_video_profile_mode` | `target`: camera.cam13_main <br /> `mode`: Day, Night| Sets the video profile mode to day or night
233234
`dahua.set_focus_zoom` | `target`: camera.cam13_main <br /> `focus`: The focus level, e.g.: 0.81 0 - 1 inclusive <br /> `zoom`: The zoom level, e.g.: 0.72 0 - 1 inclusive | Sets the focus and zoom level
234235
`dahua.set_channel_title` | `target`: camera.cam13_main <br /> `channel`: The camera channel, e.g.: 0 <br /> `text1`: The text 1<br /> `text2`: The text 2| Sets the channel title

custom_components/dahua/__init__.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,12 @@ def __init__(self, hass: HomeAssistant, events: list, address: str, port: int, r
112112
self._supports_disarming_linkage = False
113113
self._supports_event_notifications = False
114114
self._supports_smart_motion_detection = False
115+
self._supports_ptz_position = False
115116
self._supports_lighting = False
116117
self._supports_floodlightmode = False
117118
self._serial_number: str
118119
self._profile_mode = "0"
120+
self._preset_position = "0"
119121
self._supports_profile_mode = False
120122
self._channel = channel
121123
self._address = address
@@ -247,6 +249,15 @@ async def _async_update_data(self):
247249
self._supports_event_notifications = False
248250
_LOGGER.info("Device supports event notifications=%s", self._supports_event_notifications)
249251

252+
# PTZ
253+
# The following lines are for Dahua devices
254+
try:
255+
await self.client.async_get_ptz_position()
256+
self._supports_ptz_position = True
257+
except ClientError:
258+
self._supports_ptz_position = False
259+
_LOGGER.info("Device supports PTZ position=%s", self._supports_ptz_position)
260+
250261
# Smart motion detection is enabled/disabled/fetched differently on Dahua devices compared to Amcrest
251262
# The following lines are for Dahua devices
252263
try:
@@ -320,6 +331,19 @@ async def _async_update_data(self):
320331
# I believe this API is missing on some cameras so we'll just ignore it and move on
321332
_LOGGER.debug("Could not get profile mode", exc_info=exception)
322333
pass
334+
335+
# We need the ptz status
336+
if self._supports_ptz_position:
337+
try:
338+
ptz_data = await self.client.async_get_ptz_position()
339+
data.update(ptz_data)
340+
self._preset_position = ptz_data.get("status.PresetID", "0")
341+
if not self._preset_position:
342+
self._preset_position = "0"
343+
except Exception as exception:
344+
# I believe this API is missing on some cameras so we'll just ignore it and move on
345+
_LOGGER.debug("Could not get preset position", exc_info=exception)
346+
pass
323347

324348
# Figure out which APIs we need to call and then fan out and gather the results
325349
coros = [
@@ -598,6 +622,12 @@ def supports_illuminator(self) -> bool:
598622
IPC-HDW3849HP-AS-PV does
599623
"""
600624
return not (self.is_amcrest_doorbell() or self.is_flood_light()) and "table.Lighting_V2[{0}][0][0].Mode".format(self._channel) in self.data
625+
626+
def supports_ptz_position(self) -> bool:
627+
"""
628+
Returns true if this camera supports PTZ preset position
629+
"""
630+
return not (self.is_amcrest_doorbell() or self.is_flood_light()) and "table.Lighting_V2[{0}][0][0].Mode".format(self._channel) in self.data
601631

602632
def is_motion_detection_enabled(self) -> bool:
603633
""" Returns true if motion detection is enabled for the camera """

custom_components/dahua/camera.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
SERVICE_VTO_CANCEL_CALL = "vto_cancel_call"
3838
SERVICE_SET_DAY_NIGHT_MODE = "set_video_in_day_night_mode"
3939
SERVICE_REBOOT = "reboot"
40+
SERVICE_GOTO_PRESET_POSITION = "goto_preset_position"
4041

4142

4243
async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
@@ -224,6 +225,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entitie
224225
"async_set_infrared_mode"
225226
)
226227

228+
platform.async_register_entity_service(
229+
SERVICE_GOTO_PRESET_POSITION,
230+
{
231+
vol.Required('position', default=1): vol.All(vol.Coerce(int), vol.Range(min=1, max=10)),
232+
},
233+
"async_goto_preset_position"
234+
)
227235

228236
class DahuaCamera(DahuaBaseEntity, Camera):
229237
"""An implementation of a Dahua IP camera."""
@@ -295,6 +303,12 @@ async def async_set_infrared_mode(self, mode: str, brightness: int):
295303
await self._coordinator.client.async_set_lighting_v1_mode(channel, mode, brightness)
296304
await self._coordinator.async_refresh()
297305

306+
async def async_goto_preset_position(self, position: int):
307+
""" Handles the service call from SERVICE_GOTO_PRESET_POSITION to go to a specific preset position """
308+
channel = self._coordinator.get_channel()
309+
await self._coordinator.client.async_goto_preset_position(channel, position)
310+
await self._coordinator.async_refresh()
311+
298312
async def async_set_video_in_day_night_mode(self, config_type: str, mode: str):
299313
""" Handles the service call from SERVICE_SET_DAY_NIGHT_MODE to set the day/night color mode """
300314
channel = self._coordinator.get_channel()

custom_components/dahua/client.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,23 @@ async def async_get_smart_motion_detection(self) -> dict:
297297
"""
298298
url = "/cgi-bin/configManager.cgi?action=getConfig&name=SmartMotionDetect"
299299
return await self.get(url)
300+
301+
async def async_get_ptz_position(self) -> dict:
302+
"""
303+
Gets the status of PTZ Example output:
304+
status.Action=Preset
305+
status.MoveStatus=Idle
306+
status.PTS=0
307+
status.Postion[0]=91.600000
308+
status.Postion[1]=-2.600000
309+
status.Postion[2]=1.000000
310+
status.PresetID=2
311+
status.Sequence=0
312+
status.UTC=0
313+
status.ZoomStatus=Idle
314+
"""
315+
url = "/cgi-bin/ptz.cgi?action=getStatus"
316+
return await self.get(url)
300317

301318
async def async_get_light_global_enabled(self) -> dict:
302319
"""
@@ -348,6 +365,17 @@ async def async_set_lighting_v1_mode(self, channel: int, mode: str, brightness:
348365
channel=channel, mode=mode, brightness=brightness
349366
)
350367
return await self.get(url)
368+
369+
async def async_goto_preset_position(self, channel: int, position: int) -> dict:
370+
"""
371+
async_goto_preset_position will go to a specific preset position
372+
Position should be between 1 and 10 inclusive.
373+
"""
374+
375+
url = "/cgi-bin/ptz.cgi?action=start&channel={0}&code=GotoPreset&arg1=0&arg2={1}&arg3=0".format(
376+
channel, position
377+
)
378+
return await self.get(url)
351379

352380
async def async_set_video_profile_mode(self, channel: int, mode: str):
353381
"""

custom_components/dahua/select.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices):
2020
if coordinator.is_amcrest_doorbell() and coordinator.supports_security_light():
2121
devices.append(DahuaDoorbellLightSelect(coordinator, entry))
2222

23+
#if coordinator._supports_ptz_position:
24+
devices.append(DahuaCameraPresetPositionSelect(coordinator, entry))
25+
2326
async_add_devices(devices)
2427

2528

@@ -59,3 +62,36 @@ def name(self):
5962
def unique_id(self):
6063
""" https://developers.home-assistant.io/docs/entity_registry_index/#unique-id-requirements """
6164
return self._attr_unique_id
65+
66+
67+
class DahuaCameraPresetPositionSelect(DahuaBaseEntity, SelectEntity):
68+
"""allows """
69+
70+
def __init__(self, coordinator: DahuaDataUpdateCoordinator, config_entry):
71+
DahuaBaseEntity.__init__(self, coordinator, config_entry)
72+
SelectEntity.__init__(self)
73+
self._coordinator = coordinator
74+
self._attr_name = f"{coordinator.get_device_name()} Preset Position"
75+
self._attr_unique_id = f"{coordinator.get_serial_number()}_preset_position"
76+
self._attr_options = ["Manual","1","2","3","4","5","6","7","8","9","10"]
77+
78+
@property
79+
def current_option(self) -> str:
80+
presetID = self._coordinator.data.get("status.PresetID", "0")
81+
if presetID == "0":
82+
return "Manual"
83+
return presetID
84+
85+
async def async_select_option(self, option: str) -> None:
86+
channel = self._coordinator.get_channel()
87+
await self._coordinator.client.async_goto_preset_position(channel, int(option))
88+
await self._coordinator.async_refresh()
89+
90+
@property
91+
def name(self):
92+
return self._attr_name
93+
94+
@property
95+
def unique_id(self):
96+
""" https://developers.home-assistant.io/docs/entity_registry_index/#unique-id-requirements """
97+
return self._attr_unique_id

custom_components/dahua/services.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,3 +444,22 @@ set_privacy_masking:
444444
default: true
445445
selector:
446446
boolean:
447+
448+
goto_preset_position:
449+
name: Go to a preset position
450+
description: Go to a position already preset
451+
target:
452+
entity:
453+
integration: dahua
454+
domain: camera
455+
fields:
456+
position:
457+
name: Position
458+
description: Position number, from 1 to 10 inclusive.
459+
example: 1
460+
default: 1
461+
selector:
462+
number:
463+
min: 1
464+
max: 10
465+
mode: box

0 commit comments

Comments
 (0)