Skip to content
Closed
21 changes: 8 additions & 13 deletions homeassistant/components/camera/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,13 @@ record:
description: (Optional) Target lookback period (in seconds) to include in addition to duration. Only available if there is currently an active HLS stream.
example: 4

onvif_ptz:
description: Pan/Tilt/Zoom service for ONVIF camera.

local_file_update_file_path:
description: Update the file_path for a local_file camera.
fields:
entity_id:
description: Name(s) of entities to pan, tilt or zoom.
example: 'camera.living_room_camera'
pan:
description: "Direction of pan. Allowed values: LEFT, RIGHT."
example: 'LEFT'
tilt:
description: "Direction of tilt. Allowed values: DOWN, UP."
example: 'DOWN'
zoom:
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
example: "ZOOM_IN"
description: Name(s) of entities to update.
example: 'camera.local_file'
file_path:
description: Path to the new image file.
example: '/images/newimage.jpg'
101 changes: 65 additions & 36 deletions homeassistant/components/onvif/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@
from zeep.exceptions import Fault

from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
from homeassistant.components.camera.const import DOMAIN
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.service import async_extract_entity_ids
import homeassistant.util.dt as dt_util

from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST,
Expand All @@ -28,39 +33,33 @@
CONF_PORT,
CONF_USERNAME,
)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.service import async_extract_entity_ids
import homeassistant.util.dt as dt_util

_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME = "ONVIF Camera"
DEFAULT_PORT = 5000
DEFAULT_USERNAME = "admin"
DEFAULT_PASSWORD = "888888"
DEFAULT_ARGUMENTS = "-pred 1"
DEFAULT_PROFILE = 0

CONF_PROFILE = "profile"

ATTR_PAN = "pan"
ATTR_TILT = "tilt"
ATTR_ZOOM = "zoom"

DIR_UP = "UP"
DIR_DOWN = "DOWN"
DIR_LEFT = "LEFT"
DIR_RIGHT = "RIGHT"
ZOOM_OUT = "ZOOM_OUT"
ZOOM_IN = "ZOOM_IN"
PTZ_NONE = "NONE"

SERVICE_PTZ = "onvif_ptz"
from .const import (
ATTR_PAN,
ATTR_TILT,
ATTR_ZOOM,
CONF_PROFILE,
DEFAULT_ARGUMENTS,
DEFAULT_NAME,
DEFAULT_PASSWORD,
DEFAULT_PORT,
DEFAULT_PROFILE,
DEFAULT_USERNAME,
DIR_DOWN,
DIR_LEFT,
DIR_RIGHT,
DIR_UP,
DOMAIN,
ENTITIES,
ONVIF_DATA,
PTZ_NONE,
SERVICE_PTZ,
SERVICE_REBOOT,
ZOOM_IN,
ZOOM_OUT,
)

ONVIF_DATA = "onvif"
ENTITIES = "entities"
_LOGGER = logging.getLogger(__name__)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
Expand All @@ -85,6 +84,8 @@
}
)

