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