Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add config flow and 2FA support for Blink #35396

Merged
merged 16 commits into from May 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
219 changes: 119 additions & 100 deletions homeassistant/components/blink/__init__.py
@@ -1,157 +1,176 @@
"""Support for Blink Home Camera System."""
from datetime import timedelta
import asyncio
import logging

from blinkpy import blinkpy
from blinkpy.blinkpy import Blink
import voluptuous as vol

from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
CONF_BINARY_SENSORS,
CONF_FILENAME,
CONF_MODE,
CONF_MONITORED_CONDITIONS,
CONF_NAME,
CONF_OFFSET,
CONF_PASSWORD,
CONF_PIN,
CONF_SCAN_INTERVAL,
CONF_SENSORS,
CONF_USERNAME,
TEMP_FAHRENHEIT,
)
from homeassistant.helpers import config_validation as cv, discovery

_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=300)

TYPE_CAMERA_ARMED = "motion_enabled"
TYPE_MOTION_DETECTED = "motion_detected"
TYPE_TEMPERATURE = "temperature"
TYPE_BATTERY = "battery"
TYPE_WIFI_STRENGTH = "wifi_strength"

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"],
}

BINARY_SENSOR_SCHEMA = vol.Schema(
{
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): vol.All(
cv.ensure_list, [vol.In(BINARY_SENSORS)]
)
}
from homeassistant.helpers import config_validation as cv

from .const import (
DEFAULT_OFFSET,
DEFAULT_SCAN_INTERVAL,
DEVICE_ID,
DOMAIN,
PLATFORMS,
SERVICE_REFRESH,
SERVICE_SAVE_VIDEO,
SERVICE_SEND_PIN,
SERVICE_TRIGGER,
)

SENSOR_SCHEMA = vol.Schema(
{
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All(
cv.ensure_list, [vol.In(SENSORS)]
)
}
)
_LOGGER = logging.getLogger(__name__)

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}
)
SERVICE_SEND_PIN_SCHEMA = vol.Schema({vol.Optional(CONF_PIN): 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,
vol.Optional(CONF_OFFSET, default=1): int,
vol.Optional(CONF_MODE, default=""): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): int,
}
)
},
extra=vol.ALLOW_EXTRA,
)


def setup(hass, config):
"""Set up Blink System."""

conf = config[BLINK_DATA]
username = conf[CONF_USERNAME]
password = conf[CONF_PASSWORD]
scan_interval = conf[CONF_SCAN_INTERVAL]
is_legacy = bool(conf[CONF_MODE] == "legacy")
motion_interval = conf[CONF_OFFSET]
hass.data[BLINK_DATA] = blinkpy.Blink(
username=username,
password=password,
motion_interval=motion_interval,
legacy_subdomain=is_legacy,
def _blink_startup_wrapper(entry):
"""Startup wrapper for blink."""
blink = Blink(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
motion_interval=DEFAULT_OFFSET,
legacy_subdomain=False,
no_prompt=True,
device_id=DEVICE_ID,
)
blink.refresh_rate = entry.data[CONF_SCAN_INTERVAL]

try:
blink.login_response = entry.data["login_response"]
blink.setup_params(entry.data["login_response"])
except KeyError:
blink.get_auth_token()

blink.setup_params(entry.data["login_response"])
blink.setup_post_verify()
return blink


async def async_setup(hass, config):
"""Set up a config entry."""
hass.data[DOMAIN] = {}
if DOMAIN not in config:
return True

conf = config.get(DOMAIN, {})

if not hass.config_entries.async_entries(DOMAIN):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
)

return True


async def async_setup_entry(hass, entry):
"""Set up Blink via config entry."""
hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job(
_blink_startup_wrapper, entry
)
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]),
]
if not hass.data[DOMAIN][entry.entry_id].available:
_LOGGER.error("Blink unavailable for setup")
return False

for component, schema in platforms:
discovery.load_platform(hass, component, DOMAIN, schema, config)
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)

def trigger_camera(call):
"""Trigger a camera."""
cameras = hass.data[BLINK_DATA].cameras
cameras = hass.data[DOMAIN][entry.entry_id].cameras
name = call.data[CONF_NAME]
if name in cameras:
cameras[name].snap_picture()
hass.data[BLINK_DATA].refresh(force_cache=True)
blink_refresh()

def blink_refresh(event_time):
def blink_refresh(event_time=None):
"""Call blink to refresh info."""
hass.data[BLINK_DATA].refresh(force_cache=True)
hass.data[DOMAIN][entry.entry_id].refresh(force_cache=True)

async def async_save_video(call):
"""Call save video service handler."""
await async_handle_save_video_service(hass, call)
await async_handle_save_video_service(hass, entry, call)

