Bump Blinkpy to 0.22.2 in Blink (#98571)

This commit is contained in:
mkmer 2023-10-16 07:41:45 -04:00 committed by GitHub
parent 6b05f51413
commit eaf6197d43
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 144 additions and 107 deletions

View file

@ -1,7 +1,9 @@
"""Support for Blink Home Camera System."""
import asyncio
from copy import deepcopy
import logging
from aiohttp import ClientError
from blinkpy.auth import Auth
from blinkpy.blinkpy import Blink
import voluptuous as vol
@ -16,8 +18,9 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
DEFAULT_SCAN_INTERVAL,
@ -40,23 +43,7 @@ SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema(
)
def _blink_startup_wrapper(hass: HomeAssistant, entry: ConfigEntry) -> Blink:
"""Startup wrapper for blink."""
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)
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)
return blink
def _reauth_flow_wrapper(hass, data):
async def _reauth_flow_wrapper(hass, data):
"""Reauth flow wrapper."""
hass.add_job(
hass.config_entries.flow.async_init(
@ -79,10 +66,10 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
data = {**entry.data}
if entry.version == 1:
data.pop("login_response", None)
await hass.async_add_executor_job(_reauth_flow_wrapper, hass, data)
await _reauth_flow_wrapper(hass, data)
return False
if entry.version == 2:
await hass.async_add_executor_job(_reauth_flow_wrapper, hass, data)
await _reauth_flow_wrapper(hass, data)
return False
return True
@ -92,19 +79,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})
_async_import_options_from_data_if_missing(hass, entry)
hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job(
_blink_startup_wrapper, hass, entry
)
session = async_get_clientsession(hass)
blink = Blink(session=session)
auth_data = deepcopy(dict(entry.data))
blink.auth = Auth(auth_data, no_prompt=True, session=session)
blink.refresh_rate = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
if not hass.data[DOMAIN][entry.entry_id].available:
try:
await blink.start()
if blink.auth.check_key_required():
_LOGGER.debug("Attempting a reauth flow")
raise ConfigEntryAuthFailed("Need 2FA for Blink")
except (ClientError, asyncio.TimeoutError) as ex:
raise ConfigEntryNotReady("Can not connect to host") from ex
hass.data[DOMAIN][entry.entry_id] = blink
if not blink.available:
raise ConfigEntryNotReady
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
await blink.refresh(force=True)
def blink_refresh(event_time=None):
async def blink_refresh(event_time=None):
"""Call blink to refresh info."""
hass.data[DOMAIN][entry.entry_id].refresh(force_cache=True)
await hass.data[DOMAIN][entry.entry_id].refresh(force_cache=True)
async def async_save_video(call):
"""Call save video service handler."""
@ -114,10 +114,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Call save recent clips service handler."""
await async_handle_save_recent_clips_service(hass, entry, call)
def send_pin(call):
async def send_pin(call):
"""Call blink to send new pin."""
pin = call.data[CONF_PIN]
hass.data[DOMAIN][entry.entry_id].auth.send_auth_key(
await hass.data[DOMAIN][entry.entry_id].auth.send_auth_key(
hass.data[DOMAIN][entry.entry_id],
pin,
)
@ -176,27 +176,27 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
blink.refresh_rate = entry.options[CONF_SCAN_INTERVAL]
async def async_handle_save_video_service(hass, entry, call):
async def async_handle_save_video_service(
hass: HomeAssistant, entry: ConfigEntry, call
) -> None:
"""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(name, file_path):
"""Call video write."""
all_cameras = hass.data[DOMAIN][entry.entry_id].cameras
if name in all_cameras:
all_cameras[name].video_to_file(file_path)
try:
await hass.async_add_executor_job(_write_video, camera_name, video_path)
all_cameras = hass.data[DOMAIN][entry.entry_id].cameras
if camera_name in all_cameras:
await all_cameras[camera_name].video_to_file(video_path)
except OSError as err:
_LOGGER.error("Can't write image to file: %s", err)
async def async_handle_save_recent_clips_service(hass, entry, call):
async def async_handle_save_recent_clips_service(
hass: HomeAssistant, entry: ConfigEntry, call
) -> None:
"""Save multiple recent clips to output directory."""
camera_name = call.data[CONF_NAME]
clips_dir = call.data[CONF_FILE_PATH]
@ -204,13 +204,9 @@ async def async_handle_save_recent_clips_service(hass, entry, call):
_LOGGER.error("Can't write to directory %s, no access to path!", clips_dir)
return
def _save_recent_clips(name, output_dir):
"""Call save recent clips."""
all_cameras = hass.data[DOMAIN][entry.entry_id].cameras
if name in all_cameras:
all_cameras[name].save_recent_clips(output_dir=output_dir)
try:
await hass.async_add_executor_job(_save_recent_clips, camera_name, clips_dir)
all_cameras = hass.data[DOMAIN][entry.entry_id].cameras
if camera_name in all_cameras:
await all_cameras[camera_name].save_recent_clips(output_dir=clips_dir)
except OSError as err:
_LOGGER.error("Can't write recent clips to directory: %s", err)

