First cut at blink mini support
This commit is contained in:
+18
-1
@@ -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
@@ -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
@@ -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
|
||||
)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user