Add config flow and 2FA support for Blink (#35396)

This commit is contained in:
Kevin Fronczak 2020-05-13 09:50:29 -04:00 committed by GitHub
parent 0a94d9b284
commit 85726b67b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 378 additions and 155 deletions

View File

@ -1,81 +1,40 @@
"""Support for Blink Home Camera System.""" """Support for Blink Home Camera System."""
from datetime import timedelta import asyncio
import logging import logging
from blinkpy import blinkpy from blinkpy.blinkpy import Blink
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import ( from homeassistant.const import (
CONF_BINARY_SENSORS,
CONF_FILENAME, CONF_FILENAME,
CONF_MODE,
CONF_MONITORED_CONDITIONS,
CONF_NAME, CONF_NAME,
CONF_OFFSET,
CONF_PASSWORD, CONF_PASSWORD,
CONF_PIN,
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
CONF_SENSORS,
CONF_USERNAME, CONF_USERNAME,
TEMP_FAHRENHEIT,
) )
from homeassistant.helpers import config_validation as cv, discovery 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,
)
_LOGGER = logging.getLogger(__name__) _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)]
)
}
)
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_TRIGGER_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string})
SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema( SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
{vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FILENAME): cv.string} {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( CONFIG_SCHEMA = vol.Schema(
{ {
@ -83,13 +42,7 @@ CONFIG_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional( vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): int,
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,
} }
) )
}, },
@ -97,61 +50,127 @@ CONFIG_SCHEMA = vol.Schema(
) )
def setup(hass, config): def _blink_startup_wrapper(entry):
"""Set up Blink System.""" """Startup wrapper for blink."""
blink = Blink(
conf = config[BLINK_DATA] username=entry.data[CONF_USERNAME],
username = conf[CONF_USERNAME] password=entry.data[CONF_PASSWORD],
password = conf[CONF_PASSWORD] motion_interval=DEFAULT_OFFSET,
scan_interval = conf[CONF_SCAN_INTERVAL] legacy_subdomain=False,
is_legacy = bool(conf[CONF_MODE] == "legacy") no_prompt=True,
motion_interval = conf[CONF_OFFSET] device_id=DEVICE_ID,
hass.data[BLINK_DATA] = blinkpy.Blink(
username=username,
password=password,
motion_interval=motion_interval,
legacy_subdomain=is_legacy,
) )
hass.data[BLINK_DATA].refresh_rate = scan_interval.total_seconds() blink.refresh_rate = entry.data[CONF_SCAN_INTERVAL]
hass.data[BLINK_DATA].start()
platforms = [ try:
("alarm_control_panel", {}), blink.login_response = entry.data["login_response"]
("binary_sensor", conf[CONF_BINARY_SENSORS]), blink.setup_params(entry.data["login_response"])
("camera", {}), except KeyError:
("sensor", conf[CONF_SENSORS]), blink.get_auth_token()
]
for component, schema in platforms: blink.setup_params(entry.data["login_response"])
discovery.load_platform(hass, component, DOMAIN, schema, config) blink.setup_post_verify()
return blink
def trigger_camera(call):
"""Trigger a camera."""
cameras = hass.data[BLINK_DATA].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): async def async_setup(hass, config):
"""Call blink to refresh info.""" """Set up a config entry."""
hass.data[BLINK_DATA].refresh(force_cache=True) hass.data[DOMAIN] = {}
if DOMAIN not in config:
return True
async def async_save_video(call): conf = config.get(DOMAIN, {})
"""Call save video service handler."""
await async_handle_save_video_service(hass, call) 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
)
)
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 return True
async def async_handle_save_video_service(hass, call): 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
)
if not hass.data[DOMAIN][entry.entry_id].available:
_LOGGER.error("Blink unavailable for setup")
return False
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[DOMAIN][entry.entry_id].cameras
name = call.data[CONF_NAME]
if name in cameras:
cameras[name].snap_picture()
blink_refresh()
def blink_refresh(event_time=None):
"""Call blink to refresh info."""
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, entry, 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], 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.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, entry, call):
"""Handle save video service calls.""" """Handle save video service calls."""
camera_name = call.data[CONF_NAME] camera_name = call.data[CONF_NAME]
video_path = call.data[CONF_FILENAME] video_path = call.data[CONF_FILENAME]
@ -161,7 +180,7 @@ async def async_handle_save_video_service(hass, call):
def _write_video(camera_name, video_path): def _write_video(camera_name, video_path):
"""Call video write.""" """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: if camera_name in all_cameras:
all_cameras[camera_name].video_to_file(video_path) all_cameras[camera_name].video_to_file(video_path)

View File

@ -9,23 +9,21 @@ from homeassistant.const import (
STATE_ALARM_DISARMED, STATE_ALARM_DISARMED,
) )
from . import BLINK_DATA, DEFAULT_ATTRIBUTION from .const import DEFAULT_ATTRIBUTION, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ICON = "mdi:security" ICON = "mdi:security"
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config, async_add_entities):
"""Set up the Arlo Alarm Control Panels.""" """Set up the Blink Alarm Control Panels."""
if discovery_info is None: data = hass.data[DOMAIN][config.entry_id]
return
data = hass.data[BLINK_DATA]
sync_modules = [] sync_modules = []
for sync_name, sync_module in data.sync.items(): for sync_name, sync_module in data.sync.items():
sync_modules.append(BlinkSyncModule(data, sync_name, sync_module)) sync_modules.append(BlinkSyncModule(data, sync_name, sync_module))
add_entities(sync_modules, True) async_add_entities(sync_modules)
class BlinkSyncModule(AlarmControlPanelEntity): class BlinkSyncModule(AlarmControlPanelEntity):
@ -61,7 +59,7 @@ class BlinkSyncModule(AlarmControlPanelEntity):
@property @property
def name(self): def name(self):
"""Return the name of the panel.""" """Return the name of the panel."""
return f"{BLINK_DATA} {self._name}" return f"{DOMAIN} {self._name}"
@property @property
def device_state_attributes(self): def device_state_attributes(self):

View File

@ -1,21 +1,23 @@
"""Support for Blink system camera control.""" """Support for Blink system camera control."""
from homeassistant.components.binary_sensor import BinarySensorEntity 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.""" """Set up the blink binary sensors."""
if discovery_info is None: data = hass.data[DOMAIN][config.entry_id]
return
data = hass.data[BLINK_DATA]
devs = [] entities = []
for camera in data.cameras: for camera in data.cameras:
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: for sensor_type in BINARY_SENSORS:
devs.append(BlinkBinarySensor(data, camera, sensor_type)) entities.append(BlinkBinarySensor(data, camera, sensor_type))
add_entities(devs, True) async_add_entities(entities)
class BlinkBinarySensor(BinarySensorEntity): class BlinkBinarySensor(BinarySensorEntity):
@ -26,7 +28,7 @@ class BlinkBinarySensor(BinarySensorEntity):
self.data = data self.data = data
self._type = sensor_type self._type = sensor_type
name, icon = BINARY_SENSORS[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._icon = icon
self._camera = data.cameras[camera] self._camera = data.cameras[camera]
self._state = None self._state = None

View File

@ -3,7 +3,7 @@ import logging
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from . import BLINK_DATA, DEFAULT_BRAND from .const import DEFAULT_BRAND, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -11,16 +11,14 @@ ATTR_VIDEO_CLIP = "video"
ATTR_IMAGE = "image" ATTR_IMAGE = "image"
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config, async_add_entities):
"""Set up a Blink Camera.""" """Set up a Blink Camera."""
if discovery_info is None: data = hass.data[DOMAIN][config.entry_id]
return entities = []
data = hass.data[BLINK_DATA]
devs = []
for name, camera in data.cameras.items(): for name, camera in data.cameras.items():
devs.append(BlinkCamera(data, name, camera)) entities.append(BlinkCamera(data, name, camera))
add_entities(devs) async_add_entities(entities)
class BlinkCamera(Camera): class BlinkCamera(Camera):
@ -30,7 +28,7 @@ class BlinkCamera(Camera):
"""Initialize a camera.""" """Initialize a camera."""
super().__init__() super().__init__()
self.data = data self.data = data
self._name = f"{BLINK_DATA} {name}" self._name = f"{DOMAIN} {name}"
self._camera = camera self._camera = camera
self._unique_id = f"{camera.serial}-camera" self._unique_id = f"{camera.serial}-camera"
self.response = None self.response = None

