Skip to content

Commit c062b72

Browse files
authored
Re-architect library (bachya#255)
* Re-architect library * Update docs
1 parent e36b406 commit c062b72

45 files changed

Lines changed: 1581 additions & 2487 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/api.rst‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,36 @@ API
1212
.. automodule:: simplipy.api
1313
:members:
1414

15-
Entities
15+
Devices
1616
--------
1717

18-
.. autoclass:: simplipy.entity.Entity
18+
.. autoclass:: simplipy.device.Device
1919
:members:
2020

21-
.. autoclass:: simplipy.entity.EntityTypes
21+
.. autoclass:: simplipy.device.DeviceTypes
2222
:members:
2323
:undoc-members:
2424

25-
.. autoclass:: simplipy.entity.EntityV3
25+
.. autoclass:: simplipy.device.DeviceV3
2626
:members:
2727

2828
Lock
2929
----
3030

31-
.. autoclass:: simplipy.lock.Lock
31+
.. autoclass:: simplipy.device.lock.Lock
3232
:members:
3333

34-
.. autoclass:: simplipy.lock.LockStates
34+
.. autoclass:: simplipy.device.lock.LockStates
3535
:members:
3636
:undoc-members:
3737

3838
Sensors
3939
-------
4040

41-
.. autoclass:: simplipy.sensor.v2.SensorV2
41+
.. autoclass:: simplipy.device.sensor.v2.SensorV2
4242
:members:
4343

44-
.. autoclass:: simplipy.sensor.v3.SensorV3
44+
.. autoclass:: simplipy.device.sensor.v3.SensorV3
4545
:members:
4646

4747
Systems

‎docs/sensor.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ All ``Sensor`` objects come with a standard set of properties
3838
3939
# Return the sensor's type:
4040
sensor.type
41-
# >>> simplipy.EntityTypes.glass_break
41+
# >>> simplipy.DeviceTypes.glass_break
4242
4343
# Return whether the sensor is in an error state:
4444
sensor.error

‎pyproject.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ classifiers = [
6060
]
6161

6262
[tool.poetry.dependencies]
63-
aiohttp = "^3.7.4"
63+
aiohttp = "^3.7.4.post0"
6464
backoff = "^1.11.1"
6565
python = "^3.7.0"
6666
pytz = ">=2019.3,<2022.0"
@@ -69,7 +69,7 @@ voluptuous = ">=0.11.7,<0.13.0"
6969

7070
[tool.poetry.dev-dependencies]
7171
Sphinx = "^4.0.0"
72-
aioresponses = "^0.7.2"
72+
aresponses = "^2.1.4"
7373
asynctest = "^0.13.0"
7474
pre-commit = "^2.0.1"
7575
pytest = "^6.0.0"

‎requirements_test.txt‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
aiohttp>=3.7.4
2-
aioresponses==0.7.2
1+
aiohttp==3.7.4.post0
2+
aresponses==2.1.4
33
asynctest==0.13.0
44
backoff==1.11.1
55
pytest-aiohttp==0.3.0
6-
pytest-cov==2.8.1
7-
pytest==5.4.1
8-
pytz==2019.3
9-
voluptuous>=0.11.7,<0.13.0
6+
pytest-cov==2.12.1
7+
pytest==6.2.5
8+
pytz==2021.1
9+
voluptuous==0.12.2

‎simplipy/__init__.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
"""Define module-level imports."""
2-
from .api import get_api # noqa
1+
"""Define the simplipy package."""
2+
from simplipy.api import get_api # noqa

‎simplipy/api.py‎

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Define a SimpliSafe account."""
1+
"""Define functionality for interacting with the SimpliSafe API."""
22
import base64
33
from json.decoder import JSONDecodeError
44
import sys
@@ -83,7 +83,7 @@ def __init__(
8383
# These will get filled in after initial authentication:
8484
self._access_token: Optional[str] = None
8585
self._refresh_token: Optional[str] = None
86-
self.subscription_data: Dict[int, dict] = {}
86+
self.subscription_data: Dict[int, Any] = {}
8787
self.user_id: Optional[int] = None
8888

8989
# Implement a version of the request coroutine, but with backoff/retry logic:
@@ -168,7 +168,7 @@ async def _request(
168168

169169
assert session
170170

171-
data: Dict[str, Any] = {}
171+
data: Union[Dict[str, Any], str] = {}
172172
async with session.request(
173173
method, f"{API_URL_BASE}/{endpoint}", **kwargs
174174
) as resp:
@@ -268,52 +268,49 @@ async def login(self) -> None:
268268
async def get_systems(self) -> Dict[int, Union[SystemV2, SystemV3]]:
269269
"""Get systems associated to the associated SimpliSafe account.
270270
271-
In the dict that is returned, the keys are the system ID and the values are
272-
actual ``System`` objects.
271+
In the dict that is returned, the keys are the subscription ID and the values
272+
are actual ``System`` objects.
273273
274274
:rtype: ``Dict[int, simplipy.system.System]``
275275
"""
276+
systems: Dict[int, Union[SystemV2, SystemV3]] = {}
277+
276278
await self.update_subscription_data()
277279

278-
systems = {}
280+
for sid, subscription in self.subscription_data.items():
281+
if not subscription["activated"] != 0:
282+
LOGGER.info("Skipping inactive subscription: %s", sid)
283+
continue
279284

280-
for system_id, subscription in self.subscription_data.items():
281-
version = subscription["location"]["system"]["version"]
285+
# if "system" not in subscription["location"]:
286+
if not subscription["location"].get("system"):
287+
LOGGER.error("Skipping subscription with missing system data: %s", sid)
288+
continue
282289

283290
system: Union[SystemV2, SystemV3]
291+
version = subscription["location"]["system"]["version"]
284292
if version == 2:
285-
system = SystemV2(self, system_id)
293+
system = SystemV2(self, sid)
286294
else:
287-
system = SystemV3(self, system_id)
295+
system = SystemV3(self, sid)
288296

289-
# Skip deactivated systems:
290-
if not system.active:
291-
LOGGER.info("Skipping deactivated system: %s", system_id)
292-
continue
293-
294-
# Update the system, but don't include system data itself, since it will
295-
# already have been fetched when the API was first queried:
296-
await system.update(include_system=False)
297-
await system.generate_entities()
298-
systems[system_id] = system
297+
# Update the system, but don't include subscription data itself, since it
298+
# will already have been fetched when the API was first queried:
299+
await system.update(include_subscription=False)
300+
system.generate_device_objects()
301+
systems[sid] = system
299302

300303
return systems
301304

302305
async def update_subscription_data(self) -> None:
303-
"""Update our internal "raw data" listing of subscriptions."""
306+
"""Get the latest subscription data."""
304307
subscription_resp = await self.request(
305308
"get", f"users/{self.user_id}/subscriptions", params={"activeOnly": "true"}
306309
)
307-
308-
for subscription in subscription_resp["subscriptions"]:
309-
if "version" not in subscription["location"]["system"]:
310-
LOGGER.error(
311-
"Skipping location with missing system data: %s",
312-
subscription["location"]["sid"],
313-
)
314-
continue
315-
316-
self.subscription_data[subscription["sid"]] = subscription
310+
self.subscription_data = {
311+
subscription["sid"]: subscription
312+
for subscription in subscription_resp["subscriptions"]
313+
}
317314

318315

319316
async def get_api(

‎simplipy/device/__init__.py‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""Define a base SimpliSafe device."""
2+
from enum import Enum
3+
from typing import TYPE_CHECKING, Any, Dict, cast
4+
5+
if TYPE_CHECKING:
6+
from simplipy.system import System
7+
8+
9+
class DeviceTypes(Enum):
10+
"""Device types based on internal SimpliSafe ID number."""
11+
12+
remote = 0
13+
keypad = 1
14+
keychain = 2
15+
panic_button = 3
16+
motion = 4
17+
entry = 5
18+
glass_break = 6
19+
carbon_monoxide = 7
20+
smoke = 8
21+
leak = 9
22+
temperature = 10
23+
camera = 12
24+
siren = 13
25+
doorbell = 15
26+
lock = 16
27+
lock_keypad = 253
28+
unknown = 99
29+
30+
31+
class Device:
32+
"""A base SimpliSafe device.
33+
34+
Note that this class shouldn't be instantiated directly; it will be instantiated as
35+
appropriate via :meth:`simplipy.API.get_systems`.
36+
37+
:param system: A :meth:`simplipy.system.System` object (or one of its subclasses)
38+
:type system: :meth:`simplipy.system.System`
39+
:param device_type: The type of device represented
40+
:type device_type: :meth:`simplipy.device.DeviceTypes`
41+
:param serial: The serial number of the device
42+
:type serial: ``str``
43+
"""
44+
45+
def __init__(self, system: "System", device_type: DeviceTypes, serial: str) -> None:
46+
"""Initialize."""
47+
self._device_type = device_type
48+
self._serial = serial
49+
self._system = system
50+
51+
@property
52+
def name(self) -> str:
53+
"""Return the device name.
54+
55+
:rtype: ``str``
56+
"""
57+
return cast(str, self._system.sensor_data[self._serial]["name"])
58+
59+
@property
60+
def serial(self) -> str:
61+
"""Return the device's serial number.
62+
63+
:rtype: ``str``
64+
"""
65+
return cast(str, self._system.sensor_data[self._serial]["serial"])
66+
67+
@property
68+
def type(self) -> DeviceTypes:
69+
"""Return the device type.
70+
71+
:rtype: :meth:`simplipy.device.DeviceTypes`
72+
"""
73+
return self._device_type
74+
75+
async def update(self, cached: bool = True) -> None:
76+
"""Retrieve the latest state/properties for the device.
77+
78+
The ``cached`` parameter determines whether the SimpliSafe Cloud uses the last
79+
known values retrieved from the base station (``True``) or retrieves new data.
80+
81+
:param cached: Whether to used cached data.
82+
:type cached: ``bool``
83+
"""
84+
await self._system.update(
85+
include_subscription=False, include_settings=False, cached=cached
86+
)
87+
88+
89+
class DeviceV3(Device):
90+
"""A base device for V3 systems.
91+
92+
Note that this class shouldn't be instantiated directly; it will be
93+
instantiated as appropriate via :meth:`simplipy.API.get_systems`.
94+
"""
95+
96+
@property
97+
def error(self) -> bool:
98+
"""Return the device's error status.
99+
100+
:rtype: ``bool``
101+
"""
102+
return cast(
103+
bool,
104+
self._system.sensor_data[self._serial]["status"].get("malfunction", False),
105+
)
106+
107+
@property
108+
def low_battery(self) -> bool:
109+
"""Return whether the device's battery is low.
110+
111+
:rtype: ``bool``
112+
"""
113+
return cast(bool, self._system.sensor_data[self._serial]["flags"]["lowBattery"])
114+
115+
@property
116+
def offline(self) -> bool:
117+
"""Return whether the device is offline.
118+
119+
:rtype: ``bool``
120+
"""
121+
return cast(bool, self._system.sensor_data[self._serial]["flags"]["offline"])
122+
123+
@property
124+
def settings(self) -> Dict[str, Any]:
125+
"""Return the device's settings.
126+
127+
Note that these can change based on what device type the device is.
128+
129+
:rtype: ``dict``
130+
"""
131+
return cast(Dict[str, Any], self._system.sensor_data[self._serial]["setting"])

0 commit comments

Comments
 (0)