diff --git a/.coveragerc b/.coveragerc index fae3ebebbe7..fdd36f55925 100644 --- a/.coveragerc +++ b/.coveragerc @@ -56,7 +56,7 @@ omit = homeassistant/components/bbb_gpio.py homeassistant/components/*/bbb_gpio.py - homeassistant/components/blink.py + homeassistant/components/blink/* homeassistant/components/*/blink.py homeassistant/components/bloomsky.py diff --git a/homeassistant/components/alarm_control_panel/blink.py b/homeassistant/components/alarm_control_panel/blink.py new file mode 100644 index 00000000000..850ac52fda4 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/blink.py @@ -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() diff --git a/homeassistant/components/binary_sensor/blink.py b/homeassistant/components/binary_sensor/blink.py index 6ade20b72b9..6519d09a29a 100644 --- a/homeassistant/components/binary_sensor/blink.py +++ b/homeassistant/components/binary_sensor/blink.py @@ -2,10 +2,11 @@ Support for Blink system camera control. 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.const import CONF_MONITORED_CONDITIONS DEPENDENCIES = ['blink'] @@ -14,24 +15,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the blink binary sensors.""" if discovery_info is None: return + data = hass.data[BLINK_DATA] - data = hass.data[DOMAIN].blink - devs = list() - for name in data.cameras: - devs.append(BlinkCameraMotionSensor(name, data)) - devs.append(BlinkSystemSensor(data)) + devs = [] + for camera in data.sync.cameras: + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + devs.append(BlinkBinarySensor(data, camera, sensor_type)) add_entities(devs, True) -class BlinkCameraMotionSensor(BinarySensorDevice): +class BlinkBinarySensor(BinarySensorDevice): """Representation of a Blink binary sensor.""" - def __init__(self, name, data): + def __init__(self, data, camera, sensor_type): """Initialize the sensor.""" - self._name = 'blink_' + name + '_motion_enabled' - self._camera_name = name 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 def name(self): @@ -46,29 +50,4 @@ class BlinkCameraMotionSensor(BinarySensorDevice): def update(self): """Update sensor state.""" self.data.refresh() - self._state = self.data.cameras[self._camera_name].armed - - -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 + self._state = self._camera.attributes[self._type] diff --git a/homeassistant/components/blink.py b/homeassistant/components/blink.py deleted file mode 100644 index e84643711eb..00000000000 --- a/homeassistant/components/blink.py +++ /dev/null @@ -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 diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py new file mode 100644 index 00000000000..1d84b5be113 --- /dev/null +++ b/homeassistant/components/blink/__init__.py @@ -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) diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml new file mode 100644 index 00000000000..fc042b0d598 --- /dev/null +++ b/homeassistant/components/blink/services.yaml @@ -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' diff --git a/homeassistant/components/camera/blink.py b/homeassistant/components/camera/blink.py index 217849138c3..5a728e92ce3 100644 --- a/homeassistant/components/camera/blink.py +++ b/homeassistant/components/camera/blink.py @@ -4,31 +4,27 @@ Support for Blink system camera. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.blink/ """ -from datetime import timedelta import logging -import requests - -from homeassistant.components.blink import DOMAIN +from homeassistant.components.blink import BLINK_DATA, DEFAULT_BRAND from homeassistant.components.camera import Camera -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) 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): """Set up a Blink Camera.""" if discovery_info is None: return - - data = hass.data[DOMAIN].blink - devs = list() - for name in data.cameras: - devs.append(BlinkCamera(hass, config, data, name)) + data = hass.data[BLINK_DATA] + devs = [] + for name, camera in data.sync.cameras.items(): + devs.append(BlinkCamera(data, name, camera)) add_entities(devs) @@ -36,15 +32,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BlinkCamera(Camera): """An implementation of a Blink Camera.""" - def __init__(self, hass, config, data, name): + def __init__(self, data, name, camera): """Initialize a camera.""" super().__init__() self.data = data - self.hass = hass - self._name = name - self.notifications = self.data.cameras[self._name].notifications + self._name = "{} {}".format(BLINK_DATA, name) + self._camera = camera self.response = None - + self.current_image = None + self.last_image = None _LOGGER.debug("Initialized blink camera %s", self._name) @property @@ -52,30 +48,29 @@ class BlinkCamera(Camera): """Return the camera name.""" return self._name - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def request_image(self): - """Request a new image from Blink servers.""" - _LOGGER.debug("Requesting new image from blink servers") - image_url = self.check_for_motion() - header = self.data.cameras[self._name].header - self.response = requests.get(image_url, headers=header, stream=True) + @property + def device_state_attributes(self): + """Return the camera attributes.""" + return self._camera.attributes - def check_for_motion(self): - """Check if motion has been detected since last update.""" - self.data.refresh() - 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 + def enable_motion_detection(self): + """Enable motion detection for the camera.""" + self._camera.set_motion_detect(True) - 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): """Return a still image response from the camera.""" - self.request_image() - return self.response.content + return self._camera.image_from_cache.content diff --git a/homeassistant/components/sensor/blink.py b/homeassistant/components/sensor/blink.py index 97356b6fc61..885bb939edf 100644 --- a/homeassistant/components/sensor/blink.py +++ b/homeassistant/components/sensor/blink.py @@ -6,34 +6,24 @@ https://home-assistant.io/components/sensor.blink/ """ import logging -from homeassistant.components.blink import DOMAIN -from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.components.blink import BLINK_DATA, SENSORS from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_MONITORED_CONDITIONS _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['blink'] -SENSOR_TYPES = { - 'temperature': ['Temperature', TEMP_FAHRENHEIT], - 'battery': ['Battery', ''], - 'notifications': ['Notifications', ''] -} - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a Blink sensor.""" if discovery_info is None: return - - data = hass.data[DOMAIN].blink - devs = list() - index = 0 - for name in data.cameras: - devs.append(BlinkSensor(name, 'temperature', index, data)) - devs.append(BlinkSensor(name, 'battery', index, data)) - devs.append(BlinkSensor(name, 'notifications', index, data)) - index += 1 + data = hass.data[BLINK_DATA] + devs = [] + for camera in data.sync.cameras: + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + devs.append(BlinkSensor(data, camera, sensor_type)) add_entities(devs, True) @@ -41,21 +31,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BlinkSensor(Entity): """A Blink camera sensor.""" - def __init__(self, name, sensor_type, index, data): + def __init__(self, data, camera, sensor_type): """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._type = sensor_type self.data = data - self.index = index + self._camera = data.sync.cameras[camera] self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._unit_of_measurement = units + self._icon = icon @property def name(self): """Return the name of the camera.""" return self._name + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon + @property def state(self): """Return the camera's current state.""" @@ -68,13 +66,11 @@ class BlinkSensor(Entity): def update(self): """Retrieve sensor data from the camera.""" - camera = self.data.cameras[self._camera_name] - if self._type == 'temperature': - self._state = camera.temperature - elif self._type == 'battery': - self._state = camera.battery_string - elif self._type == 'notifications': - self._state = camera.notifications - else: + self.data.refresh() + try: + self._state = self._camera.attributes[self._type] + except KeyError: 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) diff --git a/requirements_all.txt b/requirements_all.txt index ad329545966..47357131715 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -178,7 +178,7 @@ bellows==0.7.0 bimmer_connected==0.5.3 # homeassistant.components.blink -blinkpy==0.6.0 +blinkpy==0.9.0 # homeassistant.components.light.blinksticklight blinkstick==1.1.8