Skip to content
This repository has been archived by the owner on Jul 30, 2021. It is now read-only.

Commit

Permalink
v 0.0.6 (#37)
Browse files Browse the repository at this point in the history
* Added Sphinx documentation (#23)

* Added Sphinx documentation

* Exclude flake8 from docs directory

* Added developer documentation link to README.rst

* Added support for getting the mode and also battery_level from device (#22)

* Added code to create an event stream and get notifications from arlo device. This can be used to get mode, battery level of camera and even all other properties of camera. Created properties to pull the base station properties, camera properties, schedule, rules etc. Also added property to get the battery level of cameras as well as filled the mode getter function to return the mode.

* Added code to create an event stream and get notifications from arlo device. This can be used to get mode, battery level of camera and even all other properties of camera. Created properties to pull the base station properties, camera properties, schedule, rules etc. Also added property to get the battery level of cameras as well as filled the mode getter function to return the mode.

* Updating the requirements.txt to add sseclient

* Updating the requirements.txt to add sseclient-py

* Updating the setup.py to add sseclient-py

* Addressing the review comments by @tchellomello

* Addressing the comments posted by CI bot

* Added code to get the per camera battery level. Added a new property called get_battery_level for camera. It will return the battery level of that camera alone.

* Fixing the review comment by CI bot. Also updated the README file with the commands to get properties, battery level, mode etc

* Added new properties for base station and camera. Added property in base station to fetch the schedule and a boolean property to find out if motion detection is enabled or not. Added properties in camera to get flip, mirror, power save mode, connection status, signal strength etc.

* Added property in camera to fetch the motion detection sensitivity

* Added property in camera to fetch the motion detection sensitivity : fixed the lint errors.

* Updated README with new properties

* Bump version 0.0.5

* Bump dev version 0.0.6

* Fix basestation mode property for custom mode settings. (#28)

* Fix basestation mode property for custom mode settings.
Bump version.

* Correct pylint issue... extra else.

* Address comments.

* Address pylint issue. Seems linelength restriction, impairs clarity here.

* Address pylint issue with linelength.

* Unittests (#26)

* Initiated skeleton for unittests

* Added Python 3.4 on travis

* Fixed coverage link

* Expanding unit test coverage (#29)

* Added tests for pyarlo.utils

* Added tests for http_stream on pyarlo.utils

* Added tests for pyarlo.camera

* Fixed mock_open() function (#30)

* Fixed mock_open() function

* Remove any temporary file created by unit tests

* Optimization: Instead of subscribing for each query, user can subscribe once and then post query (#32)

* Optimization: Instead of subscribing, starting a new thread and then posting query for getting info from base for each and every query, user can subscribe once in the beginning and then just ask the query, get response.

* Changed a info LOGGER into debug LOGGER. This was spamming the HASS output with query request/response

* Remove raise_for_status() since it can create problems with async functions

* Arlo Optimizations & New property to get camera signal strength (#34)

* Optimizations for improving the speed/time of subscribing and waiting for event. Earlier it used to be 5 seconds for each subscribe and getting event. This new model allows client applications using this library to just subscribe first, and then just query for information. This saves time in getting the info needed to barely 0.7 seconds. Removed the while loop to wait for an event, and using the threading event concept. Also added a new property to get the signal strength as a dictionary.

* Updated the code to use the get function get the element instead of directly accessing it by its key. This will protect from exception if that key is not present for some reason.

* Use the get function to get the element instead of directly accessing. This will take care of any exceptions thus raised in case that element is not in the dict

* Added a check for None in case we didn't get the modes response within our timeout period.

* Operator == always will return bool

* Make lint happy
  • Loading branch information
tchellomello authored Jul 20, 2017
1 parent 563c4b1 commit 2ed5629
Show file tree
Hide file tree
Showing 18 changed files with 547 additions and 47 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ matrix:
include:
- python: "2.7"
env: TOXENV=py27
- python: "3.4"
env: TOXENV=py34
- python: "3.5"
env: TOXENV=py35
- python: "3.6"
Expand Down
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Python Arlo
.. image:: https://travis-ci.org/tchellomello/python-arlo.svg?branch=master
:target: https://travis-ci.org/tchellomello/python-arlo

.. image:: https://coveralls.io/repos/github/tchellomello/python-arlo/badge.svg
:target: https://coveralls.io/github/tchellomello/python-arlo
.. image:: https://coveralls.io/repos/github/tchellomello/python-arlo/badge.svg?branch=master
:target: https://coveralls.io/github/tchellomello/python-arlo?branch=master

.. image:: https://img.shields.io/pypi/pyversions/pyarlo.svg
:target: https://pypi.python.org/pypi/pyarlo
Expand Down
5 changes: 2 additions & 3 deletions pyarlo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ def query(self,
elif method == 'POST':
req = self.session.post(url, json=params, headers=headers)

req.raise_for_status()
if req.status_code == 200:
if raw:
_LOGGER.debug("Required raw object.")
Expand All @@ -154,12 +153,12 @@ def query(self,
@property
def cameras(self):
"""Return all cameras linked on Arlo account."""
return self.devices['cameras']
return self.devices.get('cameras')

@property
def base_stations(self):
"""Return all base stations linked on Arlo account."""
return self.devices['base_station']
return self.devices.get('base_station')

@property
def devices(self):
Expand Down
91 changes: 61 additions & 30 deletions pyarlo/base_station.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"""Generic Python Class file for Netgear Arlo Base Station module."""
import json
import threading
import time
import logging
import sseclient
from pyarlo.const import (
Expand All @@ -29,18 +28,20 @@ def __init__(self, name, attrs, session_token, arlo_session):
self.__sseclient = None
self.__subscribed = False
self.__events = []
self.__event_handle = None

def __repr__(self):
"""Representation string of object."""
return "<{0}: {1}>".format(self.__class__.__name__, self.name)

def thread_function(self):
"""Thread function."""

self.__subscribed = True
url = SUBSCRIBE_ENDPOINT + "?token=" + self._session_token

data = self._session.query(url, method='GET', raw=True, stream=True)
self.__sseclient = sseclient.SSEClient(data)
if self.__sseclient:
self.__subscribed = True
for event in (self.__sseclient).events():
if not self.__subscribed:
break
Expand All @@ -56,9 +57,11 @@ def thread_function(self):
break
elif action == "is" and "subscriptions/" not in resource:
self.__events.append(data)
self.__event_handle.set()

def _get_event_stream(self):
"""Spawn a thread and monitor the Arlo Event Stream."""
self.__event_handle = threading.Event()
event_thread = threading.Thread(target=self.thread_function)
event_thread.start()

Expand All @@ -78,37 +81,43 @@ def _unsubscribe_myself(self):
def _close_event_stream(self):
"""Stop the Event stream thread."""
self.__subscribed = False
del self.__events[:]
self.__event_handle.clear()

def publish_and_get_event(self, resource):
"""Publish and get the event from base station."""
self._get_event_stream()
self._subscribe_myself()
l_subscribed = False
this_event = None

if not self.__subscribed:
self._get_event_stream()
self._subscribe_myself()
l_subscribed = True

this_event = ''
status = self.__run_action(
method='GET',
resource=resource,
mode=None,
publish_response=False)
if status == 'success':
for i in range(0, 5):
_LOGGER.debug("Trying instance " + str(i))
time.sleep(1)
i = 0
while not this_event and i < 2:
self.__event_handle.wait(5.0)
self.__event_handle.clear()
_LOGGER.debug("Instance " + str(i) + " resource: " + resource)
for event in self.__events:
if event['resource'] == resource:
this_event = event
self.__events.remove(event)
break
if this_event:
break
i = i + 1

self._unsubscribe_myself()
self._close_event_stream()
if l_subscribed:
self._unsubscribe_myself()
self._close_event_stream()
l_subscribed = False

if this_event:
return this_event

return None
return this_event

def __run_action(
self,
Expand Down Expand Up @@ -158,7 +167,7 @@ def __run_action(
body['to'] = self.device_id
body['transId'] = "web!e6d1b969.8aa4b!1498165992111"

_LOGGER.info("Action body: %s", body)
_LOGGER.debug("Action body: %s", body)

ret = \
self._session.query(url, method='POST', extra_params=body,
Expand Down Expand Up @@ -235,12 +244,16 @@ def mode(self):
resource = "modes"
mode_event = self.publish_and_get_event(resource)
if mode_event:
active_mode = mode_event['properties']['active']
modes = mode_event['properties']['modes']
for mode in modes:
if mode['id'] == active_mode:
return mode['type']
properties = mode_event.get('properties')
active_mode = properties.get('active')
modes = properties.get('modes')
if not modes:
return None

for mode in modes:
if mode.get('id') == active_mode:
return mode.get('type') \
if mode.get('type') is not None else mode.get('name')
return None

@property
Expand All @@ -249,7 +262,7 @@ def get_camera_properties(self):
resource = "cameras"
resource_event = self.publish_and_get_event(resource)
if resource_event:
return resource_event['properties']
return resource_event.get('properties')

return None

Expand All @@ -260,20 +273,38 @@ def get_camera_battery_level(self):
resource = "cameras"
resource_event = self.publish_and_get_event(resource)
if resource_event:
cameras = resource_event['properties']
cameras = resource_event.get('properties')
for camera in cameras:
battery_levels[camera['serialNumber']] = camera['batteryLevel']
serialnum = camera.get('serialNumber')
cam_battery = camera.get('batteryLevel')
battery_levels[serialnum] = cam_battery
return battery_levels

return None

@property
def get_camera_signal_strength(self):
"""Return a list of signal strength of all cameras."""
signal_strength = {}
resource = "cameras"
resource_event = self.publish_and_get_event(resource)
if resource_event:
cameras = resource_event.get('properties')
for camera in cameras:
serialnum = camera.get('serialNumber')
cam_strength = camera.get('signalStrength')
signal_strength[serialnum] = cam_strength
return signal_strength

return None

@property
def get_basestation_properties(self):
"""Return the base station info."""
resource = "basestation"
basestn_event = self.publish_and_get_event(resource)
if basestn_event:
return basestn_event['properties']
return basestn_event.get('properties')

return None

Expand All @@ -283,7 +314,7 @@ def get_camera_rules(self):
resource = "rules"
rules_event = self.publish_and_get_event(resource)
if rules_event:
return rules_event['properties']
return rules_event.get('properties')

return None

Expand All @@ -293,14 +324,14 @@ def get_camera_schedule(self):
resource = "schedule"
schedule_event = self.publish_and_get_event(resource)
if schedule_event:
return schedule_event['properties']
return schedule_event.get('properties')

return None

@property
def is_motion_detection_enabled(self):
"""Return Boolean if motion is enabled."""
return bool(self.mode == "armed" or self.get_camera_schedule['active'])
return self.mode == "armed"

@property
def subscribe(self):
Expand Down
2 changes: 1 addition & 1 deletion pyarlo/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def load(self, days=PRELOAD_DAYS, only_cameras=None,
# make sure only_cameras is a list
if only_cameras and \
not isinstance(only_cameras, list):
only_cameras = list(only_cameras)
only_cameras = [(only_cameras)]

# filter by camera only
if only_cameras:
Expand Down
1 change: 1 addition & 0 deletions requirements_tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ flake8
mock
pylint
pytest
requests_mock
tox
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
setup(
name='pyarlo',
packages=['pyarlo'],
version='0.0.5',
version='0.0.6',
description='Python Arlo is a library written in Python 2.7/3x ' +
'that exposes the Netgear Arlo cameras as Python objects.',
author='Marcelo Moreira de Mello',
Expand Down
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for PyArlo components."""
9 changes: 9 additions & 0 deletions tests/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Helper methods for tests."""
import os


def load_fixture(filename):
"""Load a fixture."""
path = os.path.join(os.path.dirname(__file__), 'fixtures', filename)
with open(path) as fdp:
return fdp.read()
12 changes: 12 additions & 0 deletions tests/fixtures/pyarlo_authentication.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{"data": {"accountStatus": "registered",
"arlo": true,
"authenticated": 1498801924,
"countryCode": "US",
"dateCreated": 1456549914113,
"email": "[email protected]",
"policyUpdate": false,
"tocUpdate": false,
"token": "999999999999",
"userId": "999-123456",
"validEmail": true},
"success": true}
74 changes: 74 additions & 0 deletions tests/fixtures/pyarlo_devices.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{"data": [
{"deviceId": "48B14CAAAAAAA",
"deviceName": "Front Door",
"deviceType": "camera",
"displayOrder": 1,
"interfaceSchemaVer": "2",
"interfaceVersion": "i000",
"lastImageUploaded": "false",
"lastModified": 1498804517542,
"mediaObjectCount": 39,
"modelId": "VMC3030",
"owner": {"firstName": "Foo ",
"lastName": "Bar",
"ownerId": "999-123456"},
"parentId": "48B14CBBBBBBB",
"presignedFullFrameSnapshotUrl": "https://arlos3-prod-z2.s3.amazonaws.com/fullFrameSnapshot.jpg",
"presignedLastImageUrl": "https://arlolastimage-z2.s3.amazonaws.com/lastImage.jpg",
"presignedSnapshotUrl": "https://arlos3-prod-z2.s3.amazonaws.com/snapshot.jpg",
"properties": {"hwVersion": "H7",
"modelId": "VMC3030",
"olsonTimeZone": "America/New_York"},
"state": "provisioned",
"uniqueId": "235-48B14CAAAAAAA",
"userId": "999-123456",
"userRole": "ADMIN",
"xCloudId": "1005-123-999999"},

{"deviceId": "48B14C1299999",
"deviceName": "Patio",
"deviceType": "camera",
"displayOrder": 0,
"interfaceSchemaVer": "2",
"interfaceVersion": "i000",
"lastImageUploaded": "false",
"lastModified": 1498804517542,
"mediaObjectCount": 233,
"modelId": "VMC3030",
"owner": {"firstName": "Foo ",
"lastName": "Bar",
"ownerId": "999-123456"},
"parentId": "48B14CBBBBBBB",
"presignedFullFrameSnapshotUrl": "https://arlos3-prod-z2.s3.amazonaws.com/fullFrameSnapshot.jpg",
"presignedLastImageUrl": "https://arlolastimage-z2.s3.amazonaws.com/lastImage.jpg",
"presignedSnapshotUrl": "https://arlos3-prod-z2.s3.amazonaws.com/snapshot.jpg",
"properties": {"hwVersion": "H7",
"modelId": "VMC3030",
"olsonTimeZone": "America/New_York"},
"state": "provisioned",
"uniqueId": "999-123456_48B14C1299999",
"userId": "999-123456",
"userRole": "ADMIN",
"xCloudId": "1005-123-999999"},

{"deviceId": "48B14CBBBBBBB",
"deviceName": "Arlo Station",
"deviceType": "basestation",
"displayOrder": 2,
"interfaceSchemaVer": "1",
"interfaceVersion": "i000",
"lastModified": 1498804517553,
"mediaObjectCount": 0,
"modelId": "VMB3010",
"owner": {"firstName": "Foo ",
"lastName": "Bar",
"ownerId": "999-123456"},
"properties": {"hwVersion": "VMB3010r2",
"modelId": null,
"olsonTimeZone": "America/New_York"},
"state": "provisioned",
"uniqueId": "235-48B14CBBBBBBB",
"userId": "999-123456",
"userRole": "ADMIN",
"xCloudId": "1005-123-999999"}],
"success": false}
Loading

0 comments on commit 2ed5629

Please sign in to comment.