First cut at blink mini support

This commit is contained in:
Kevin Fronczak
2020-06-09 04:04:07 +00:00
parent 31de65f155
commit 2788ab58ea
4 changed files with 181 additions and 24 deletions
+18 -1
View File
@@ -23,7 +23,7 @@ from dateutil.parser import parse
from slugify import slugify
from blinkpy import api
from blinkpy.sync_module import BlinkSyncModule
from blinkpy.sync_module import BlinkSyncModule, BlinkOwl
from blinkpy.helpers import util
from blinkpy.helpers.constants import (
DEFAULT_MOTION_INTERVAL,
@@ -68,6 +68,7 @@ class Blink:
self.version = __version__
self.available = False
self.key_required = False
self.homescreen = {}
@util.Throttle(seconds=MIN_THROTTLE_TIME)
def refresh(self, force=False):
@@ -129,6 +130,8 @@ class Blink:
self.setup_sync_module(name, network_id, sync_cameras)
self.cameras = self.merge_cameras()
self.setup_owls()
self.available = True
self.key_required = False
return True
@@ -138,6 +141,20 @@ class Blink:
self.sync[name] = BlinkSyncModule(self, name, network_id, cameras)
self.sync[name].start()
def setup_owls(self):
"""Check for mini cameras."""
response = api.request_homescreen(self)
self.homescreen = response
try:
for owl in response["owls"]:
name = owl["name"]
network_id = owl["network_id"]
if owl["onboarded"]:
self.sync[name] = BlinkOwl(self, name, network_id, owl)
except KeyError:
# No sync-less devices found
pass
def setup_camera_list(self):
"""Create camera list for onboarded networks."""
all_cameras = {}
+22 -4
View File
@@ -133,9 +133,10 @@ class BlinkCamera:
self.temperature_calibrated = self.temperature
_LOGGER.warning("Could not retrieve calibrated temperature.")
# Check if thumbnail exists in config, if not try to
# get it from the homescreen info in the sync module
# otherwise set it to None and log an error
self.update_images(config, force_cache=force_cache)
def update_images(self, config, force_cache=False):
"""Update images for camera."""
new_thumbnail = None
thumb_addr = None
if config["thumbnail"]:
@@ -202,7 +203,8 @@ class BlinkCamera:
)
def video_to_file(self, path):
"""Write video to file.
"""
Write video to file.
:param path: Path to write file
"""
@@ -213,3 +215,19 @@ class BlinkCamera:
return
with open(path, "wb") as vidfile:
copyfileobj(response.raw, vidfile)
class BlinkCameraMini(BlinkCamera):
"""Define a class for a Blink Mini camera."""
def update(self, config, force_cache=False, **kwargs):
"""Update a blink mini camera."""
self.name = config["name"]
self.camera_id = str(config["id"])
self.network_id = str(config["network_id"])
self.serial = config["serial"]
if not self.serial:
self.serial = f"{self.network_id}-{self.camera_id}"
self.motion_enabled = config["enabled"]
self.update_images(config, force_cache=force_cache)
+108 -19
View File
@@ -4,7 +4,7 @@ import logging
from requests.structures import CaseInsensitiveDict
from blinkpy import api
from blinkpy.camera import BlinkCamera
from blinkpy.camera import BlinkCamera, BlinkCameraMini
from blinkpy.helpers.util import time_to_seconds
from blinkpy.helpers.constants import ONLINE
@@ -85,16 +85,8 @@ class BlinkSyncModule:
def start(self):
"""Initialize the system."""
response = api.request_syncmodule(self.blink, self.network_id)
try:
self.summary = response["syncmodule"]
self.network_id = self.summary["network_id"]
except (TypeError, KeyError):
_LOGGER.error(
("Could not retrieve sync module information " "with response: %s"),
response,
exc_info=True,
)
response = self.sync_initialize()
if not response:
return False
try:
@@ -108,24 +100,39 @@ class BlinkSyncModule:
is_ok = self.get_network_info()
self.check_new_videos()
if not is_ok or not self.update_cameras():
return False
self.available = True
return True
def sync_initialize(self):
"""Initialize a sync module."""
response = api.request_syncmodule(self.blink, self.network_id)
try:
self.summary = response["syncmodule"]
self.network_id = self.summary["network_id"]
except (TypeError, KeyError):
_LOGGER.error(
"Could not retrieve sync module information with response: %s", response
)
return False
return response
def update_cameras(self, camera_type=BlinkCamera):
"""Update cameras from server."""
try:
for camera_config in self.camera_list:
if "name" not in camera_config:
break
name = camera_config["name"]
self.cameras[name] = BlinkCamera(self)
self.cameras[name] = camera_type(self)
self.motion[name] = False
camera_info = self.get_camera_info(camera_config["id"])
self.cameras[name].update(camera_info, force_cache=True, force=True)
except KeyError:
_LOGGER.error(
"Could not create cameras instances for %s", self.name, exc_info=True
)
_LOGGER.error("Could not create camera instances for %s", self.name)
return False
if not is_ok:
return False
self.available = True
return True
def get_events(self, **kwargs):
@@ -205,3 +212,85 @@ class BlinkSyncModule:
def check_new_video_time(self, timestamp):
"""Check if video has timestamp since last refresh."""
return time_to_seconds(timestamp) > self.blink.last_refresh
class BlinkOwl(BlinkSyncModule):
"""Representation of a sync-less device."""
def __init__(self, blink, name, network_id, response):
"""Initialize a sync-less object."""
self._response = response
cameras = [{network_id: {"name": name, "id": response["id"]}}]
super().__init__(blink, name, network_id, cameras)
self.sync_id = response["id"]
self.serial = response["serial"]
if not self.serial:
self.serial = f"{network_id}-{self.sync_id}"
def sync_initialize(self):
"""Initialize a sync-less module."""
self.summary = {
"id": self.sync_id,
"name": self.name,
"serial": self.serial,
"status": self.status,
"onboarded": True,
"account_id": self.blink.account_id,
"network_id": self.network_id,
}
return self.summary
def update_cameras(self, camera_type=BlinkCameraMini):
"""Update sync-less cameras."""
super().update_cameras(camera_type=camera_type)
def get_camera_info(self, camera_id):
"""Retrieve camera information."""
try:
for owl in self.blink.homescreen["owls"]:
if owl["name"] == self.name:
return owl
except KeyError:
pass
return None
def get_network_info(self):
"""Get network info for sync-less module."""
return True
@property
def network_info(self):
"""Format owl response to resemble sync module."""
return {
"network": {
"id": self.network_id,
"name": self.name,
"armed": self._response["enabled"],
"sync_module_error": False,
"account_id": self.blink.account_id,
}
}
@network_info.setter
def network_info(self, value):
"""Set network_info property."""
@property
def arm(self):
"""Return arm status."""
try:
return self.network_info["network"]["armed"]
except (KeyError, TypeError):
self.available = False
return None
@arm.setter
def arm(self, value):
"""Arm or disarm camera."""
if value:
return api.request_motion_detection_enable(
self.blink, self.network_id, self.sync_id
)
return api.request_motion_detection_disable(
self.blink, self.network_id, self.sync_id
)
+33
View File
@@ -9,6 +9,7 @@ any communication related errors at startup.
import unittest
from unittest import mock
from blinkpy.blinkpy import Blink, BlinkSetupError
from blinkpy.sync_module import BlinkOwl
from blinkpy.helpers.constants import __version__
@@ -192,6 +193,38 @@ class TestBlinkSetup(unittest.TestCase):
self.assertEqual(combined["fizz"], "buzz")
self.assertEqual(combined["bar"], "foo")
@mock.patch("blinkpy.api.request_homescreen")
def test_initialize_blink_minis(self, mock_home):
"""Test blink mini initialization."""
mock_home.return_value = {
"owls": [
{
"enabled": False,
"id": 1,
"name": "foo",
"network_id": 2,
"onboarded": True,
"status": "online",
"thumbnail": "/foo/bar",
"serial": "",
},
{
"enabled": True,
"id": 3,
"name": "bar",
"network_id": 4,
"onboarded": True,
"status": "online",
"thumbnail": "/foo/bar",
"serial": "",
},
]
}
self.blink.sync = {}
self.blink.setup_owls()
self.assertEqual(self.blink.sync["foo"].__class__, BlinkOwl)
self.assertEqual(self.blink.sync["bar"].__class__, BlinkOwl)
class MockSync:
"""Mock sync module class."""