From 5f24cc229d94257ba92da48ba12abf90071c2e8f Mon Sep 17 00:00:00 2001
From: Andy Castille <andy@robiotic.net>
Date: Sun, 17 Sep 2017 13:47:30 -0500
Subject: [PATCH] DoorBird Component (#9281)

* DoorBird Component

* add newlines at end of files

* fix lint

* fix doorbird components conventions

* fix doorbird domain import and log strings

* don't redundantly add switches

* Remove return statement from setup_platform
---
 .coveragerc                                   |  3 +
 .../components/binary_sensor/doorbird.py      | 60 ++++++++++++
 homeassistant/components/camera/doorbird.py   | 90 +++++++++++++++++
 homeassistant/components/doorbird.py          | 44 +++++++++
 homeassistant/components/switch/doorbird.py   | 97 +++++++++++++++++++
 requirements_all.txt                          |  3 +
 6 files changed, 297 insertions(+)
 create mode 100644 homeassistant/components/binary_sensor/doorbird.py
 create mode 100644 homeassistant/components/camera/doorbird.py
 create mode 100644 homeassistant/components/doorbird.py
 create mode 100644 homeassistant/components/switch/doorbird.py

diff --git a/.coveragerc b/.coveragerc
index 4f621763bec..1f4e705dd67 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -52,6 +52,9 @@ omit =
 
     homeassistant/components/digital_ocean.py
     homeassistant/components/*/digital_ocean.py
+	
+	homeassistant/components/doorbird.py
+	homeassistant/components/*/doorbird.py
 
     homeassistant/components/dweet.py
     homeassistant/components/*/dweet.py
diff --git a/homeassistant/components/binary_sensor/doorbird.py b/homeassistant/components/binary_sensor/doorbird.py
new file mode 100644
index 00000000000..9a13687fc54
--- /dev/null
+++ b/homeassistant/components/binary_sensor/doorbird.py
@@ -0,0 +1,60 @@
+"""Support for reading binary states from a DoorBird video doorbell."""
+from datetime import timedelta
+import logging
+
+from homeassistant.components.binary_sensor import BinarySensorDevice
+from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN
+from homeassistant.util import Throttle
+
+DEPENDENCIES = ['doorbird']
+
+_LOGGER = logging.getLogger(__name__)
+_MIN_UPDATE_INTERVAL = timedelta(milliseconds=250)
+
+SENSOR_TYPES = {
+    "doorbell": {
+        "name": "Doorbell Ringing",
+        "icon": {
+            True: "bell-ring",
+            False: "bell",
+            None: "bell-outline"
+        }
+    }
+}
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+    """Set up the DoorBird binary sensor component."""
+    device = hass.data.get(DOORBIRD_DOMAIN)
+    add_devices([DoorBirdBinarySensor(device, "doorbell")], True)
+
+
+class DoorBirdBinarySensor(BinarySensorDevice):
+    """A binary sensor of a DoorBird device."""
+
+    def __init__(self, device, sensor_type):
+        """Initialize a binary sensor on a DoorBird device."""
+        self._device = device
+        self._sensor_type = sensor_type
+        self._state = None
+
+    @property
+    def name(self):
+        """Get the name of the sensor."""
+        return SENSOR_TYPES[self._sensor_type]["name"]
+
+    @property
+    def icon(self):
+        """Get an icon to display."""
+        state_icon = SENSOR_TYPES[self._sensor_type]["icon"][self._state]
+        return "mdi:{}".format(state_icon)
+
+    @property
+    def is_on(self):
+        """Get the state of the binary sensor."""
+        return self._state
+
+    @Throttle(_MIN_UPDATE_INTERVAL)
+    def update(self):
+        """Pull the latest value from the device."""
+        self._state = self._device.doorbell_state()
diff --git a/homeassistant/components/camera/doorbird.py b/homeassistant/components/camera/doorbird.py
new file mode 100644
index 00000000000..cf6b6b2871f
--- /dev/null
+++ b/homeassistant/components/camera/doorbird.py
@@ -0,0 +1,90 @@
+"""Support for viewing the camera feed from a DoorBird video doorbell."""
+
+import asyncio
+import datetime
+import logging
+import voluptuous as vol
+
+import aiohttp
+import async_timeout
+
+from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
+from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+DEPENDENCIES = ['doorbird']
+
+_CAMERA_LIVE = "DoorBird Live"
+_CAMERA_LAST_VISITOR = "DoorBird Last Ring"
+_LIVE_INTERVAL = datetime.timedelta(seconds=1)
+_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1)
+_LOGGER = logging.getLogger(__name__)
+_TIMEOUT = 10  # seconds
+
+CONF_SHOW_LAST_VISITOR = 'last_visitor'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+    vol.Optional(CONF_SHOW_LAST_VISITOR, default=False): cv.boolean
+})
+
+
+@asyncio.coroutine
+def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+    """Set up the DoorBird camera platform."""
+    device = hass.data.get(DOORBIRD_DOMAIN)
+
+    _LOGGER.debug("Adding DoorBird camera %s", _CAMERA_LIVE)
+    entities = [DoorBirdCamera(device.live_image_url, _CAMERA_LIVE,
+                               _LIVE_INTERVAL)]
+
+    if config.get(CONF_SHOW_LAST_VISITOR):
+        _LOGGER.debug("Adding DoorBird camera %s", _CAMERA_LAST_VISITOR)
+        entities.append(DoorBirdCamera(device.history_image_url(1),
+                                       _CAMERA_LAST_VISITOR,
+                                       _LAST_VISITOR_INTERVAL))
+
+    async_add_devices(entities)
+    _LOGGER.info("Added DoorBird camera(s)")
+
+
+class DoorBirdCamera(Camera):
+    """The camera on a DoorBird device."""
+
+    def __init__(self, url, name, interval=None):
+        """Initialize the camera on a DoorBird device."""
+        self._url = url
+        self._name = name
+        self._last_image = None
+        self._interval = interval or datetime.timedelta
+        self._last_update = datetime.datetime.min
+        super().__init__()
+
+    @property
+    def name(self):
+        """Get the name of the camera."""
+        return self._name
+
+    @asyncio.coroutine
+    def async_camera_image(self):
+        """Pull a still image from the camera."""
+        now = datetime.datetime.now()
+
+        if self._last_image and now - self._last_update < self._interval:
+            return self._last_image
+
+        try:
+            websession = async_get_clientsession(self.hass)
+
+            with async_timeout.timeout(_TIMEOUT, loop=self.hass.loop):
+                response = yield from websession.get(self._url)
+
+            self._last_image = yield from response.read()
+            self._last_update = now
+            return self._last_image
+        except asyncio.TimeoutError:
+            _LOGGER.error("Camera image timed out")
+            return self._last_image
+        except aiohttp.ClientError as error:
+            _LOGGER.error("Error getting camera image: %s", error)
+            return self._last_image
diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py
new file mode 100644
index 00000000000..421c85a0f94
--- /dev/null
+++ b/homeassistant/components/doorbird.py
@@ -0,0 +1,44 @@
+"""Support for a DoorBird video doorbell."""
+
+import logging
+import voluptuous as vol
+
+from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['DoorBirdPy==0.0.4']
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = 'doorbird'
+
+CONFIG_SCHEMA = vol.Schema({
+    DOMAIN: vol.Schema({
+        vol.Required(CONF_HOST): cv.string,
+        vol.Required(CONF_USERNAME): cv.string,
+        vol.Required(CONF_PASSWORD): cv.string
+    })
+}, extra=vol.ALLOW_EXTRA)
+
+
+def setup(hass, config):
+    """Set up the DoorBird component."""
+    device_ip = config[DOMAIN].get(CONF_HOST)
+    username = config[DOMAIN].get(CONF_USERNAME)
+    password = config[DOMAIN].get(CONF_PASSWORD)
+
+    from doorbirdpy import DoorBird
+    device = DoorBird(device_ip, username, password)
+    status = device.ready()
+
+    if status[0]:
+        _LOGGER.info("Connected to DoorBird at %s as %s", device_ip, username)
+        hass.data[DOMAIN] = device
+        return True
+    elif status[1] == 401:
+        _LOGGER.error("Authorization rejected by DoorBird at %s", device_ip)
+        return False
+    else:
+        _LOGGER.error("Could not connect to DoorBird at %s: Error %s",
+                      device_ip, str(status[1]))
+        return False
diff --git a/homeassistant/components/switch/doorbird.py b/homeassistant/components/switch/doorbird.py
new file mode 100644
index 00000000000..66c3bf73116
--- /dev/null
+++ b/homeassistant/components/switch/doorbird.py
@@ -0,0 +1,97 @@
+"""Support for powering relays in a DoorBird video doorbell."""
+import datetime
+import logging
+import voluptuous as vol
+
+from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN
+from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
+from homeassistant.const import CONF_SWITCHES
+import homeassistant.helpers.config_validation as cv
+
+DEPENDENCIES = ['doorbird']
+
+_LOGGER = logging.getLogger(__name__)
+
+SWITCHES = {
+    "open_door": {
+        "name": "Open Door",
+        "icon": {
+            True: "lock-open",
+            False: "lock"
+        },
+        "time": datetime.timedelta(seconds=3)
+    },
+    "light_on": {
+        "name": "Light On",
+        "icon": {
+            True: "lightbulb-on",
+            False: "lightbulb"
+        },
+        "time": datetime.timedelta(minutes=5)
+    }
+}
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+    vol.Required(CONF_SWITCHES, default=[]):
+        vol.All(cv.ensure_list([vol.In(SWITCHES)]))
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+    """Set up the DoorBird switch platform."""
+    device = hass.data.get(DOORBIRD_DOMAIN)
+
+    switches = []
+    for switch in SWITCHES:
+        _LOGGER.debug("Adding DoorBird switch %s", SWITCHES[switch]["name"])
+        switches.append(DoorBirdSwitch(device, switch))
+
+    add_devices(switches)
+    _LOGGER.info("Added DoorBird switches")
+
+
+class DoorBirdSwitch(SwitchDevice):
+    """A relay in a DoorBird device."""
+
+    def __init__(self, device, switch):
+        """Initialize a relay in a DoorBird device."""
+        self._device = device
+        self._switch = switch
+        self._state = False
+        self._assume_off = datetime.datetime.min
+
+    @property
+    def name(self):
+        """Get the name of the switch."""
+        return SWITCHES[self._switch]["name"]
+
+    @property
+    def icon(self):
+        """Get an icon to display."""
+        return "mdi:{}".format(SWITCHES[self._switch]["icon"][self._state])
+
+    @property
+    def is_on(self):
+        """Get the assumed state of the relay."""
+        return self._state
+
+    def turn_on(self, **kwargs):
+        """Power the relay."""
+        if self._switch == "open_door":
+            self._state = self._device.open_door()
+        elif self._switch == "light_on":
+            self._state = self._device.turn_light_on()
+
+        now = datetime.datetime.now()
+        self._assume_off = now + SWITCHES[self._switch]["time"]
+
+    def turn_off(self, **kwargs):
+        """The relays are time-based."""
+        raise NotImplementedError("DoorBird relays cannot be manually turned "
+                                  "off.")
+
+    def update(self):
+        """Wait for the correct amount of assumed time to pass."""
+        if self._state and self._assume_off <= datetime.datetime.now():
+            self._state = False
+            self._assume_off = datetime.datetime.min
diff --git a/requirements_all.txt b/requirements_all.txt
index 67170ccb0b2..96e45c89474 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -17,6 +17,9 @@ astral==1.4
 # homeassistant.components.bbb_gpio
 # Adafruit_BBIO==1.0.0
 
+# homeassistant.components.doorbird
+DoorBirdPy==0.0.4
+
 # homeassistant.components.isy994
 PyISY==1.0.8
 
-- 
GitLab