diff --git a/.coveragerc b/.coveragerc index e51f4de886deb74701b97a36b1e45fd9f0165c27..6dcd20f6ad6eb1f232ab1eea68c10413d6762cd4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -217,6 +217,7 @@ omit = homeassistant/components/eufy/* homeassistant/components/everlights/light.py homeassistant/components/evohome/* + homeassistant/components/ezviz/* homeassistant/components/familyhub/camera.py homeassistant/components/fastdotcom/* homeassistant/components/ffmpeg/camera.py diff --git a/CODEOWNERS b/CODEOWNERS index cb254824039cf6d2bf22526e3427723c46f7dce3..411c615e8572551f8c29f5450e5746a19a302ac0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -103,6 +103,7 @@ homeassistant/components/eq3btsmart/* @rytilahti homeassistant/components/esphome/* @OttoWinter homeassistant/components/essent/* @TheLastProject homeassistant/components/evohome/* @zxdavb +homeassistant/components/ezviz/* @baqs homeassistant/components/fastdotcom/* @rohankapoorcom homeassistant/components/file/* @fabaff homeassistant/components/filter/* @dgomes diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..96891e8b291e477fc6ff2769b79d92c4a178c1a4 --- /dev/null +++ b/homeassistant/components/ezviz/__init__.py @@ -0,0 +1 @@ +"""Support for Ezviz devices via Ezviz Cloud API.""" diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py new file mode 100644 index 0000000000000000000000000000000000000000..b8ede42a508aadc1fce3b10f21ac9861d5dc8f6e --- /dev/null +++ b/homeassistant/components/ezviz/camera.py @@ -0,0 +1,237 @@ +"""This component provides basic support for Ezviz IP cameras.""" +import asyncio +import logging + +from haffmpeg.tools import IMAGE_JPEG, ImageFrame +from pyezviz.camera import EzvizCamera +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" + +DEFAULT_CAMERA_USERNAME = "admin" +DEFAULT_RTSP_PORT = "554" + +DATA_FFMPEG = "ffmpeg" + +EZVIZ_DATA = "ezviz" +ENTITIES = "entities" + +CAMERA_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CAMERAS, default={}): {cv.string: CAMERA_SCHEMA}, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Ezviz IP Cameras.""" + + conf_cameras = config[CONF_CAMERAS] + + account = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + + try: + ezviz_client = EzvizClient(account, password) + ezviz_client.login() + cameras = ezviz_client.load_cameras() + + except PyEzvizError as exp: + _LOGGER.error(exp) + return + + # now, let's build the HASS devices + camera_entities = [] + + # Add the cameras as devices in HASS + for camera in cameras: + + camera_username = DEFAULT_CAMERA_USERNAME + camera_password = "" + camera_rtsp_stream = "" + camera_serial = camera["serial"] + + # There seem to be a bug related to localRtspPort in Ezviz API... + local_rtsp_port = DEFAULT_RTSP_PORT + if camera["local_rtsp_port"] and camera["local_rtsp_port"] != 0: + local_rtsp_port = camera["local_rtsp_port"] + + if camera_serial in conf_cameras: + camera_username = conf_cameras[camera_serial][CONF_USERNAME] + camera_password = conf_cameras[camera_serial][CONF_PASSWORD] + camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{camera['local_ip']}:{local_rtsp_port}" + _LOGGER.debug( + "Camera %s source stream: %s", camera["serial"], camera_rtsp_stream + ) + + else: + _LOGGER.info( + "Found camera with serial %s without configuration. Add it to configuration.yaml to see the camera stream", + camera_serial, + ) + + camera["username"] = camera_username + camera["password"] = camera_password + camera["rtsp_stream"] = camera_rtsp_stream + + camera["ezviz_camera"] = EzvizCamera(ezviz_client, camera_serial) + + camera_entities.append(HassEzvizCamera(**camera)) + + add_entities(camera_entities) + + +class HassEzvizCamera(Camera): + """An implementation of a Foscam IP camera.""" + + def __init__(self, **data): + """Initialize an Ezviz camera.""" + super().__init__() + + self._username = data["username"] + self._password = data["password"] + self._rtsp_stream = data["rtsp_stream"] + + self._ezviz_camera = data["ezviz_camera"] + self._serial = data["serial"] + self._name = data["name"] + self._status = data["status"] + self._privacy = data["privacy"] + self._audio = data["audio"] + self._ir_led = data["ir_led"] + self._state_led = data["state_led"] + self._follow_move = data["follow_move"] + self._alarm_notify = data["alarm_notify"] + self._alarm_sound_mod = data["alarm_sound_mod"] + self._encrypted = data["encrypted"] + self._local_ip = data["local_ip"] + self._detection_sensibility = data["detection_sensibility"] + self._device_sub_category = data["device_sub_category"] + self._local_rtsp_port = data["local_rtsp_port"] + + self._ffmpeg = None + + def update(self): + """Update the camera states.""" + + data = self._ezviz_camera.status() + + self._name = data["name"] + self._status = data["status"] + self._privacy = data["privacy"] + self._audio = data["audio"] + self._ir_led = data["ir_led"] + self._state_led = data["state_led"] + self._follow_move = data["follow_move"] + self._alarm_notify = data["alarm_notify"] + self._alarm_sound_mod = data["alarm_sound_mod"] + self._encrypted = data["encrypted"] + self._local_ip = data["local_ip"] + self._detection_sensibility = data["detection_sensibility"] + self._device_sub_category = data["device_sub_category"] + self._local_rtsp_port = data["local_rtsp_port"] + + async def async_added_to_hass(self): + """Subscribe to ffmpeg and add camera to list.""" + self._ffmpeg = self.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 Ezviz-specific camera state attributes.""" + return { + # if privacy == true, the device closed the lid or did a 180° tilt + "privacy": self._privacy, + # is the camera listening ? + "audio": self._audio, + # infrared led on ? + "ir_led": self._ir_led, + # state led on ? + "state_led": self._state_led, + # if true, the camera will move automatically to follow movements + "follow_move": self._follow_move, + # if true, if some movement is detected, the app is notified + "alarm_notify": self._alarm_notify, + # if true, if some movement is detected, the camera makes some sound + "alarm_sound_mod": self._alarm_sound_mod, + # are the camera's stored videos/images encrypted? + "encrypted": self._encrypted, + # camera's local ip on local network + "local_ip": self._local_ip, + # from 1 to 9, the higher is the sensibility, the more it will detect small movements + "detection_sensibility": self._detection_sensibility, + } + + @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._device_sub_category + + @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 diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..167f063c0f700fad5d04203283235ac929f3e4c6 --- /dev/null +++ b/homeassistant/components/ezviz/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "ezviz", + "name": "Ezviz", + "documentation": "https://www.home-assistant.io/integrations/ezviz", + "dependencies": [], + "codeowners": ["@baqs"], + "requirements": ["pyezviz==0.1.5"] +} diff --git a/requirements_all.txt b/requirements_all.txt index 4e301df499861dcc7b69558e8627c574106b5288..0e79b3a35a8cc1e05d96b78c1b0cc5177c0789eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1237,6 +1237,9 @@ pyephember==0.3.1 # homeassistant.components.everlights pyeverlights==0.1.0 +# homeassistant.components.ezviz +pyezviz==0.1.5 + # homeassistant.components.fortigate pyfgt==0.5.1