2019-04-03 15:40:03 +00:00
|
|
|
"""This component provides basic support for Foscam IP cameras."""
|
2019-10-07 11:17:43 +00:00
|
|
|
import asyncio
|
|
|
|
|
|
|
|
from libpyfoscam import FoscamCamera
|
2016-08-27 20:43:33 +00:00
|
|
|
import voluptuous as vol
|
2015-09-16 03:58:46 +00:00
|
|
|
|
2019-12-09 13:14:40 +00:00
|
|
|
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
|
2021-01-13 15:09:05 +00:00
|
|
|
from homeassistant.config_entries import SOURCE_IMPORT
|
2019-10-07 11:17:43 +00:00
|
|
|
from homeassistant.const import (
|
2021-01-13 15:09:05 +00:00
|
|
|
CONF_HOST,
|
2019-10-07 11:17:43 +00:00
|
|
|
CONF_NAME,
|
|
|
|
CONF_PASSWORD,
|
|
|
|
CONF_PORT,
|
2019-12-09 13:14:40 +00:00
|
|
|
CONF_USERNAME,
|
2019-10-07 11:17:43 +00:00
|
|
|
)
|
2020-12-01 08:28:30 +00:00
|
|
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
2019-10-07 11:17:43 +00:00
|
|
|
|
2021-02-05 21:39:31 +00:00
|
|
|
from .const import (
|
|
|
|
CONF_RTSP_PORT,
|
|
|
|
CONF_STREAM,
|
|
|
|
DOMAIN,
|
|
|
|
LOGGER,
|
|
|
|
SERVICE_PTZ,
|
|
|
|
SERVICE_PTZ_PRESET,
|
|
|
|
)
|
2015-11-29 22:04:44 +00:00
|
|
|
|
2021-01-13 15:09:05 +00:00
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Required("ip"): cv.string,
|
|
|
|
vol.Required(CONF_PASSWORD): cv.string,
|
|
|
|
vol.Required(CONF_USERNAME): cv.string,
|
|
|
|
vol.Optional(CONF_NAME, default="Foscam Camera"): cv.string,
|
|
|
|
vol.Optional(CONF_PORT, default=88): cv.port,
|
2021-02-05 21:39:31 +00:00
|
|
|
vol.Optional(CONF_RTSP_PORT): cv.port,
|
2021-01-13 15:09:05 +00:00
|
|
|
}
|
|
|
|
)
|
2019-10-07 11:17:43 +00:00
|
|
|
|
|
|
|
DIR_UP = "up"
|
|
|
|
DIR_DOWN = "down"
|
|
|
|
DIR_LEFT = "left"
|
|
|
|
DIR_RIGHT = "right"
|
|
|
|
|
|
|
|
DIR_TOPLEFT = "top_left"
|
|
|
|
DIR_TOPRIGHT = "top_right"
|
|
|
|
DIR_BOTTOMLEFT = "bottom_left"
|
|
|
|
DIR_BOTTOMRIGHT = "bottom_right"
|
|
|
|
|
|
|
|
MOVEMENT_ATTRS = {
|
|
|
|
DIR_UP: "ptz_move_up",
|
|
|
|
DIR_DOWN: "ptz_move_down",
|
|
|
|
DIR_LEFT: "ptz_move_left",
|
|
|
|
DIR_RIGHT: "ptz_move_right",
|
|
|
|
DIR_TOPLEFT: "ptz_move_top_left",
|
|
|
|
DIR_TOPRIGHT: "ptz_move_top_right",
|
|
|
|
DIR_BOTTOMLEFT: "ptz_move_bottom_left",
|
|
|
|
DIR_BOTTOMRIGHT: "ptz_move_bottom_right",
|
|
|
|
}
|
2017-08-01 06:14:34 +00:00
|
|
|
|
2021-01-13 15:09:05 +00:00
|
|
|
DEFAULT_TRAVELTIME = 0.125
|
|
|
|
|
|
|
|
ATTR_MOVEMENT = "movement"
|
|
|
|
ATTR_TRAVELTIME = "travel_time"
|
2021-01-27 14:43:36 +00:00
|
|
|
ATTR_PRESET_NAME = "preset_name"
|
|
|
|
|
|
|
|
PTZ_GOTO_PRESET_COMMAND = "ptz_goto_preset"
|
2021-01-13 15:09:05 +00:00
|
|
|
|
2016-08-27 20:43:33 +00:00
|
|
|
|
2019-10-07 11:17:43 +00:00
|
|
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Set up a Foscam IP Camera."""
|
2021-01-13 15:09:05 +00:00
|
|
|
LOGGER.warning(
|
|
|
|
"Loading foscam via platform config is deprecated, it will be automatically imported. Please remove it afterwards."
|
|
|
|
)
|
|
|
|
|
|
|
|
config_new = {
|
|
|
|
CONF_NAME: config[CONF_NAME],
|
|
|
|
CONF_HOST: config["ip"],
|
|
|
|
CONF_PORT: config[CONF_PORT],
|
|
|
|
CONF_USERNAME: config[CONF_USERNAME],
|
|
|
|
CONF_PASSWORD: config[CONF_PASSWORD],
|
|
|
|
CONF_STREAM: "Main",
|
2021-02-05 21:39:31 +00:00
|
|
|
CONF_RTSP_PORT: config.get(CONF_RTSP_PORT, 554),
|
2021-01-13 15:09:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
hass.async_create_task(
|
|
|
|
hass.config_entries.flow.async_init(
|
|
|
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=config_new
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
|
|
|
"""Add a Foscam IP camera from a config entry."""
|
2020-12-01 08:28:30 +00:00
|
|
|
platform = entity_platform.current_platform.get()
|
|
|
|
platform.async_register_entity_service(
|
2021-01-15 17:24:31 +00:00
|
|
|
SERVICE_PTZ,
|
|
|
|
{
|
|
|
|
vol.Required(ATTR_MOVEMENT): vol.In(
|
|
|
|
[
|
|
|
|
DIR_UP,
|
|
|
|
DIR_DOWN,
|
|
|
|
DIR_LEFT,
|
|
|
|
DIR_RIGHT,
|
|
|
|
DIR_TOPLEFT,
|
|
|
|
DIR_TOPRIGHT,
|
|
|
|
DIR_BOTTOMLEFT,
|
|
|
|
DIR_BOTTOMRIGHT,
|
|
|
|
]
|
|
|
|
),
|
|
|
|
vol.Optional(ATTR_TRAVELTIME, default=DEFAULT_TRAVELTIME): cv.small_float,
|
|
|
|
},
|
|
|
|
"async_perform_ptz",
|
2019-10-07 11:17:43 +00:00
|
|
|
)
|
|
|
|
|
2021-01-27 14:43:36 +00:00
|
|
|
platform.async_register_entity_service(
|
|
|
|
SERVICE_PTZ_PRESET,
|
|
|
|
{
|
|
|
|
vol.Required(ATTR_PRESET_NAME): cv.string,
|
|
|
|
},
|
|
|
|
"async_perform_ptz_preset",
|
|
|
|
)
|
|
|
|
|
2019-10-07 11:17:43 +00:00
|
|
|
camera = FoscamCamera(
|
2021-01-13 15:09:05 +00:00
|
|
|
config_entry.data[CONF_HOST],
|
|
|
|
config_entry.data[CONF_PORT],
|
|
|
|
config_entry.data[CONF_USERNAME],
|
|
|
|
config_entry.data[CONF_PASSWORD],
|
2019-10-07 11:17:43 +00:00
|
|
|
verbose=False,
|
|
|
|
)
|
|
|
|
|
2021-01-13 15:09:05 +00:00
|
|
|
async_add_entities([HassFoscamCamera(camera, config_entry)])
|
2015-09-16 03:58:46 +00:00
|
|
|
|
|
|
|
|
2019-10-07 11:17:43 +00:00
|
|
|
class HassFoscamCamera(Camera):
|
2016-03-07 16:45:06 +00:00
|
|
|
"""An implementation of a Foscam IP camera."""
|
2016-03-07 19:29:54 +00:00
|
|
|
|
2021-01-13 15:09:05 +00:00
|
|
|
def __init__(self, camera, config_entry):
|
2016-03-07 19:29:54 +00:00
|
|
|
"""Initialize a Foscam camera."""
|
2019-09-24 22:38:20 +00:00
|
|
|
super().__init__()
|
2015-09-16 05:09:16 +00:00
|
|
|
|
2019-10-07 11:17:43 +00:00
|
|
|
self._foscam_session = camera
|
2021-01-13 15:09:05 +00:00
|
|
|
self._name = config_entry.title
|
|
|
|
self._username = config_entry.data[CONF_USERNAME]
|
|
|
|
self._password = config_entry.data[CONF_PASSWORD]
|
|
|
|
self._stream = config_entry.data[CONF_STREAM]
|
2021-02-05 21:39:31 +00:00
|
|
|
self._unique_id = config_entry.entry_id
|
|
|
|
self._rtsp_port = config_entry.data[CONF_RTSP_PORT]
|
2021-01-13 15:09:05 +00:00
|
|
|
self._motion_status = False
|
2019-10-07 11:17:43 +00:00
|
|
|
|
|
|
|
async def async_added_to_hass(self):
|
|
|
|
"""Handle entity addition to hass."""
|
2021-01-13 15:09:05 +00:00
|
|
|
# Get motion detection status
|
|
|
|
ret, response = await self.hass.async_add_executor_job(
|
|
|
|
self._foscam_session.get_motion_detect_config
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2021-01-13 15:09:05 +00:00
|
|
|
|
2021-02-05 21:39:31 +00:00
|
|
|
if ret == -3:
|
|
|
|
LOGGER.info(
|
|
|
|
"Can't get motion detection status, camera %s configured with non-admin user",
|
|
|
|
self._name,
|
|
|
|
)
|
|
|
|
|
|
|
|
elif ret != 0:
|
2021-01-13 15:09:05 +00:00
|
|
|
LOGGER.error(
|
|
|
|
"Error getting motion detection status of %s: %s", self._name, ret
|
|
|
|
)
|
|
|
|
|
|
|
|
else:
|
|
|
|
self._motion_status = response == 1
|
|
|
|
|
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
"""Return the entity unique ID."""
|
|
|
|
return self._unique_id
|
2019-03-31 04:01:58 +00:00
|
|
|
|
2015-09-16 03:58:46 +00:00
|
|
|
def camera_image(self):
|
2017-09-23 15:15:46 +00:00
|
|
|
"""Return a still image response from the camera."""
|
2015-11-04 04:02:28 +00:00
|
|
|
# Send the request to snap a picture and return raw jpg data
|
2017-04-07 05:40:33 +00:00
|
|
|
# Handle exception if host is not reachable or url failed
|
2017-08-01 06:14:34 +00:00
|
|
|
result, response = self._foscam_session.snap_picture_2()
|
2019-10-07 11:17:43 +00:00
|
|
|
if result != 0:
|
2017-04-07 05:40:33 +00:00
|
|
|
return None
|
2017-08-01 06:14:34 +00:00
|
|
|
|
|
|
|
return response
|
|
|
|
|
2019-03-31 04:01:58 +00:00
|
|
|
@property
|
|
|
|
def supported_features(self):
|
|
|
|
"""Return supported features."""
|
2019-04-05 06:40:47 +00:00
|
|
|
if self._rtsp_port:
|
2019-04-02 16:59:38 +00:00
|
|
|
return SUPPORT_STREAM
|
2021-01-13 15:09:05 +00:00
|
|
|
|
|
|
|
return None
|
2019-03-31 04:01:58 +00:00
|
|
|
|
2019-05-23 16:45:30 +00:00
|
|
|
async def stream_source(self):
|
2019-03-31 04:01:58 +00:00
|
|
|
"""Return the stream source."""
|
2019-04-05 06:40:47 +00:00
|
|
|
if self._rtsp_port:
|
2021-01-13 15:09:05 +00:00
|
|
|
return f"rtsp://{self._username}:{self._password}@{self._foscam_session.host}:{self._rtsp_port}/video{self._stream}"
|
|
|
|
|
2019-04-02 16:59:38 +00:00
|
|
|
return None
|
2019-03-31 04:01:58 +00:00
|
|
|
|
2017-08-01 06:14:34 +00:00
|
|
|
@property
|
|
|
|
def motion_detection_enabled(self):
|
|
|
|
"""Camera Motion Detection Status."""
|
|
|
|
return self._motion_status
|
|
|
|
|
|
|
|
def enable_motion_detection(self):
|
|
|
|
"""Enable motion detection in camera."""
|
2018-01-25 22:47:53 +00:00
|
|
|
try:
|
|
|
|
ret = self._foscam_session.enable_motion_detection()
|
2019-10-07 11:17:43 +00:00
|
|
|
|
|
|
|
if ret != 0:
|
2021-02-05 21:39:31 +00:00
|
|
|
if ret == -3:
|
|
|
|
LOGGER.info(
|
|
|
|
"Can't set motion detection status, camera %s configured with non-admin user",
|
|
|
|
self._name,
|
|
|
|
)
|
2019-10-07 11:17:43 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
self._motion_status = True
|
2018-01-25 22:47:53 +00:00
|
|
|
except TypeError:
|
2021-01-13 15:09:05 +00:00
|
|
|
LOGGER.debug(
|
|
|
|
"Failed enabling motion detection on '%s'. Is it supported by the device?",
|
|
|
|
self._name,
|
|
|
|
)
|
2017-08-01 06:14:34 +00:00
|
|
|
|
|
|
|
def disable_motion_detection(self):
|
|
|
|
"""Disable motion detection."""
|
2018-01-25 22:47:53 +00:00
|
|
|
try:
|
|
|
|
ret = self._foscam_session.disable_motion_detection()
|
2019-10-07 11:17:43 +00:00
|
|
|
|
|
|
|
if ret != 0:
|
2021-02-05 21:39:31 +00:00
|
|
|
if ret == -3:
|
|
|
|
LOGGER.info(
|
|
|
|
"Can't set motion detection status, camera %s configured with non-admin user",
|
|
|
|
self._name,
|
|
|
|
)
|
2019-10-07 11:17:43 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
self._motion_status = False
|
2018-01-25 22:47:53 +00:00
|
|
|
except TypeError:
|
2021-01-13 15:09:05 +00:00
|
|
|
LOGGER.debug(
|
|
|
|
"Failed disabling motion detection on '%s'. Is it supported by the device?",
|
|
|
|
self._name,
|
|
|
|
)
|
2019-10-07 11:17:43 +00:00
|
|
|
|
|
|
|
async def async_perform_ptz(self, movement, travel_time):
|
|
|
|
"""Perform a PTZ action on the camera."""
|
2021-01-13 15:09:05 +00:00
|
|
|
LOGGER.debug("PTZ action '%s' on %s", movement, self._name)
|
2019-10-07 11:17:43 +00:00
|
|
|
|
|
|
|
movement_function = getattr(self._foscam_session, MOVEMENT_ATTRS[movement])
|
|
|
|
|
|
|
|
ret, _ = await self.hass.async_add_executor_job(movement_function)
|
|
|
|
|
|
|
|
if ret != 0:
|
2021-01-13 15:09:05 +00:00
|
|
|
LOGGER.error("Error moving %s '%s': %s", movement, self._name, ret)
|
2019-10-07 11:17:43 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
await asyncio.sleep(travel_time)
|
|
|
|
|
|
|
|
ret, _ = await self.hass.async_add_executor_job(
|
|
|
|
self._foscam_session.ptz_stop_run
|
|
|
|
)
|
|
|
|
|
|
|
|
if ret != 0:
|
2021-01-13 15:09:05 +00:00
|
|
|
LOGGER.error("Error stopping movement on '%s': %s", self._name, ret)
|
2019-10-07 11:17:43 +00:00
|
|
|
return
|
2015-09-16 03:58:46 +00:00
|
|
|
|
2021-01-27 14:43:36 +00:00
|
|
|
async def async_perform_ptz_preset(self, preset_name):
|
|
|
|
"""Perform a PTZ preset action on the camera."""
|
|
|
|
LOGGER.debug("PTZ preset '%s' on %s", preset_name, self._name)
|
|
|
|
|
|
|
|
preset_function = getattr(self._foscam_session, PTZ_GOTO_PRESET_COMMAND)
|
|
|
|
|
|
|
|
ret, _ = await self.hass.async_add_executor_job(preset_function, preset_name)
|
|
|
|
|
|
|
|
if ret != 0:
|
|
|
|
LOGGER.error(
|
|
|
|
"Error moving to preset %s on '%s': %s", preset_name, self._name, ret
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
2015-09-16 03:58:46 +00:00
|
|
|
@property
|
|
|
|
def name(self):
|
2016-03-07 16:45:06 +00:00
|
|
|
"""Return the name of this camera."""
|
2015-09-16 03:58:46 +00:00
|
|
|
return self._name
|