View File

@ -0,0 +1,115 @@
"""Config flow to configure Blink."""
import logging
from blinkpy.blinkpy import Blink
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.const import (
CONF_PASSWORD,
CONF_PIN,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
)
from .const import DEFAULT_OFFSET, DEFAULT_SCAN_INTERVAL, DEVICE_ID, DOMAIN
_LOGGER = logging.getLogger(__name__)
async def validate_input(hass: core.HomeAssistant, blink):
"""Validate the user input allows us to connect."""
response = await hass.async_add_executor_job(blink.get_auth_token)
if not response:
raise InvalidAuth
if blink.key_required:
raise Require2FA
return blink.login_response
class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Blink config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize the blink flow."""
self.blink = None
self.data = {
CONF_USERNAME: "",
CONF_PASSWORD: "",
CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
"login_response": None,
}
async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
errors = {}
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])
if CONF_SCAN_INTERVAL in user_input:
self.data[CONF_SCAN_INTERVAL] = user_input["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,
)
try:
response = await validate_input(self.hass, self.blink)
self.data["login_response"] = response
return self.async_create_entry(title=DOMAIN, data=self.data,)
except Require2FA:
return await self.async_step_2fa()
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
data_schema = {
vol.Required("username"): str,
vol.Required("password"): str,
}
return self.async_show_form(
step_id="user", data_schema=vol.Schema(data_schema), errors=errors,
)
async def async_step_2fa(self, user_input=None):
"""Handle 2FA step."""
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)
return self.async_show_form(
step_id="2fa",
data_schema=vol.Schema(
{vol.Optional("pin"): vol.All(str, vol.Length(min=1))}
),
)
async def async_step_import(self, import_data):
"""Import blink config from configuration.yaml."""
return await self.async_step_user(import_data)
class Require2FA(exceptions.HomeAssistantError):
"""Error to indicate we require 2FA."""
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@ -0,0 +1,25 @@
"""Constants for Blink."""
DOMAIN = "blink"
DEVICE_ID = "Home Assistant"
CONF_CAMERA = "camera"
CONF_ALARM_CONTROL_PANEL = "alarm_control_panel"
DEFAULT_BRAND = "Blink"
DEFAULT_ATTRIBUTION = "Data provided by immedia-semi.com"
DEFAULT_SCAN_INTERVAL = 300
DEFAULT_OFFSET = 1
SIGNAL_UPDATE_BLINK = "blink_update"
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"
SERVICE_SEND_PIN = "send_pin"
PLATFORMS = ("alarm_control_panel", "binary_sensor", "camera", "sensor")

View File

@ -2,6 +2,7 @@
"domain": "blink", "domain": "blink",
"name": "Blink", "name": "Blink",
"documentation": "https://www.home-assistant.io/integrations/blink", "documentation": "https://www.home-assistant.io/integrations/blink",
"requirements": ["blinkpy==0.14.3"], "requirements": ["blinkpy==0.15.0"],
"codeowners": ["@fronzbot"] "codeowners": ["@fronzbot"],
"config_flow": true
} }

View File

@ -1,25 +1,29 @@
"""Support for Blink system camera sensors.""" """Support for Blink system camera sensors."""
import logging import logging
from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.const import TEMP_FAHRENHEIT
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from . import BLINK_DATA, SENSORS from .const import DOMAIN, TYPE_BATTERY, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SENSORS = {
TYPE_TEMPERATURE: ["Temperature", TEMP_FAHRENHEIT, "mdi:thermometer"],
TYPE_BATTERY: ["Battery", "", "mdi:battery-80"],
TYPE_WIFI_STRENGTH: ["Wifi Signal", "dBm", "mdi:wifi-strength-2"],
}
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a Blink sensor.""" async def async_setup_entry(hass, config, async_add_entities):
if discovery_info is None: """Initialize a Blink sensor."""
return data = hass.data[DOMAIN][config.entry_id]
data = hass.data[BLINK_DATA] entities = []
devs = []
for camera in data.cameras: for camera in data.cameras:
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: for sensor_type in SENSORS:
devs.append(BlinkSensor(data, camera, sensor_type)) entities.append(BlinkSensor(data, camera, sensor_type))
add_entities(devs, True) async_add_entities(entities)
class BlinkSensor(Entity): class BlinkSensor(Entity):
@ -28,7 +32,7 @@ class BlinkSensor(Entity):
def __init__(self, data, camera, sensor_type): def __init__(self, data, camera, sensor_type):
"""Initialize sensors from Blink camera.""" """Initialize sensors from Blink camera."""
name, units, icon = SENSORS[sensor_type] name, units, icon = SENSORS[sensor_type]
self._name = f"{BLINK_DATA} {camera} {name}" self._name = f"{DOMAIN} {camera} {name}"
self._camera_name = name self._camera_name = name
self._type = sensor_type self._type = sensor_type
self.data = data self.data = data