hass.services.register(DOMAIN, SERVICE_REFRESH, blink_refresh)
hass.services.register(
def send_pin(call):
"""Call blink to send new pin."""
pin = call.data[CONF_PIN]
hass.data[DOMAIN][entry.entry_id].login_handler.send_auth_key(
hass.data[DOMAIN][entry.entry_id], pin,
)

hass.services.async_register(DOMAIN, SERVICE_REFRESH, blink_refresh)
hass.services.async_register(
DOMAIN, SERVICE_TRIGGER, trigger_camera, schema=SERVICE_TRIGGER_SCHEMA
)
hass.services.register(
hass.services.async_register(
DOMAIN, SERVICE_SAVE_VIDEO, async_save_video, schema=SERVICE_SAVE_VIDEO_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_SEND_PIN, send_pin, schema=SERVICE_SEND_PIN_SCHEMA
)

return True


async def async_unload_entry(hass, entry):
"""Unload Blink entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)

if not unload_ok:
return False

hass.data[DOMAIN].pop(entry.entry_id)

if len(hass.data[DOMAIN]) != 0:
return True

hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
hass.services.async_remove(DOMAIN, SERVICE_TRIGGER)
hass.services.async_remove(DOMAIN, SERVICE_SAVE_VIDEO_SCHEMA)
hass.services.async_remove(DOMAIN, SERVICE_SEND_PIN)

return True


async def async_handle_save_video_service(hass, call):
async def async_handle_save_video_service(hass, entry, call):
"""Handle save video service calls."""
camera_name = call.data[CONF_NAME]
video_path = call.data[CONF_FILENAME]
Expand All @@ -161,7 +180,7 @@ async def async_handle_save_video_service(hass, call):

def _write_video(camera_name, video_path):
"""Call video write."""
all_cameras = hass.data[BLINK_DATA].cameras
all_cameras = hass.data[DOMAIN][entry.entry_id].cameras
if camera_name in all_cameras:
all_cameras[camera_name].video_to_file(video_path)

Expand Down
14 changes: 6 additions & 8 deletions homeassistant/components/blink/alarm_control_panel.py
Expand Up @@ -9,23 +9,21 @@
STATE_ALARM_DISARMED,
)

from . import BLINK_DATA, DEFAULT_ATTRIBUTION
from .const import DEFAULT_ATTRIBUTION, DOMAIN

_LOGGER = logging.getLogger(__name__)

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]
async def async_setup_entry(hass, config, async_add_entities):
"""Set up the Blink Alarm Control Panels."""
data = hass.data[DOMAIN][config.entry_id]

sync_modules = []
for sync_name, sync_module in data.sync.items():
sync_modules.append(BlinkSyncModule(data, sync_name, sync_module))
add_entities(sync_modules, True)
async_add_entities(sync_modules)


class BlinkSyncModule(AlarmControlPanelEntity):
Expand Down Expand Up @@ -61,7 +59,7 @@ def supported_features(self) -> int:
@property
def name(self):
"""Return the name of the panel."""
return f"{BLINK_DATA} {self._name}"
return f"{DOMAIN} {self._name}"
fronzbot marked this conversation as resolved.
Show resolved Hide resolved

@property
def device_state_attributes(self):
Expand Down
24 changes: 13 additions & 11 deletions homeassistant/components/blink/binary_sensor.py
@@ -1,21 +1,23 @@
"""Support for Blink system camera control."""
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import CONF_MONITORED_CONDITIONS

from . import BINARY_SENSORS, BLINK_DATA
from .const import DOMAIN, TYPE_CAMERA_ARMED, TYPE_MOTION_DETECTED

BINARY_SENSORS = {
TYPE_CAMERA_ARMED: ["Camera Armed", "mdi:verified"],
TYPE_MOTION_DETECTED: ["Motion Detected", "mdi:run-fast"],
}

def setup_platform(hass, config, add_entities, discovery_info=None):

async def async_setup_entry(hass, config, async_add_entities):
"""Set up the blink binary sensors."""
if discovery_info is None:
return
data = hass.data[BLINK_DATA]
data = hass.data[DOMAIN][config.entry_id]

devs = []
entities = []
for camera in data.cameras:
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
devs.append(BlinkBinarySensor(data, camera, sensor_type))
add_entities(devs, True)
for sensor_type in BINARY_SENSORS:
entities.append(BlinkBinarySensor(data, camera, sensor_type))
async_add_entities(entities)


class BlinkBinarySensor(BinarySensorEntity):
Expand All @@ -26,7 +28,7 @@ def __init__(self, data, camera, sensor_type):
self.data = data
self._type = sensor_type
name, icon = BINARY_SENSORS[sensor_type]
self._name = f"{BLINK_DATA} {camera} {name}"
self._name = f"{DOMAIN} {camera} {name}"
self._icon = icon
self._camera = data.cameras[camera]
self._state = None
Expand Down