diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py new file mode 100644 index 00000000000..d1bb332342b --- /dev/null +++ b/homeassistant/components/ezviz/camera.py @@ -0,0 +1,276 @@ +"""This component provides basic support for Ezviz IP cameras.""" +import asyncio +import logging + +from haffmpeg.tools import IMAGE_JPEG, ImageFrame +from pyezviz.client import EzvizClient, PyEzvizError +import voluptuous as vol + +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_CAMERAS = "cameras" +CONF_SERIAL = "serial" +CONF_DEFAULT_CAMERA_USERNAME = "admin" + +DATA_FFMPEG = "ffmpeg" + +CAMERAS_CONFIG = vol.Schema( + { + vol.Optional(CONF_USERNAME, default=CONF_DEFAULT_CAMERA_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SERIAL): cv.string, + } +) + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_CAMERAS, default={}): vol.All(), + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Ezviz IP Cameras.""" + conf_cameras = {} + if CONF_CAMERAS in config: + conf_cameras = config[CONF_CAMERAS] + _LOGGER.debug("Expecting %s cameras from config", len(conf_cameras)) + + account = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + + try: + ezviz_client = EzvizClient(account, password) + ezviz_client.login() + + # Get cameras + connections = ezviz_client.get_CONNECTION() + devices = ezviz_client.get_DEVICE() + # now, let's build the HASS devices + cameras = {} + for device in devices: + + device_serial = device["deviceSerial"] + + # There seem to be a bug related to localRtspPort in Ezviz API... + local_rtsp_port = 554 + if connections[device_serial]["localRtspPort"] != 0: + local_rtsp_port = connections[device_serial]["localRtspPort"] + + cameras[device_serial] = { + "serial": device_serial, + "name": device["name"], + "deviceType": device["deviceType"], + "version": device["version"], + "status": device["status"], + "createTime": device["userDeviceCreateTime"], + "category": device["deviceCategory"], + "subCategory": device["deviceSubCategory"], + "customType": device["customType"], + "localIp": connections[device_serial]["localIp"], + "netIp": connections[device_serial]["netIp"], + "localRtspPort": local_rtsp_port, + "netType": connections[device_serial]["netType"], + "wanIp": connections[device_serial]["wanIp"], + } + + # Add the cameras as devices in HASS + for camera_serial in cameras: + camera = cameras[camera_serial] + _LOGGER.debug("CAMERA: %s", camera) + + camera_username = CONF_DEFAULT_CAMERA_USERNAME + camera_password = "" + camera_rtsp_stream = "" + + if camera_serial in conf_cameras: + camera_username = conf_cameras[camera_serial]["username"] + camera_password = conf_cameras[camera_serial]["password"] + camera_rtsp_stream = "rtsp://{}:{}@{}:{}".format( + camera_username, + camera_password, + camera["localIp"], + camera["localRtspPort"], + ) + _LOGGER.debug( + "Camera %s source stream: %s", camera["serial"], camera_rtsp_stream + ) + + else: + _LOGGER.error( + "I found a camera (%s) but it is not configured. Please configure it if you wish to see the appropriate stream. Conf cameras: %s", + camera_serial, + conf_cameras, + ) + + async_add_entities( + [ + EzvizCamera( + hass, + camera_username, + camera_password, + camera_rtsp_stream, + camera["serial"], + camera["name"], + camera["deviceType"], + camera["version"], + camera["status"], + camera["createTime"], + camera["category"], + camera["subCategory"], + camera["customType"], + camera["localIp"], + camera["netIp"], + camera["localRtspPort"], + camera["wanIp"], + camera["netType"], + ) + ] + ) + except PyEzvizError as exp: + _LOGGER.error(exp) + return + + +class EzvizCamera(Camera): + """An implementation of a Foscam IP camera.""" + + def __init__( + self, + hass, + username, + password, + rtsp_stream, + serial, + name, + device_type, + version, + status, + create_time, + category, + sub_category, + custom_type, + local_ip, + net_ip, + local_rtsp_port, + wan_ip, + net_type, + ): + """Initialize an Ezviz camera.""" + super().__init__() + + self._username = username + self._password = password + self._rtsp_stream = rtsp_stream + + self._serial = serial + self._name = name + self._type = device_type + self._version = version + self._status = status + self._create_time = create_time + self._category = category + self._sub_category = sub_category + self._custom_type = custom_type + self._local_ip = local_ip + self._net_ip = net_ip + self._local_rtsp_port = local_rtsp_port + self._wan_ip = wan_ip + self._net_type = net_type + + self._ffmpeg = hass.data[DATA_FFMPEG] + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return True + + @property + def device_state_attributes(self): + """Return the Netatmo-specific camera state attributes.""" + _LOGGER.debug("Getting new attributes from ezviz camera '%s'", self._name) + + attr = {} + + attr["serial"] = self._serial + attr["type"] = self._type + attr["version"] = self._version + # attr['status'] = self._status + attr["createTime"] = self._create_time + attr["category"] = self._category + attr["subCategory"] = self._sub_category + attr["customType"] = self._custom_type + attr["localIp"] = self._local_ip + attr["netIp"] = self._net_ip + attr["localRtspPort"] = self._local_rtsp_port + attr["wanIp"] = self._wan_ip + attr["netType"] = self._net_type + attr["rtspStream"] = self._rtsp_stream + + _LOGGER.debug("Attributes of '%s' = %s", self._name, attr) + + return attr + + @property + def available(self): + """Return True if entity is available.""" + return self._status + + @property + def brand(self): + """Return the camera brand.""" + return "Ezviz" + + @property + def supported_features(self): + """Return supported features.""" + if self._rtsp_stream: + return SUPPORT_STREAM + return 0 + + @property + def model(self): + """Return the camera model.""" + return self._type + + @property + def is_on(self): + """Return true if on.""" + return self._status + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + async def async_camera_image(self): + """Return a frame from the camera stream.""" + ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) + + image = await asyncio.shield( + ffmpeg.get_image(self._rtsp_stream, output_format=IMAGE_JPEG,) + ) + return image + + async def stream_source(self): + """Return the stream source.""" + if self._local_rtsp_port: + rtsp_stream_source = "rtsp://{}:{}@{}:{}".format( + self._username, self._password, self._local_ip, self._local_rtsp_port + ) + _LOGGER.debug( + "Camera %s source stream: %s", self._serial, rtsp_stream_source + ) + self._rtsp_stream = rtsp_stream_source + return rtsp_stream_source + return None