View File

@ -19,3 +19,10 @@ save_video:
filename: filename:
description: Filename to writable path (directory may need to be included in whitelist_dirs in config) description: Filename to writable path (directory may need to be included in whitelist_dirs in config)
example: "/tmp/video.mp4" example: "/tmp/video.mp4"
send_pin:
description: Send a new pin to blink for 2FA.
fields:
pin:
description: Pin received from blink. Leave empty if you only received a verification email.
example: "abc123"

View File

@ -0,0 +1,25 @@
{
"config": {
"step": {
"user": {
"title": "Sign-in with Blink account",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"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"
}
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@ -0,0 +1,28 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"2fa": {
"data": {
"2fa": "Two-factor code"
},
"title": "Two-factor authentication",
"description": "Enter the pin sent to your email. If the email does not contain a pin, leave blank"
},
"user": {
"data": {
"password": "Password",
"username": "Username",
"scan_interval": "Scan Interval"
},
"title": "Sign-in with Blink account"
}
}
}
}

View File

@ -18,6 +18,7 @@ FLOWS = [
"august", "august",
"axis", "axis",
"blebox", "blebox",
"blink",
"braviatv", "braviatv",
"brother", "brother",
"bsblan", "bsblan",

View File

@ -345,7 +345,7 @@ bizkaibus==0.1.1
blebox_uniapi==1.3.2 blebox_uniapi==1.3.2
# homeassistant.components.blink # homeassistant.components.blink
blinkpy==0.14.3 blinkpy==0.15.0
# homeassistant.components.blinksticklight # homeassistant.components.blinksticklight
blinkstick==1.1.8 blinkstick==1.1.8