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

Blink auth flow improvement and mini camera support #38027

Merged
merged 15 commits into from Aug 5, 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
102 changes: 43 additions & 59 deletions homeassistant/components/blink/__init__.py
@@ -1,32 +1,25 @@
"""Support for Blink Home Camera System."""
import asyncio
from copy import deepcopy
import logging

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

from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
CONF_FILENAME,
CONF_NAME,
CONF_PASSWORD,
CONF_PIN,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv

from .const import (
DEFAULT_OFFSET,
from homeassistant.components import persistent_notification
from homeassistant.components.blink.const import (
DEFAULT_SCAN_INTERVAL,
DEVICE_ID,
DOMAIN,
PLATFORMS,
SERVICE_REFRESH,
SERVICE_SAVE_VIDEO,
SERVICE_SEND_PIN,
)
from homeassistant.const import CONF_FILENAME, CONF_NAME, CONF_PIN, CONF_SCAN_INTERVAL
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv

_LOGGER = logging.getLogger(__name__)

Expand All @@ -35,58 +28,50 @@
)
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): int,
}
)
},
extra=vol.ALLOW_EXTRA,
)


def _blink_startup_wrapper(entry):
def _blink_startup_wrapper(hass, 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 = Blink()
auth_data = deepcopy(dict(entry.data))
blink.auth = Auth(auth_data, no_prompt=True)
blink.refresh_rate = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)

try:
blink.login_response = entry.data["login_response"]
blink.setup_params(entry.data["login_response"])
except KeyError:
blink.get_auth_token()
if blink.start():
blink.setup_post_verify()
elif blink.auth.check_key_required():
_LOGGER.debug("Attempting a reauth flow")
_reauth_flow_wrapper(hass, auth_data)

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


def _reauth_flow_wrapper(hass, data):
"""Reauth flow wrapper."""
hass.add_job(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": "reauth"}, data=data
)
)
persistent_notification.async_create(
hass,
"Blink configuration migrated to a new version. Please go to the integrations page to re-configure (such as sending a new 2FA key).",
"Blink Migration",
)


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

conf = config.get(DOMAIN, {})
return True

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
)
)

async def async_migrate_entry(hass, entry):
"""Handle migration of a previous version config entry."""
data = {**entry.data}
if entry.version == 1:
data.pop("login_response", None)
await hass.async_add_executor_job(_reauth_flow_wrapper, hass, data)
return False
return True


Expand All @@ -95,12 +80,11 @@ async def async_setup_entry(hass, entry):
_async_import_options_from_data_if_missing(hass, entry)

hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job(
_blink_startup_wrapper, entry
_blink_startup_wrapper, hass, entry
)

if not hass.data[DOMAIN][entry.entry_id].available:
_LOGGER.error("Blink unavailable for setup")
return False
raise ConfigEntryNotReady

for component in PLATFORMS:
hass.async_create_task(
Expand All @@ -118,7 +102,7 @@ async def async_save_video(call):
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].auth.send_auth_key(
hass.data[DOMAIN][entry.entry_id], pin,
)

Expand Down
94 changes: 54 additions & 40 deletions homeassistant/components/blink/config_flow.py
@@ -1,10 +1,16 @@
"""Config flow to configure Blink."""
import logging

from blinkpy.blinkpy import Blink
from blinkpy.auth import Auth, LoginError, TokenRefreshFailed
from blinkpy.blinkpy import Blink, BlinkSetupError
import voluptuous as vol

from homeassistant import config_entries, core, exceptions
from homeassistant.components.blink.const import (
DEFAULT_SCAN_INTERVAL,
DEVICE_ID,
DOMAIN,
)
from homeassistant.const import (
CONF_PASSWORD,
CONF_PIN,
Expand All @@ -13,36 +19,36 @@
)
from homeassistant.core import callback

from .const import DEFAULT_OFFSET, DEFAULT_SCAN_INTERVAL, DEVICE_ID, DOMAIN

_LOGGER = logging.getLogger(__name__)


async def validate_input(hass: core.HomeAssistant, blink):
def validate_input(hass: core.HomeAssistant, auth):
"""Validate the user input allows us to connect."""
response = await hass.async_add_executor_job(blink.get_auth_token)
if not response:
try:
auth.startup()
except (LoginError, TokenRefreshFailed):
raise InvalidAuth
if blink.key_required:
if auth.check_key_required():
raise Require2FA

return blink.login_response

def _send_blink_2fa_pin(auth, pin):
"""Send 2FA pin to blink servers."""
blink = Blink()
blink.auth = auth
blink.setup_urls()
return auth.send_auth_key(blink, pin)