View file

@ -1,8 +1,11 @@
"""Support for Blink Alarm Control Panel."""
from __future__ import annotations
import asyncio
import logging
from blinkpy.blinkpy import Blink
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
@ -16,6 +19,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN
@ -32,11 +36,11 @@ async def async_setup_entry(
sync_modules = []
for sync_name, sync_module in data.sync.items():
sync_modules.append(BlinkSyncModule(data, sync_name, sync_module))
async_add_entities(sync_modules)
sync_modules.append(BlinkSyncModuleHA(data, sync_name, sync_module))
async_add_entities(sync_modules, update_before_add=True)
class BlinkSyncModule(AlarmControlPanelEntity):
class BlinkSyncModuleHA(AlarmControlPanelEntity):
"""Representation of a Blink Alarm Control Panel."""
_attr_icon = ICON
@ -44,19 +48,19 @@ class BlinkSyncModule(AlarmControlPanelEntity):
_attr_name = None
_attr_has_entity_name = True
def __init__(self, data, name, sync):
def __init__(self, data, name: str, sync) -> None:
"""Initialize the alarm control panel."""
self.data = data
self.data: Blink = data
self.sync = sync
self._name = name
self._attr_unique_id = sync.serial
self._name: str = name
self._attr_unique_id: str = sync.serial
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, sync.serial)},
name=f"{DOMAIN} {name}",
manufacturer=DEFAULT_BRAND,
)
def update(self) -> None:
async def async_update(self) -> None:
"""Update the state of the device."""
if self.data.check_if_ok_to_update():
_LOGGER.debug(
@ -64,23 +68,38 @@ class BlinkSyncModule(AlarmControlPanelEntity):
self._name,
self.data,
)
self.data.refresh()
try:
await self.data.refresh(force=True)
self._attr_available = True
except asyncio.TimeoutError:
self._attr_available = False
_LOGGER.info("Updating State of Blink Alarm Control Panel '%s'", self._name)
self._attr_state = (
STATE_ALARM_ARMED_AWAY if self.sync.arm else STATE_ALARM_DISARMED
)
self.sync.attributes["network_info"] = self.data.networks
self.sync.attributes["associated_cameras"] = list(self.sync.cameras)
self.sync.attributes[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION
self._attr_extra_state_attributes = self.sync.attributes
def alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
self.sync.arm = False
self.sync.refresh()
@property
def state(self) -> StateType:
"""Return state of alarm."""
return STATE_ALARM_ARMED_AWAY if self.sync.arm else STATE_ALARM_DISARMED
def alarm_arm_away(self, code: str | None = None) -> None:
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
try:
await self.sync.async_arm(False)
await self.sync.refresh(force=True)
self.async_write_ha_state()
except asyncio.TimeoutError:
self._attr_available = False
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm command."""
self.sync.arm = True
self.sync.refresh()
try:
await self.sync.async_arm(True)
await self.sync.refresh(force=True)
self.async_write_ha_state()
except asyncio.TimeoutError:
self._attr_available = False

View file

@ -52,7 +52,7 @@ async def async_setup_entry(
for camera in data.cameras
for description in BINARY_SENSORS_TYPES
]
async_add_entities(entities)
async_add_entities(entities, update_before_add=True)
class BlinkBinarySensor(BinarySensorEntity):
@ -75,15 +75,16 @@ class BlinkBinarySensor(BinarySensorEntity):
model=self._camera.camera_type,
)
def update(self) -> None:
@property
def is_on(self) -> bool | None:
"""Update sensor state."""
state = self._camera.attributes[self.entity_description.key]
is_on = self._camera.attributes[self.entity_description.key]
_LOGGER.debug(
"'%s' %s = %s",
self._camera.attributes["name"],
self.entity_description.key,
state,
is_on,
)
if self.entity_description.key == TYPE_BATTERY:
state = state != "ok"
self._attr_is_on = state
is_on = is_on != "ok"
return is_on

View file

@ -1,7 +1,10 @@
"""Support for Blink system camera."""
from __future__ import annotations
import asyncio
from collections.abc import Mapping
import logging
from typing import Any
from requests.exceptions import ChunkedEncodingError
@ -29,7 +32,7 @@ async def async_setup_entry(
BlinkCamera(data, name, camera) for name, camera in data.cameras.items()
]
async_add_entities(entities)
async_add_entities(entities, update_before_add=True)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(SERVICE_TRIGGER, {}, "trigger_camera")
@ -56,19 +59,25 @@ class BlinkCamera(Camera):
_LOGGER.debug("Initialized blink camera %s", self.name)
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return the camera attributes."""
return self._camera.attributes
def enable_motion_detection(self) -> None:
async def async_enable_motion_detection(self) -> None:
"""Enable motion detection for the camera."""
self._camera.arm = True
self.data.refresh()
try:
await self._camera.async_arm(True)
await self.data.refresh(force=True)
except asyncio.TimeoutError:
self._attr_available = False
def disable_motion_detection(self) -> None:
async def async_disable_motion_detection(self) -> None:
"""Disable motion detection for the camera."""
self._camera.arm = False
self.data.refresh()
try:
await self._camera.async_arm(False)
await self.data.refresh(force=True)
except asyncio.TimeoutError:
self._attr_available = False
@property
def motion_detection_enabled(self) -> bool:
@ -76,21 +85,24 @@ class BlinkCamera(Camera):
return self._camera.arm
@property
def brand(self):
def brand(self) -> str | None:
"""Return the camera brand."""
return DEFAULT_BRAND
def trigger_camera(self):
async def trigger_camera(self) -> None:
"""Trigger camera to take a snapshot."""
self._camera.snap_picture()
self.data.refresh()
try:
await self._camera.snap_picture()
self.async_schedule_update_ha_state(force_refresh=True)
except asyncio.TimeoutError:
pass
def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera."""
try:
return self._camera.image_from_cache.content
return self._camera.image_from_cache
except ChunkedEncodingError:
_LOGGER.debug("Could not retrieve image for %s", self._camera.name)
return None

View file

@ -16,10 +16,11 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL,
CONF_USERNAME,
)
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import selector
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowFormStep,
SchemaOptionsFlowHandler,
@ -49,23 +50,23 @@ OPTIONS_FLOW = {
}
def validate_input(auth: Auth) -> None:
async def validate_input(auth: Auth) -> None:
"""Validate the user input allows us to connect."""
try:
auth.startup()
await auth.startup()
except (LoginError, TokenRefreshFailed) as err:
raise InvalidAuth from err
if auth.check_key_required():
raise Require2FA
def _send_blink_2fa_pin(auth: Auth, pin: str | None) -> bool:
async def _send_blink_2fa_pin(hass: HomeAssistant, auth: Auth, pin: str) -> bool:
"""Send 2FA pin to blink servers."""
blink = Blink()
blink = Blink(session=async_get_clientsession(hass))
blink.auth = auth
blink.setup_login_ids()
blink.setup_urls()
return auth.send_auth_key(blink, pin)
return await auth.send_auth_key(blink, pin)
class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
@ -91,11 +92,15 @@ class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a flow initiated by the user."""
errors = {}
if user_input is not None:
self.auth = Auth({**user_input, "device_id": DEVICE_ID}, no_prompt=True)
self.auth = Auth(
{**user_input, "device_id": DEVICE_ID},
no_prompt=True,
session=async_get_clientsession(self.hass),
)
await self.async_set_unique_id(user_input[CONF_USERNAME])
try:
await self.hass.async_add_executor_job(validate_input, self.auth)
await validate_input(self.auth)
return self._async_finish_flow()
except Require2FA:
return await self.async_step_2fa()
@ -122,12 +127,9 @@ class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle 2FA step."""
errors = {}
if user_input is not None:
pin: str | None = user_input.get(CONF_PIN)
pin: str = str(user_input.get(CONF_PIN))
try:
assert self.auth
valid_token = await self.hass.async_add_executor_job(
_send_blink_2fa_pin, self.auth, pin
)
valid_token = await _send_blink_2fa_pin(self.hass, self.auth, pin)
except BlinkSetupError:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except

View file

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/blink",
"iot_class": "cloud_polling",
"loggers": ["blinkpy"],
"requirements": ["blinkpy==0.21.0"]
"requirements": ["blinkpy==0.22.2"]
}

View file

@ -1,12 +1,15 @@
"""Support for Blink system camera sensors."""
from __future__ import annotations
from datetime import date, datetime
from decimal import Decimal
import logging
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@ -17,6 +20,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import DEFAULT_BRAND, DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH
@ -28,6 +32,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_WIFI_STRENGTH,
@ -35,6 +40,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
),
)
@ -50,7 +56,7 @@ async def async_setup_entry(
for description in SENSOR_TYPES
]
async_add_entities(entities)
async_add_entities(entities, update_before_add=True)
class BlinkSensor(SensorEntity):
@ -76,10 +82,11 @@ class BlinkSensor(SensorEntity):
model=self._camera.camera_type,
)
def update(self) -> None:
@property
def native_value(self) -> StateType | date | datetime | Decimal:
"""Retrieve sensor data from the camera."""
try:
self._attr_native_value = self._camera.attributes[self._sensor_key]
native_value = self._camera.attributes[self._sensor_key]
_LOGGER.debug(
"'%s' %s = %s",
self._camera.attributes["name"],
@ -87,7 +94,8 @@ class BlinkSensor(SensorEntity):
self._attr_native_value,
)
except KeyError:
self._attr_native_value = None
native_value = None
_LOGGER.error(
"%s not a valid camera attribute. Did the API change?", self._sensor_key
)
return native_value

View file

@ -539,7 +539,7 @@ bleak==0.21.1
blebox-uniapi==2.2.0
# homeassistant.components.blink
blinkpy==0.21.0
blinkpy==0.22.2
# homeassistant.components.bitcoin
blockchain==1.4.4

View file

@ -460,7 +460,7 @@ bleak==0.21.1
blebox-uniapi==2.2.0
# homeassistant.components.blink
blinkpy==0.21.0
blinkpy==0.22.2
# homeassistant.components.bluemaestro
bluemaestro-ble==0.2.3

View file

@ -1,5 +1,5 @@
"""Test the Blink config flow."""
from unittest.mock import Mock, patch
from unittest.mock import AsyncMock, Mock, patch
from blinkpy.auth import LoginError
from blinkpy.blinkpy import BlinkSetupError
@ -268,10 +268,10 @@ async def test_options_flow(hass: HomeAssistant) -> None:
)
config_entry.add_to_hass(hass)
mock_auth = Mock(
mock_auth = AsyncMock(
startup=Mock(return_value=True), check_key_required=Mock(return_value=False)
)
mock_blink = Mock()
mock_blink = AsyncMock()
with patch("homeassistant.components.blink.Auth", return_value=mock_auth), patch(
"homeassistant.components.blink.Blink", return_value=mock_blink
@ -293,7 +293,6 @@ async def test_options_flow(hass: HomeAssistant) -> None:
result["flow_id"],
user_input={"scan_interval": 5},
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"] == {"scan_interval": 5}
await hass.async_block_till_done()