Overhaul of Blink platform (#16942)

* Using new methods for blink camera

- Refactored blink platform (breaking change)
- Camera needs to be uniquely enabled in config from now on
- Added motion detection enable/disable to camera platform

* Fix motion detection

- bumped blinkpy to 0.8.1
- Added wifi strength sensor

* Added platform schema to sensor

- Added global variables for brand and attribution to main platform

* Removed blink binary sensor

* Add alarm control panel

* Fixed dependency, added alarm_home

* Update requirements

* Fix lint errors

* Updated throttle times

* Add trigger_camera service (replaced snap_picture)

* Add refresh after camera trigger

* Update blinkpy version

* Wait for valid camera response before returning image

- Motion detection now working!

* Updated for new blinkpy 0.9.0

* Add refresh control and other fixes for new blinkpy release

* Add save video service

* Pushing to force bot to update

* Changed based on first review

- Pass blink as BLINK_DATA instead of DOMAIN
- Remove alarm_arm_home from alarm_control_panel
- Re-add discovery with schema for sensors/binar_sensors
- Change motion_detected to a binary_sensor
- Added camera_armed binary sensor
- Update camera device_state_attributes rather than state_attributes

* Moved blink.py to own folder. Added service hints.

* Updated coveragerc to reflect previous change

* Register services with DOMAIN

- Change device add for loop order in binary_sensor

* Fix lint error

* services.async_register -> services.register
This commit is contained in:
Kevin Fronczak 2018-10-02 22:17:14 -04:00 committed by Martin Hjelmare
parent 8e3a70e568
commit c78850a983
9 changed files with 346 additions and 197 deletions

View file

@ -56,7 +56,7 @@ omit =
homeassistant/components/bbb_gpio.py homeassistant/components/bbb_gpio.py
homeassistant/components/*/bbb_gpio.py homeassistant/components/*/bbb_gpio.py
homeassistant/components/blink.py homeassistant/components/blink/*
homeassistant/components/*/blink.py homeassistant/components/*/blink.py
homeassistant/components/bloomsky.py homeassistant/components/bloomsky.py

View file

@ -0,0 +1,86 @@
"""
Support for Blink Alarm Control Panel.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.blink/
"""
import logging
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.components.blink import (
BLINK_DATA, DEFAULT_ATTRIBUTION)
from homeassistant.const import (
ATTR_ATTRIBUTION, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['blink']
ICON = 'mdi:security'
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Arlo Alarm Control Panels."""
if discovery_info is None:
return
data = hass.data[BLINK_DATA]
# Current version of blinkpy API only supports one sync module. When
# support for additional models is added, the sync module name should
# come from the API.
sync_modules = []
sync_modules.append(BlinkSyncModule(data, 'sync'))
add_entities(sync_modules, True)
class BlinkSyncModule(AlarmControlPanel):
"""Representation of a Blink Alarm Control Panel."""
def __init__(self, data, name):
"""Initialize the alarm control panel."""
self.data = data
self.sync = data.sync
self._name = name
self._state = None
@property
def icon(self):
"""Return icon."""
return ICON
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def name(self):
"""Return the name of the panel."""
return "{} {}".format(BLINK_DATA, self._name)
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION,
}
def update(self):
"""Update the state of the device."""
_LOGGER.debug("Updating Blink Alarm Control Panel %s", self._name)
self.data.refresh()
mode = self.sync.arm
if mode:
self._state = STATE_ALARM_ARMED_AWAY
else:
self._state = STATE_ALARM_DISARMED
def alarm_disarm(self, code=None):
"""Send disarm command."""
self.sync.arm = False
self.sync.refresh()
def alarm_arm_away(self, code=None):
"""Send arm command."""
self.sync.arm = True
self.sync.refresh()

View file

@ -2,10 +2,11 @@
Support for Blink system camera control. Support for Blink system camera control.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.blink/ https://home-assistant.io/components/binary_sensor.blink.
""" """
from homeassistant.components.blink import DOMAIN from homeassistant.components.blink import BLINK_DATA, BINARY_SENSORS
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_MONITORED_CONDITIONS
DEPENDENCIES = ['blink'] DEPENDENCIES = ['blink']
@ -14,24 +15,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the blink binary sensors.""" """Set up the blink binary sensors."""
if discovery_info is None: if discovery_info is None:
return return
data = hass.data[BLINK_DATA]
data = hass.data[DOMAIN].blink devs = []
devs = list() for camera in data.sync.cameras:
for name in data.cameras: for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
devs.append(BlinkCameraMotionSensor(name, data)) devs.append(BlinkBinarySensor(data, camera, sensor_type))
devs.append(BlinkSystemSensor(data))
add_entities(devs, True) add_entities(devs, True)
class BlinkCameraMotionSensor(BinarySensorDevice): class BlinkBinarySensor(BinarySensorDevice):
"""Representation of a Blink binary sensor.""" """Representation of a Blink binary sensor."""
def __init__(self, name, data): def __init__(self, data, camera, sensor_type):
"""Initialize the sensor.""" """Initialize the sensor."""
self._name = 'blink_' + name + '_motion_enabled'
self._camera_name = name
self.data = data self.data = data
self._state = self.data.cameras[self._camera_name].armed self._type = sensor_type
name, icon = BINARY_SENSORS[sensor_type]
self._name = "{} {} {}".format(BLINK_DATA, camera, name)
self._icon = icon
self._camera = data.sync.cameras[camera]
self._state = None
@property @property
def name(self): def name(self):
@ -46,29 +50,4 @@ class BlinkCameraMotionSensor(BinarySensorDevice):
def update(self): def update(self):
"""Update sensor state.""" """Update sensor state."""
self.data.refresh() self.data.refresh()
self._state = self.data.cameras[self._camera_name].armed self._state = self._camera.attributes[self._type]
class BlinkSystemSensor(BinarySensorDevice):
"""A representation of a Blink system sensor."""
def __init__(self, data):
"""Initialize the sensor."""
self._name = 'blink armed status'
self.data = data
self._state = self.data.arm
@property
def name(self):
"""Return the name of the blink sensor."""
return self._name.replace(" ", "_")
@property
def is_on(self):
"""Return the status of the sensor."""
return self._state
def update(self):
"""Update sensor state."""
self.data.refresh()
self._state = self.data.arm

View file

@ -1,89 +0,0 @@
"""
Support for Blink Home Camera System.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/blink/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, ATTR_FRIENDLY_NAME, ATTR_ARMED)
from homeassistant.helpers import discovery
REQUIREMENTS = ['blinkpy==0.6.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'blink'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
}, extra=vol.ALLOW_EXTRA)
ARM_SYSTEM_SCHEMA = vol.Schema({
vol.Optional(ATTR_ARMED): cv.boolean
})
ARM_CAMERA_SCHEMA = vol.Schema({
vol.Required(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(ATTR_ARMED): cv.boolean
})
SNAP_PICTURE_SCHEMA = vol.Schema({
vol.Required(ATTR_FRIENDLY_NAME): cv.string
})
class BlinkSystem:
"""Blink System class."""
def __init__(self, config_info):
"""Initialize the system."""
import blinkpy
self.blink = blinkpy.Blink(username=config_info[DOMAIN][CONF_USERNAME],
password=config_info[DOMAIN][CONF_PASSWORD])
self.blink.setup_system()
def setup(hass, config):
"""Set up Blink System."""
hass.data[DOMAIN] = BlinkSystem(config)
discovery.load_platform(hass, 'camera', DOMAIN, {}, config)
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
def snap_picture(call):
"""Take a picture."""
cameras = hass.data[DOMAIN].blink.cameras
name = call.data.get(ATTR_FRIENDLY_NAME, '')
if name in cameras:
cameras[name].snap_picture()
def arm_camera(call):
"""Arm a camera."""
cameras = hass.data[DOMAIN].blink.cameras
name = call.data.get(ATTR_FRIENDLY_NAME, '')
value = call.data.get(ATTR_ARMED, True)
if name in cameras:
cameras[name].set_motion_detect(value)
def arm_system(call):
"""Arm the system."""
value = call.data.get(ATTR_ARMED, True)
hass.data[DOMAIN].blink.arm = value
hass.data[DOMAIN].blink.refresh()
hass.services.register(
DOMAIN, 'snap_picture', snap_picture, schema=SNAP_PICTURE_SCHEMA)
hass.services.register(
DOMAIN, 'arm_camera', arm_camera, schema=ARM_CAMERA_SCHEMA)
hass.services.register(
DOMAIN, 'arm_system', arm_system, schema=ARM_SYSTEM_SCHEMA)
return True

View file

@ -0,0 +1,161 @@
"""
Support for Blink Home Camera System.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/blink/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.helpers import (
config_validation as cv, discovery)
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_SCAN_INTERVAL,
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
REQUIREMENTS = ['blinkpy==0.9.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'blink'
BLINK_DATA = 'blink'
CONF_CAMERA = 'camera'
CONF_ALARM_CONTROL_PANEL = 'alarm_control_panel'
DEFAULT_BRAND = 'Blink'
DEFAULT_ATTRIBUTION = "Data provided by immedia-semi.com"
SIGNAL_UPDATE_BLINK = "blink_update"
DEFAULT_SCAN_INTERVAL = timedelta(seconds=60)
TYPE_CAMERA_ARMED = 'motion_enabled'
TYPE_MOTION_DETECTED = 'motion_detected'
TYPE_TEMPERATURE = 'temperature'
TYPE_BATTERY = 'battery'
TYPE_WIFI_STRENGTH = 'wifi_strength'
TYPE_STATUS = 'status'
SERVICE_REFRESH = 'blink_update'
SERVICE_TRIGGER = 'trigger_camera'
SERVICE_SAVE_VIDEO = 'save_video'
BINARY_SENSORS = {
TYPE_CAMERA_ARMED: ['Camera Armed', 'mdi:verified'],
TYPE_MOTION_DETECTED: ['Motion Detected', 'mdi:run-fast'],
}
SENSORS = {
TYPE_TEMPERATURE: ['Temperature', TEMP_FAHRENHEIT, 'mdi:thermometer'],
TYPE_BATTERY: ['Battery', '%', 'mdi:battery-80'],
TYPE_WIFI_STRENGTH: ['Wifi Signal', 'dBm', 'mdi:wifi-strength-2'],
TYPE_STATUS: ['Status', '', 'mdi:bell']
}
BINARY_SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)):
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)])
})
SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)):
vol.All(cv.ensure_list, [vol.In(SENSORS)])
})
SERVICE_TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string
})
SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_FILENAME): cv.string,
})
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN:
vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
cv.time_period,
vol.Optional(CONF_BINARY_SENSORS, default={}):
BINARY_SENSOR_SCHEMA,
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
})
},
extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up Blink System."""
from blinkpy import blinkpy
conf = config[BLINK_DATA]
username = conf[CONF_USERNAME]
password = conf[CONF_PASSWORD]
scan_interval = conf[CONF_SCAN_INTERVAL]
hass.data[BLINK_DATA] = blinkpy.Blink(username=username,
password=password)
hass.data[BLINK_DATA].refresh_rate = scan_interval.total_seconds()
hass.data[BLINK_DATA].start()
platforms = [
('alarm_control_panel', {}),
('binary_sensor', conf[CONF_BINARY_SENSORS]),
('camera', {}),
('sensor', conf[CONF_SENSORS]),
]
for component, schema in platforms:
discovery.load_platform(hass, component, DOMAIN, schema, config)
def trigger_camera(call):
"""Trigger a camera."""
cameras = hass.data[BLINK_DATA].sync.cameras
name = call.data[CONF_NAME]
if name in cameras:
cameras[name].snap_picture()
hass.data[BLINK_DATA].refresh(force_cache=True)
def blink_refresh(event_time):
"""Call blink to refresh info."""
hass.data[BLINK_DATA].refresh(force_cache=True)
async def async_save_video(call):
"""Call save video service handler."""
await async_handle_save_video_service(hass, call)
hass.services.register(DOMAIN, SERVICE_REFRESH, blink_refresh)
hass.services.register(DOMAIN,
SERVICE_TRIGGER,
trigger_camera,
schema=SERVICE_TRIGGER_SCHEMA)
hass.services.register(DOMAIN,
SERVICE_SAVE_VIDEO,
async_save_video,
schema=SERVICE_SAVE_VIDEO_SCHEMA)
return True
async def async_handle_save_video_service(hass, call):
"""Handle save video service calls."""
camera_name = call.data[CONF_NAME]
video_path = call.data[CONF_FILENAME]
if not hass.config.is_allowed_path(video_path):
_LOGGER.error(
"Can't write %s, no access to path!", video_path)
return
def _write_video(camera_name, video_path):
"""Call video write."""
all_cameras = hass.data[BLINK_DATA].sync.cameras
if camera_name in all_cameras:
all_cameras[camera_name].video_to_file(video_path)
try:
await hass.async_add_executor_job(
_write_video, camera_name, video_path)
except OSError as err:
_LOGGER.error("Can't write image to file: %s", err)

View file

@ -0,0 +1,21 @@
# Describes the format for available Blink services
blink_update:
description: Force a refresh.
trigger_camera:
description: Request named camera to take new image.
fields:
name:
description: Name of camera to take new image.
example: 'Living Room'
save_video:
description: Save last recorded video clip to local file.
fields:
name:
description: Name of camera to grab video from.
example: 'Living Room'
filename:
description: Filename to writable path (directory may need to be included in whitelist_dirs in config)
example: '/tmp/video.mp4'

View file

@ -4,31 +4,27 @@ Support for Blink system camera.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.blink/ https://home-assistant.io/components/camera.blink/
""" """
from datetime import timedelta
import logging import logging
import requests from homeassistant.components.blink import BLINK_DATA, DEFAULT_BRAND
from homeassistant.components.blink import DOMAIN
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['blink'] DEPENDENCIES = ['blink']
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) ATTR_VIDEO_CLIP = 'video'
ATTR_IMAGE = 'image'
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a Blink Camera.""" """Set up a Blink Camera."""
if discovery_info is None: if discovery_info is None:
return return
data = hass.data[BLINK_DATA]
data = hass.data[DOMAIN].blink devs = []
devs = list() for name, camera in data.sync.cameras.items():
for name in data.cameras: devs.append(BlinkCamera(data, name, camera))
devs.append(BlinkCamera(hass, config, data, name))
add_entities(devs) add_entities(devs)
@ -36,15 +32,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class BlinkCamera(Camera): class BlinkCamera(Camera):
"""An implementation of a Blink Camera.""" """An implementation of a Blink Camera."""
def __init__(self, hass, config, data, name): def __init__(self, data, name, camera):
"""Initialize a camera.""" """Initialize a camera."""
super().__init__() super().__init__()
self.data = data self.data = data
self.hass = hass self._name = "{} {}".format(BLINK_DATA, name)
self._name = name self._camera = camera
self.notifications = self.data.cameras[self._name].notifications
self.response = None self.response = None
self.current_image = None
self.last_image = None
_LOGGER.debug("Initialized blink camera %s", self._name) _LOGGER.debug("Initialized blink camera %s", self._name)
@property @property
@ -52,30 +48,29 @@ class BlinkCamera(Camera):
"""Return the camera name.""" """Return the camera name."""
return self._name return self._name
@Throttle(MIN_TIME_BETWEEN_UPDATES) @property
def request_image(self): def device_state_attributes(self):
"""Request a new image from Blink servers.""" """Return the camera attributes."""
_LOGGER.debug("Requesting new image from blink servers") return self._camera.attributes
image_url = self.check_for_motion()
header = self.data.cameras[self._name].header
self.response = requests.get(image_url, headers=header, stream=True)
def check_for_motion(self): def enable_motion_detection(self):
"""Check if motion has been detected since last update.""" """Enable motion detection for the camera."""
self.data.refresh() self._camera.set_motion_detect(True)
notifs = self.data.cameras[self._name].notifications
if notifs > self.notifications:
# We detected motion at some point
self.data.last_motion()
self.notifications = notifs
# Returning motion image currently not working
# return self.data.cameras[self._name].motion['image']
elif notifs < self.notifications:
self.notifications = notifs
return self.data.camera_thumbs[self._name] def disable_motion_detection(self):
"""Disable motion detection for the camera."""
self._camera.set_motion_detect(False)
@property
def motion_detection_enabled(self):
"""Return the state of the camera."""
return self._camera.armed
@property
def brand(self):
"""Return the camera brand."""
return DEFAULT_BRAND
def camera_image(self): def camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
self.request_image() return self._camera.image_from_cache.content
return self.response.content

View file

@ -6,34 +6,24 @@ https://home-assistant.io/components/sensor.blink/
""" """
import logging import logging
from homeassistant.components.blink import DOMAIN from homeassistant.components.blink import BLINK_DATA, SENSORS
from homeassistant.const import TEMP_FAHRENHEIT
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_MONITORED_CONDITIONS
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['blink'] DEPENDENCIES = ['blink']
SENSOR_TYPES = {
'temperature': ['Temperature', TEMP_FAHRENHEIT],
'battery': ['Battery', ''],
'notifications': ['Notifications', '']
}
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a Blink sensor.""" """Set up a Blink sensor."""
if discovery_info is None: if discovery_info is None:
return return
data = hass.data[BLINK_DATA]
data = hass.data[DOMAIN].blink devs = []
devs = list() for camera in data.sync.cameras:
index = 0 for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
for name in data.cameras: devs.append(BlinkSensor(data, camera, sensor_type))
devs.append(BlinkSensor(name, 'temperature', index, data))
devs.append(BlinkSensor(name, 'battery', index, data))
devs.append(BlinkSensor(name, 'notifications', index, data))
index += 1
add_entities(devs, True) add_entities(devs, True)
@ -41,21 +31,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class BlinkSensor(Entity): class BlinkSensor(Entity):
"""A Blink camera sensor.""" """A Blink camera sensor."""
def __init__(self, name, sensor_type, index, data): def __init__(self, data, camera, sensor_type):
"""Initialize sensors from Blink camera.""" """Initialize sensors from Blink camera."""
self._name = 'blink_' + name + '_' + SENSOR_TYPES[sensor_type][0] name, units, icon = SENSORS[sensor_type]
self._name = "{} {} {}".format(
BLINK_DATA, camera, name)
self._camera_name = name self._camera_name = name
self._type = sensor_type self._type = sensor_type
self.data = data self.data = data
self.index = index self._camera = data.sync.cameras[camera]
self._state = None self._state = None
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._unit_of_measurement = units
self._icon = icon
@property @property
def name(self): def name(self):
"""Return the name of the camera.""" """Return the name of the camera."""
return self._name return self._name
@property
def icon(self):
"""Return the icon of the sensor."""
return self._icon
@property @property
def state(self): def state(self):
"""Return the camera's current state.""" """Return the camera's current state."""
@ -68,13 +66,11 @@ class BlinkSensor(Entity):
def update(self): def update(self):
"""Retrieve sensor data from the camera.""" """Retrieve sensor data from the camera."""
camera = self.data.cameras[self._camera_name] self.data.refresh()
if self._type == 'temperature': try:
self._state = camera.temperature self._state = self._camera.attributes[self._type]
elif self._type == 'battery': except KeyError:
self._state = camera.battery_string
elif self._type == 'notifications':
self._state = camera.notifications
else:
self._state = None self._state = None
_LOGGER.warning("Could not retrieve state from %s", self.name) _LOGGER.error(
"%s not a valid camera attribute. Did the API change?",
self._type)

View file

@ -178,7 +178,7 @@ bellows==0.7.0
bimmer_connected==0.5.3 bimmer_connected==0.5.3
# homeassistant.components.blink # homeassistant.components.blink
blinkpy==0.6.0 blinkpy==0.9.0
# homeassistant.components.light.blinksticklight # homeassistant.components.light.blinksticklight
blinkstick==1.1.8 blinkstick==1.1.8