SERVICE_REBOOT_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up a ONVIF camera."""
Expand All @@ -111,6 +112,24 @@ async def async_handle_ptz(service):
DOMAIN, SERVICE_PTZ, async_handle_ptz, schema=SERVICE_PTZ_SCHEMA
)

async def async_handle_reboot(service):
"""Handle PTZ service call."""
all_cameras = hass.data[ONVIF_DATA][ENTITIES]
entity_ids = await async_extract_entity_ids(hass, service)
target_cameras = []
if not entity_ids:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We are dropping this logic for all other integrations in #29178. Let's not use it here.

target_cameras = all_cameras
else:
target_cameras = [
camera for camera in all_cameras if camera.entity_id in entity_ids
]
for camera in target_cameras:
await camera.async_perform_reboot()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we speed this up by awaiting all camera calls concurrently, eg by using asyncio.gather?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

possible i guess, but fairly unlikely if you ask me. To be honest: I'd improve it but don't know how asyncio,gather works.

Copy link
Copy Markdown
Member

@MartinHjelmare MartinHjelmare Nov 29, 2019

Choose a reason for hiding this comment

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

Awaiting asyncio.gather(*coros) will schedule all coroutines on the event loop and return when all are done. Exceptions are either propagated immediately or returned in the result list.

https://docs.python.org/3/library/asyncio-task.html#asyncio.gather

Awaiting tasks in loops, like the for loop here, should be avoided if the task order doesn't matter and there's not a problem with executing the tasks "at the same time". But some devices or APIs can't handle this so in those cases it's ok to keep awaiting in the loop.


hass.services.async_register(
DOMAIN, SERVICE_REBOOT, async_handle_reboot, schema=SERVICE_REBOOT_SCHEMA
)

_LOGGER.debug("Constructing the ONVIFHassCamera")

hass_camera = ONVIFHassCamera(hass, config)
Expand Down Expand Up @@ -278,10 +297,6 @@ async def async_obtain_input_uri(self):

_LOGGER.debug("Retrieving stream uri")

# Fix Onvif setup error on Goke GK7102 based IP camera
# where we need to recreate media_service #26781
media_service = self._camera.create_media_service()

req = media_service.create_type("GetStreamUri")
req.ProfileToken = profiles[self._profile_index].token
req.StreamSetup = {
Expand Down Expand Up @@ -345,6 +360,20 @@ async def async_perform_ptz(self, pan, tilt, zoom):
else:
_LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)

async def async_perform_reboot(self):
"""Perform a SystemReboot action on the camera."""
try:
_LOGGER.debug("Calling SystemReboot")
ret = await self._camera.devicemgmt.SystemReboot()
_LOGGER.debug("Camera '%s' Reboot command returned '%s'", self._name, ret)
except exceptions.ONVIFError as err:
_LOGGER.error(
"Couldn't reboot the camera '%s', please verify "
"that the camera supports the command. Error: %s",
self._name,
err,
)

async def async_added_to_hass(self):
"""Handle entity addition to hass."""
_LOGGER.debug("Camera '%s' added to hass", self._name)
Expand Down
29 changes: 29 additions & 0 deletions homeassistant/components/onvif/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Constants for the ONVIF Camera component."""
DOMAIN = "onvif"

DEFAULT_NAME = "ONVIF Camera"
DEFAULT_PORT = 5000
DEFAULT_USERNAME = "admin"
DEFAULT_PASSWORD = "888888"
DEFAULT_ARGUMENTS = "-pred 1"
DEFAULT_PROFILE = 0

CONF_PROFILE = "profile"

ATTR_PAN = "pan"
ATTR_TILT = "tilt"
ATTR_ZOOM = "zoom"

DIR_UP = "UP"
DIR_DOWN = "DOWN"
DIR_LEFT = "LEFT"
DIR_RIGHT = "RIGHT"
ZOOM_OUT = "ZOOM_OUT"
ZOOM_IN = "ZOOM_IN"
PTZ_NONE = "NONE"

SERVICE_PTZ = "ptz"
SERVICE_REBOOT = "reboot"

ONVIF_DATA = "onvif"
ENTITIES = "entities"
9 changes: 7 additions & 2 deletions homeassistant/components/onvif/services.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
onvif_ptz:
ptz:
description: If your ONVIF camera supports PTZ, you will be able to pan, tilt or zoom your camera.
fields:
entity_id:
Expand All @@ -13,4 +13,9 @@ onvif_ptz:
zoom:
description: 'Zoom. Allowed values: ZOOM_IN, ZOOM_OUT, NONE'
example: 'NONE'

reboot:
description: If your ONVIF camera supports SystemReboot, you will be able to reboot it.
fields:
entity_id:
description: 'String or list of strings that point at entity_ids of cameras. Else targets all.'
example: 'camera.backyard'