268 lines
8.4 KiB
Python
268 lines
8.4 KiB
Python
"""This component provides support for Reolink IP cameras."""
|
|
import asyncio
|
|
from datetime import datetime
|
|
import logging
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.camera import SUPPORT_STREAM, Camera
|
|
|
|
# from homeassistant.components.ffmpeg import DATA_FFMPEG
|
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
|
from homeassistant.helpers.aiohttp_client import (
|
|
async_aiohttp_proxy_web,
|
|
async_get_clientsession,
|
|
)
|
|
|
|
from .const import (
|
|
DOMAIN_DATA,
|
|
LAST_EVENT,
|
|
SERVICE_PTZ_CONTROL,
|
|
SERVICE_QUERY_VOD,
|
|
SERVICE_SET_BACKLIGHT,
|
|
SERVICE_SET_DAYNIGHT,
|
|
SERVICE_SET_SENSITIVITY,
|
|
SUPPORT_PLAYBACK,
|
|
SUPPORT_PTZ,
|
|
)
|
|
from .entity import ReolinkEntity
|
|
from .typings import VoDEvent
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
async def async_setup_entry(hass, config_entry, async_add_devices):
|
|
"""Set up a Reolink IP Camera."""
|
|
|
|
platform = entity_platform.current_platform.get()
|
|
camera = ReolinkCamera(hass, config_entry)
|
|
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_SENSITIVITY,
|
|
{
|
|
vol.Required("sensitivity"): cv.positive_int,
|
|
vol.Optional("preset"): cv.positive_int,
|
|
},
|
|
SERVICE_SET_SENSITIVITY,
|
|
)
|
|
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_DAYNIGHT,
|
|
{
|
|
vol.Required("mode"): cv.string,
|
|
},
|
|
SERVICE_SET_DAYNIGHT,
|
|
)
|
|
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_BACKLIGHT,
|
|
{
|
|
vol.Required("mode"): cv.string,
|
|
},
|
|
SERVICE_SET_BACKLIGHT,
|
|
)
|
|
|
|
platform.async_register_entity_service(
|
|
SERVICE_PTZ_CONTROL,
|
|
{
|
|
vol.Required("command"): cv.string,
|
|
vol.Optional("preset"): cv.positive_int,
|
|
vol.Optional("speed"): cv.positive_int,
|
|
},
|
|
SERVICE_PTZ_CONTROL,
|
|
[SUPPORT_PTZ],
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_QUERY_VOD,
|
|
{
|
|
vol.Required("event_id"): cv.string,
|
|
vol.Optional("start"): cv.datetime,
|
|
vol.Optional("end"): cv.datetime,
|
|
},
|
|
SERVICE_QUERY_VOD,
|
|
[SUPPORT_PLAYBACK],
|
|
)
|
|
|
|
async_add_devices([camera])
|
|
|
|
|
|
class ReolinkCamera(ReolinkEntity, Camera):
|
|
"""An implementation of a Reolink IP camera."""
|
|
|
|
def __init__(self, hass, config):
|
|
"""Initialize a Reolink camera."""
|
|
ReolinkEntity.__init__(self, hass, config)
|
|
Camera.__init__(self)
|
|
self._entry_id = config.entry_id
|
|
|
|
# self._ffmpeg = self._hass.data[DATA_FFMPEG]
|
|
# self._last_image = None
|
|
self._ptz_commands = {
|
|
"AUTO": "Auto",
|
|
"DOWN": "Down",
|
|
"FOCUSDEC": "FocusDec",
|
|
"FOCUSINC": "FocusInc",
|
|
"LEFT": "Left",
|
|
"LEFTDOWN": "LeftDown",
|
|
"LEFTUP": "LeftUp",
|
|
"RIGHT": "Right",
|
|
"RIGHTDOWN": "RightDown",
|
|
"RIGHTUP": "RightUp",
|
|
"STOP": "Stop",
|
|
"TOPOS": "ToPos",
|
|
"UP": "Up",
|
|
"ZOOMDEC": "ZoomDec",
|
|
"ZOOMINC": "ZoomInc",
|
|
}
|
|
self._daynight_modes = {
|
|
"AUTO": "Auto",
|
|
"COLOR": "Color",
|
|
"BLACKANDWHITE": "Black&White",
|
|
}
|
|
|
|
self._backlight_modes = {
|
|
"BACKLIGHTCONTROL": "BackLightControl",
|
|
"DYNAMICRANGECONTROL": "DynamicRangeControl",
|
|
"OFF": "Off",
|
|
}
|
|
|
|
@property
|
|
def unique_id(self):
|
|
"""Return Unique ID string."""
|
|
return f"reolink_camera_{self._base.unique_id}"
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of this camera."""
|
|
return self._base.name
|
|
|
|
@property
|
|
def ptz_support(self):
|
|
"""Return whether the camera has PTZ support."""
|
|
return self._base.api.ptz_support
|
|
|
|
@property
|
|
def playback_support(self):
|
|
""" Return whethere the camera has VoDs. """
|
|
# TODO : this should probably be like ptz above, and be a property of the api
|
|
return bool(self._base.api.hdd_info)
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return the camera state attributes."""
|
|
attrs = {}
|
|
if self._base.api.ptz_support:
|
|
attrs["ptz_presets"] = self._base.api.ptz_presets
|
|
|
|
for key, value in self._backlight_modes.items():
|
|
if value == self._base.api.backlight_state:
|
|
attrs["backlight_state"] = key
|
|
|
|
for key, value in self._daynight_modes.items():
|
|
if value == self._base.api.daynight_state:
|
|
attrs["daynight_state"] = key
|
|
|
|
if self._base.api.sensitivity_presets:
|
|
attrs["sensitivity"] = self.get_sensitivity_presets()
|
|
|
|
if self.playback_support:
|
|
data: dict = self.hass.data.get(DOMAIN_DATA)
|
|
data = data.get(self._base.unique_id) if data else None
|
|
last: VoDEvent = data.get(LAST_EVENT) if data else None
|
|
if last and last.url:
|
|
attrs["video_url"] = last.url
|
|
if last.thumbnail and last.thumbnail.exists:
|
|
attrs["video_thumbnail"] = last.thumbnail.url
|
|
|
|
return attrs
|
|
|
|
@property
|
|
def supported_features(self):
|
|
"""Return supported features."""
|
|
features = SUPPORT_STREAM
|
|
if self.ptz_support:
|
|
features += SUPPORT_PTZ
|
|
if self.playback_support:
|
|
features += SUPPORT_PLAYBACK
|
|
return features
|
|
|
|
async def stream_source(self):
|
|
"""Return the source of the stream."""
|
|
return await self._base.api.get_stream_source()
|
|
|
|
async def handle_async_mjpeg_stream(self, request):
|
|
"""Generate an HTTP MJPEG stream from the camera."""
|
|
stream_source = await self.stream_source()
|
|
|
|
websession = async_get_clientsession(self._hass)
|
|
stream_coro = websession.get(stream_source, timeout=10)
|
|
|
|
return await async_aiohttp_proxy_web(self._hass, request, stream_coro)
|
|
|
|
async def async_camera_image(self):
|
|
"""Return a still image response from the camera."""
|
|
return await self._base.api.get_snapshot()
|
|
|
|
async def ptz_control(self, command, **kwargs):
|
|
"""Pass PTZ command to the camera."""
|
|
if not self.ptz_support:
|
|
_LOGGER.error("PTZ is not supported on this device")
|
|
return
|
|
|
|
await self._base.api.set_ptz_command(
|
|
command=self._ptz_commands[command], **kwargs
|
|
)
|
|
|
|
async def query_vods(self, event_id, **kwargs):
|
|
""" Query camera for VoDs and emit results """
|
|
if not self.playback_support:
|
|
_LOGGER.error("Video Playback is not supported on this device")
|
|
return
|
|
|
|
await self._base.emit_search_results(
|
|
event_id, self._entry_id, context=self._context, **kwargs
|
|
)
|
|
|
|
def get_sensitivity_presets(self):
|
|
"""Get formatted sensitivity presets."""
|
|
presets = list()
|
|
preset = dict()
|
|
|
|
for api_preset in self._base.api.sensitivity_presets:
|
|
preset["id"] = api_preset["id"]
|
|
preset["sensitivity"] = api_preset["sensitivity"]
|
|
|
|
time_string = f'{api_preset["beginHour"]}:{api_preset["beginMin"]}'
|
|
begin = datetime.strptime(time_string, "%H:%M")
|
|
preset["begin"] = begin.strftime("%H:%M")
|
|
|
|
time_string = f'{api_preset["endHour"]}:{api_preset["endMin"]}'
|
|
end = datetime.strptime(time_string, "%H:%M")
|
|
preset["end"] = end.strftime("%H:%M")
|
|
|
|
presets.append(preset.copy())
|
|
|
|
return presets
|
|
|
|
async def set_sensitivity(self, sensitivity, **kwargs):
|
|
"""Set the sensitivity to the camera."""
|
|
if "preset" in kwargs:
|
|
kwargs["preset"] += 1 # The camera preset ID's on the GUI are always +1
|
|
await self._base.api.set_sensitivity(value=sensitivity, **kwargs)
|
|
|
|
async def set_daynight(self, mode):
|
|
"""Set the day and night mode to the camera."""
|
|
await self._base.api.set_daynight(value=self._daynight_modes[mode])
|
|
|
|
async def set_backlight(self, mode):
|
|
"""Set the backlight mode to the camera."""
|
|
await self._base.api.set_backlight(value=self._backlight_modes[mode])
|
|
|
|
async def async_enable_motion_detection(self):
|
|
"""Predefined camera service implementation."""
|
|
self._base.motion_detection_state = True
|
|
|
|
async def async_disable_motion_detection(self):
|
|
"""Predefined camera service implementation."""
|
|
self._base.motion_detection_state = False
|