class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Blink config flow."""

VERSION = 1
VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

def __init__(self):
"""Initialize the blink flow."""
self.blink = None
self.data = {
CONF_USERNAME: "",
CONF_PASSWORD: "",
"login_response": None,
}
self.auth = None

@staticmethod
@callback
Expand All @@ -53,28 +59,19 @@ def async_get_options_flow(config_entry):
async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
errors = {}
data = {CONF_USERNAME: "", CONF_PASSWORD: "", "device_id": DEVICE_ID}
if user_input is not None:
self.data[CONF_USERNAME] = user_input["username"]
self.data[CONF_PASSWORD] = user_input["password"]

await self.async_set_unique_id(self.data[CONF_USERNAME])
data[CONF_USERNAME] = user_input["username"]
data[CONF_PASSWORD] = user_input["password"]

if CONF_SCAN_INTERVAL in user_input:
self.data[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL]

self.blink = Blink(
username=self.data[CONF_USERNAME],
password=self.data[CONF_PASSWORD],
motion_interval=DEFAULT_OFFSET,
legacy_subdomain=False,
no_prompt=True,
device_id=DEVICE_ID,
)
self.auth = Auth(data, no_prompt=True)
await self.async_set_unique_id(data[CONF_USERNAME])

try:
response = await validate_input(self.hass, self.blink)
self.data["login_response"] = response
return self.async_create_entry(title=DOMAIN, data=self.data,)
await self.hass.async_add_executor_job(
validate_input, self.hass, self.auth
)
return self._async_finish_flow()
except Require2FA:
return await self.async_step_2fa()
except InvalidAuth:
Expand All @@ -94,23 +91,40 @@ async def async_step_user(self, user_input=None):

async def async_step_2fa(self, user_input=None):
"""Handle 2FA step."""
errors = {}
if user_input is not None:
pin = user_input.get(CONF_PIN)
if await self.hass.async_add_executor_job(
self.blink.login_handler.send_auth_key, self.blink, pin
):
return await self.async_step_user(user_input=self.data)
try:
valid_token = await self.hass.async_add_executor_job(
_send_blink_2fa_pin, self.auth, pin
)
except BlinkSetupError:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"

else:
if valid_token:
return self._async_finish_flow()
errors["base"] = "invalid_access_token"

return self.async_show_form(
step_id="2fa",
data_schema=vol.Schema(
{vol.Optional("pin"): vol.All(str, vol.Length(min=1))}
),
errors=errors,
)

async def async_step_import(self, import_data):
"""Import blink config from configuration.yaml."""
return await self.async_step_user(import_data)
async def async_step_reauth(self, entry_data):
"""Perform reauth upon migration of old entries."""
return await self.async_step_user(entry_data)

@callback
def _async_finish_flow(self):
"""Finish with setup."""
return self.async_create_entry(title=DOMAIN, data=self.auth.login_attributes)
fronzbot marked this conversation as resolved.
Show resolved Hide resolved


class BlinkOptionsFlowHandler(config_entries.OptionsFlow):
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/blink/const.py
Expand Up @@ -2,6 +2,7 @@
DOMAIN = "blink"
DEVICE_ID = "Home Assistant"

CONF_MIGRATE = "migrate"
CONF_CAMERA = "camera"
CONF_ALARM_CONTROL_PANEL = "alarm_control_panel"

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/blink/manifest.json
Expand Up @@ -2,7 +2,7 @@
"domain": "blink",
"name": "Blink",
"documentation": "https://www.home-assistant.io/integrations/blink",
"requirements": ["blinkpy==0.15.1"],
"requirements": ["blinkpy==0.16.3"],
"codeowners": ["@fronzbot"],
"config_flow": true
}
4 changes: 3 additions & 1 deletion homeassistant/components/blink/strings.json
Expand Up @@ -11,11 +11,13 @@
"2fa": {
"title": "Two-factor authentication",
"data": { "2fa": "Two-factor code" },
"description": "Enter the pin sent to your email. If the email does not contain a pin, leave blank"
"description": "Enter the pin sent to your email"
}
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Expand Up @@ -340,7 +340,7 @@ bizkaibus==0.1.1
blebox_uniapi==1.3.2

# homeassistant.components.blink
blinkpy==0.15.1
blinkpy==0.16.3

# homeassistant.components.blinksticklight
blinkstick==1.1.8
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Expand Up @@ -181,7 +181,7 @@ bellows==0.18.0
blebox_uniapi==1.3.2

# homeassistant.components.blink
blinkpy==0.15.1
blinkpy==0.16.3

# homeassistant.components.bom
bomradarloop==0.1.4
Expand Down