Skip to content

Commit

Permalink
Renamed model_name to model_type, added model type based feature …
Browse files Browse the repository at this point in the history
…detection to simplify HASS integration.
  • Loading branch information
evanjd committed Sep 1, 2018
1 parent faf8b6f commit d4ce119
Show file tree
Hide file tree
Showing 7 changed files with 59 additions and 37 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
[![Coverage Status][coverage-badge]][coverage-url]
[![Open Issues][open-issues-badge]][open-issues-url]


This library exposes the [Logi Circle](https://www.logitech.com/en-us/product/circle-2-home-security-camera) family of cameras as Python objects. The goal is to expose most of the functionality from Logi's 1st party applications, allowing integration of those features into other projects.

Note that the API this project is based on is not open, and therefore could change/break at any time.
Expand Down Expand Up @@ -50,6 +49,7 @@ $ pip install \
- Battery %
- Charging status
- Model
- Model type (eg. 1st gen, 2nd gen wired, etc)
- Connected Wifi SSID
- Signal strength %
- IP address
Expand All @@ -74,8 +74,6 @@ $ pip install \

## Usage example

As this project is still in its early days, expect breaking changes!

#### Setup and authenticate:

```python
Expand Down Expand Up @@ -189,6 +187,7 @@ async def play_with_props():
print('%s: %s%% battery remaining' %
(camera.name, camera.battery_level))
print('%s: Model number is %s' % (camera.name, camera.model))
print('%s: Model type is %s' % (camera.name, camera.model_type))
print('%s: Signal strength is %s%% (%s)' % (
camera.name, camera.signal_strength_percentage, camera.signal_strength_category))
print('%s: last activity was at %s and lasted for %s seconds.' % (
Expand Down Expand Up @@ -236,6 +235,9 @@ loop.close()
- Removed `is_streaming` property as I've discovered this is not a binary sensor for 2nd gen cameras. Replaced with `streaming_mode`.
- `set_streaming_mode` now accepts a string instead a boolean.
- Added `model_name` property.
- 0.1.3
- Renamed `model_name` to `model_type` to better reflect what the property reports.
- Added rudimentary feature detection, exposed via `supported_features` and `supports_feature` methods and derived from model type.

## Meta

Expand All @@ -250,7 +252,7 @@ Distributed under the MIT license. See `LICENSE` for more information.

## Contributing

They're very welcome, every little bit helps! I'm especially keen for help supporting devices that I do not own and cannot test with (eg. Circle 2 indoor & outdoor cameras).
They're very welcome, every little bit helps! I'm especially keen for help supporting devices that I do not own and cannot test with (eg. Circle 2 wired and wireless cameras).

1. Fork it (<https://github.com/evanjd/python-logi-circle/fork>).
2. Create your feature branch (`git checkout -b feature/fooBar`).
Expand Down
20 changes: 14 additions & 6 deletions logi_circle/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import pytz
from aiohttp.client_exceptions import ClientResponseError
from .const import (
PROTOCOL, ACCESSORIES_ENDPOINT, ACTIVITIES_ENDPOINT, IMAGES_ENDPOINT, JPEG_CONTENT_TYPE)
PROTOCOL, ACCESSORIES_ENDPOINT, ACTIVITIES_ENDPOINT, IMAGES_ENDPOINT, JPEG_CONTENT_TYPE, FEATURES)
from .activity import Activity
from .live_stream import LiveStream
from .utils import _stream_to_file, _model_number_to_name
from .utils import _stream_to_file, _model_number_to_type
from .exception import UnexpectedContentType

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -83,6 +83,14 @@ async def update(self):

self._set_attributes(camera)

def supported_features(self):
"""Returns an array of supported sensors for this camera."""
return FEATURES[self.model_type]

def supports_feature(self, feature):
"""Returns a bool indicating whether a given sensor is implemented for this camera."""
return True if feature in self.supported_features() else False

async def query_activity_history(self, property_filter=None, date_filter=None, date_operator='<=', limit=100):
"""Filter the activity history, returning Activity objects for any matching result."""

Expand Down Expand Up @@ -289,12 +297,12 @@ def model(self):
return self._attrs.get('model')

@property
def model_name(self):
"""Return product name, derived from other camera properties."""
def model_type(self):
"""Return product type, derived from other camera properties."""
if isinstance(self.battery_level, int):
return _model_number_to_name(self.model, self.battery_level)
return _model_number_to_type(self.model, self.battery_level)
else:
return _model_number_to_name(self.model)
return _model_number_to_type(self.model)

@property
def firmware(self):
Expand Down
18 changes: 14 additions & 4 deletions logi_circle/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,17 @@
# Model to product mapping
MODEL_GEN_1 = 'A1533'
MODEL_GEN_2 = 'V-R0008'
MODEL_NAME_GEN_1 = 'Logi Circle 1st generation'
MODEL_NAME_GEN_2_WIRED = 'Logi Circle 2nd generation - wired'
MODEL_NAME_GEN_2_WIRELESS = 'Logi Circle 2nd generation - wireless'
MODEL_NAME_UNKNOWN = 'Unknown'
MODEL_TYPE_GEN_1 = '1st gen'
MODEL_TYPE_GEN_2_WIRED = '2nd gen - wired'
MODEL_TYPE_GEN_2_WIRELESS = '2nd gen - wireless'
MODEL_TYPE_UNKNOWN = 'Unknown'

# Feature mapping
FEATURES = {
MODEL_TYPE_GEN_1: ['is_charging', 'battery_level', 'last_activity_time', 'privacy_mode',
'signal_strength_percentage', 'signal_strength_category'],
MODEL_TYPE_GEN_2_WIRED: ['privacy_mode', 'signal_strength_percentage', 'signal_strength_category'],
MODEL_TYPE_GEN_2_WIRELESS: ['is_charging', 'battery_level', 'last_activity_time', 'privacy_mode',
'signal_strength_percentage', 'signal_strength_category'],
MODEL_TYPE_UNKNOWN: ['last_activity_time']
}
19 changes: 10 additions & 9 deletions logi_circle/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
except ImportError:
import pickle
from logi_circle.const import (
CACHE_ATTRS, COOKIE_NAME, MODEL_GEN_1, MODEL_GEN_2, MODEL_NAME_GEN_1, MODEL_NAME_GEN_2_WIRED, MODEL_NAME_GEN_2_WIRELESS, MODEL_NAME_UNKNOWN)
CACHE_ATTRS, COOKIE_NAME, MODEL_GEN_1, MODEL_GEN_2, MODEL_TYPE_GEN_1,
MODEL_TYPE_GEN_2_WIRED, MODEL_TYPE_GEN_2_WIRELESS, MODEL_TYPE_UNKNOWN)
from .exception import BadSession

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -48,15 +49,15 @@ async def _stream_to_file(stream, filename, open_mode='wb'):
file_handle.write(chunk)


def _model_number_to_name(model, battery_level=-1):
def _model_number_to_type(model, battery_level=-1):
"""Converts the model number to a friendly product name."""
if (model == MODEL_GEN_1):
return MODEL_NAME_GEN_1
if (model == MODEL_GEN_2):
if (battery_level < 0):
return MODEL_NAME_GEN_2_WIRED
return MODEL_NAME_GEN_2_WIRELESS
return MODEL_NAME_UNKNOWN
if model == MODEL_GEN_1:
return MODEL_TYPE_GEN_1
if model == MODEL_GEN_2:
if battery_level < 0:
return MODEL_TYPE_GEN_2_WIRED
return MODEL_TYPE_GEN_2_WIRELESS
return MODEL_TYPE_UNKNOWN


def _write_to_file(data, filename):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def readme():
setup(
name='logi_circle',
packages=['logi_circle'],
version='0.1.2',
version='0.1.3',
description='A Python library to communicate with Logi Circle cameras',
long_description=readme(),
author='Evan Bruhn',
Expand Down
2 changes: 1 addition & 1 deletion tests/test_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async def run_test():
self.assertEqual(
camera.model, 'A1533', 'Model mismatch')
self.assertEqual(
camera.model_name, 'Logi Circle 1st generation', 'Model name mismatch')
camera.model_type, '1st gen', 'Model name mismatch')
self.assertEqual(
camera.node_id, 'node-mocked.video.logi.com', 'Node ID mismatch')
self.assertEqual(
Expand Down
25 changes: 13 additions & 12 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import unittest
import asyncio
from logi_circle.utils import (
_get_session_cookie, _handle_response, _model_number_to_name, _clean_cache, _exists_cache, _save_cache, _read_cache)
from logi_circle.const import CACHE_ATTRS, COOKIE_NAME, MODEL_GEN_1, MODEL_GEN_2, MODEL_NAME_GEN_1, MODEL_NAME_GEN_2_WIRED, MODEL_NAME_GEN_2_WIRELESS, MODEL_NAME_UNKNOWN
_get_session_cookie, _handle_response, _model_number_to_type, _clean_cache, _exists_cache, _save_cache, _read_cache)
from logi_circle.const import (CACHE_ATTRS, COOKIE_NAME, MODEL_GEN_1, MODEL_GEN_2, MODEL_TYPE_GEN_1,
MODEL_TYPE_GEN_2_WIRED, MODEL_TYPE_GEN_2_WIRELESS, MODEL_TYPE_UNKNOWN)
from logi_circle.exception import BadSession

CACHE = 'tests/cache.db'
Expand Down Expand Up @@ -89,16 +90,16 @@ async def run_test():
loop.run_until_complete(run_test())
loop.close()

def test_model_number_to_name(self):
"""Test _model_number_to_name method."""
self.assertEqual(_model_number_to_name(MODEL_GEN_1), MODEL_NAME_GEN_1)
self.assertEqual(_model_number_to_name(MODEL_GEN_2, 50),
MODEL_NAME_GEN_2_WIRELESS)
self.assertEqual(_model_number_to_name(
MODEL_GEN_2, -1), MODEL_NAME_GEN_2_WIRED)
self.assertEqual(_model_number_to_name(
MODEL_GEN_2), MODEL_NAME_GEN_2_WIRED)
self.assertEqual(_model_number_to_name('A1234'), MODEL_NAME_UNKNOWN)
def test_model_number_to_type(self):
"""Test _model_number_to_type method."""
self.assertEqual(_model_number_to_type(MODEL_GEN_1), MODEL_TYPE_GEN_1)
self.assertEqual(_model_number_to_type(MODEL_GEN_2, 50),
MODEL_TYPE_GEN_2_WIRELESS)
self.assertEqual(_model_number_to_type(
MODEL_GEN_2, -1), MODEL_TYPE_GEN_2_WIRED)
self.assertEqual(_model_number_to_type(
MODEL_GEN_2), MODEL_TYPE_GEN_2_WIRED)
self.assertEqual(_model_number_to_type('A1234'), MODEL_TYPE_UNKNOWN)

def test_init_clean_cache(self):
"""Test _clean_cache method."""
Expand Down

0 comments on commit d4ce119

Please sign in to comment.