From a1a835cf542ed7ece0adf1419d283072d8d82012 Mon Sep 17 00:00:00 2001
From: Pierre <3458055+BaQs@users.noreply.github.com>
Date: Mon, 24 Feb 2020 10:39:55 +0100
Subject: [PATCH] Add platform Ezviz (#30378)

* hookid : isort fix

* New platform: Ezviz

* updated CODEOWNERS for ezviz

* proper test requirements

* resolved conflict

* regenerated requirements

* removed stale comments, only one call to add_entities, removed unnecessary attributes

* setup is sync, not async. Removed stale comments

* Compatible with pyezviz 0.1.4

* pyezviz 0.1.4 is now requiredf

* Added PTZ + switch management

* added services.yaml

* proper requirement

* PTZ working in async mode

* Now updates the entity

* Compatible with pyezviz 0.1.5.1

* Fixed switch ir service registering

* now requires pyezviz 0.1.5.2

* now requires pyezviz 0.1.5.2

* Revert "regenerated requirements"

This reverts commit 848b317cf9f9df296c3a6ab9e128ec330c4fd365.

* Rollbacked to a simpler version

* snake_case names everywhere, logging sanatizing, voluptuous proper check

* pyezviz 0.1.5, reworked the PR so that it's intelligible

* no need for services.yaml for now

* proper voluptuous validation

* Removed stale code, use proper conf variable, describe attributes

* regenerated requirements

* stale

* removed status from attributes, since we use it for available we don't need it here then.

* Fixed log message
---
 .coveragerc                                  |   1 +
 CODEOWNERS                                   |   1 +
 homeassistant/components/ezviz/__init__.py   |   1 +
 homeassistant/components/ezviz/camera.py     | 237 +++++++++++++++++++
 homeassistant/components/ezviz/manifest.json |   8 +
 requirements_all.txt                         |   3 +
 6 files changed, 251 insertions(+)
 create mode 100644 homeassistant/components/ezviz/__init__.py
 create mode 100644 homeassistant/components/ezviz/camera.py
 create mode 100644 homeassistant/components/ezviz/manifest.json

diff --git a/.coveragerc b/.coveragerc
index e51f4de886d..6dcd20f6ad6 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 cb254824039..411c615e857 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 00000000000..96891e8b291
--- /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 00000000000..b8ede42a508
--- /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 00000000000..167f063c0f7
--- /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 4e301df4998..0e79b3a35a8 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
 
-- 
GitLab