Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed docs/images/ss-login-screen.png
Binary file not shown.
Binary file removed docs/images/ss-verification-confirmed.png
Binary file not shown.
Binary file removed docs/images/ss-verification-email.png
Binary file not shown.
141 changes: 62 additions & 79 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,116 +31,99 @@ the current system state.
Accessing the API
-----------------

Starting in 2021, SimpliSafe™ began to implement an OAuth-based form of authentication.
To use this library, you must handshake with the SimpliSafe™ API; although this process
cannot be 100% accomplished programmatically, the procedure is fairly straightforward.
First, authenticate using your SimpliSafe username/email and password:

Authentication
**************

``simplipy`` comes with a helper script to get you started. To use it, follow these
steps from a command line:

1. Clone the ``simplipy`` Git repo and ``cd`` into it:

.. code:: bash

$ git clone https://github.com/bachya/simplisafe-python.git
$ cd simplisafe-python/

2. Set up and activate a Python virtual environment:

.. code:: bash

$ python3 -m virtualenv .venv
$ source .venv/bin/activate

3. Initialize the dev environment for ``simplipy``:
.. code:: python

.. code:: bash
import asyncio

$ script/setup
from aiohttp import ClientSession
import simplipy

4. Run the ``auth`` script:

.. code:: bash
async def main() -> None:
"""Create the aiohttp session and run."""
async with ClientSession() as session:
api = await simplipy.API.async_from_credentials(
"<USERNAME>"
"<PASSWORD>"
session=session,
)

$ script/auth

5. This will open your browser to a SimpliSafe™ login page. Once you log in with your
credentials, you will see a "Verification Pending" webpage (we'll call this
``Tab 1``):
asyncio.run(main())

.. image:: images/ss-login-screen.png
:width: 400

6. You will be prompted to verify your account. Depending on what you have configured in
your SimpliSafe™ account, this could take the form of an email or an SMS:
This will create an object that is in a "pending" state, meaning that it now awaits
two-factor authentication. You can find the type of two-factor authentication by looking
at the ``auth_state`` property:

.. image:: images/ss-verification-email.png
:width: 400
.. code:: python

7. Once you click the "Verify Device" link, a new browser tab (``Tab 2``) will open
and notify you that the verification is successful:
# If your account uses email-based two-factor authentication:
api.auth_state
# >>> simplipy.api.AuthStates.PENDING_2FA_EMAIL

.. image:: images/ss-verification-confirmed.png
:width: 400
# If your account uses SMS-based two-factor authentication:
api.auth_state
# >>> simplipy.api.AuthStates.PENDING_2FA_SMS

8. Return to ``Tab 1``. You need to find an authorization code; the location of this
code will be different depending on which browser you use:
Performing Email-Based Two-Factor Authentication
*************************************************

* Safari: ``Develop -> Show Web Inspector -> Network Tab`` (look for a reference to ``ErrorPage.html``)
* Edge: ``Developer -> Developer Tools -> Console Tab`` (look for a ``Failed to launch`` error)
* Chrome: ``Developer -> Developer Tools -> Console Tab`` (look for a ``Failed to launch`` error)
This type of two-factor authentication requires you to click a link in an email from
SimpliSafe. At any point, you can see if two-factor authentication has been completed:

Look for a reference to a SimpliSafe™ iOS URL (starting with with
``com.simplisafe.mobile``) and note the ``code`` parameter at the very end:
.. code:: python

.. code::
api.async_verify_2fa_email()

com.simplisafe.mobile://auth.simplisafe.com/ios/com.simplisafe.mobile/callback?code=<CODE>
If the two-factor authentication hasn't succeeded yet, ``simplipy`` will raise a
:meth:`Verify2FAPending <simplipy.errors.Verify2FAPending>` exception. If it has
succeeded, the :meth:`API <simplipy.api.API>` object is ready to use.

9. Copy the ``code`` parameter, return to your terminal, and paste it into the prompt.
You should now see this message:
A common pattern in this scenario would be to loop and regularly test for authentication
(eventually timing out as appropriate):

.. code::
.. code:: python

You are now ready to use the SimpliSafe API!
Authorization Code: <CODE>
Code Verifier: <VERIFIER>
try:
async with timeout(30):
try:
await api.async_verify_2fa_email()
except Verify2FAPending as err:
print("Authentication not yet completed")
await asyncio.sleep(3)
except asyncio.TimeoutError as err:
print("Timed out waiting for authentication")

These one-time values are now ready to be used to instantiate an
:meth:`API <simplipy.api.API>` object.
# Ready to use!

Creating an API Object
**********************
Performing SMS-Based Two-Factor Authentication
**********************************************

Once you have an Authorization Code and Code Verifier, you can create an API object like
this:
This type of two-factor authentication requires you to input a code received via SMS.
SimpliSafe. After you receive the code, you use it like this:

.. code:: python

import asyncio

from aiohttp import ClientSession
import simplipy
api.async_verify_2fa_sms("<CODE>")

If the two-factor authentication hasn't succeeded yet, ``simplipy`` will raise a
:meth:`InvalidCredentialsError <simplipy.errors.InvalidCredentialsError>` exception.
If it has succeeded, the :meth:`API <simplipy.api.API>` object is ready to use.

async def main() -> None:
"""Create the aiohttp session and run."""
async with ClientSession() as session:
api = await simplipy.API.async_from_auth(
"<AUTHORIZATION_CODE>",
"<CODE_VERIFIER>",
session=session,
)

# ...
.. code:: python

try:
await api.async_verify_2fa_sms("<CODE>")
except InvalidCredentialsError as err:
print("Invalid SMS 2FA code")

asyncio.run(main())
# Ready to use!

**REMINDER:** this Authorization Code and Code Verifier can only be used once.
Key API Object Properties
*************************

The :meth:`API <simplipy.api.API>` object contains several sensitive properties to be
aware of:
Expand Down
67 changes: 0 additions & 67 deletions examples/test_client_by_auth.py

This file was deleted.

97 changes: 97 additions & 0 deletions examples/test_client_by_credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Test system functionality."""
import asyncio
import logging

from aiohttp import ClientSession
from async_timeout import timeout

from simplipy.api import API, AuthStates
from simplipy.errors import SimplipyError, Verify2FAError, Verify2FAPending

_LOGGER = logging.getLogger()

SIMPLISAFE_USERNAME = "<USERNAME>"
SIMPLISAFE_PASSWORD = "<PASSWORD>"


async def main() -> None:
"""Create the aiohttp session and run the example."""
async with ClientSession() as session:
logging.basicConfig(level=logging.INFO)

try:
# Start the process of obtaining an authenticated API object using a
# SimpliSafe username/email and password:
simplisafe = await API.async_from_credentials(
SIMPLISAFE_USERNAME,
SIMPLISAFE_PASSWORD,
session=session,
)

if simplisafe.auth_state == AuthStates.PENDING_2FA_EMAIL:
# If the SimpliSafe account is protected by email-based 2FA, go into a
# loop and periodically see if the user has verified the email
# (eventually timing out if nothing happens):
try:
async with timeout(30):
try:
await simplisafe.async_verify_2fa_email()
except Verify2FAPending as err:
_LOGGER.info(err)
await asyncio.sleep(3)
except asyncio.TimeoutError as err:
raise Verify2FAError(
"Timed out while waiting for email-based 2FA verification"
) from err
elif simplisafe.auth_state == AuthStates.PENDING_2FA_SMS:
# If the SimpliSafe account is protected by SMS-based 2FA, have the user
# input the code they receive:
sms_2fa_code = input("Input your SMS-based 2FA code: ")
await simplisafe.async_verify_2fa_sms(sms_2fa_code)

# If we somehow reach this point without the API object being authenticated,
# halt:
if simplisafe.auth_state != AuthStates.AUTHENTICATED:
raise SimplipyError("API object is not authenticated!")

systems = await simplisafe.async_get_systems()
for system in systems.values():
# Print system state:
_LOGGER.info("System state: %s", system.state)

# Print sensor info:
for serial, sensor in system.sensors.items():
_LOGGER.info(
"Sensor %s: (name: %s, type: %s, triggered: %s)",
serial,
sensor.name,
sensor.type,
sensor.triggered,
)

# Arm/disarm the system:
# await system.async_set_away()
# await system.async_set_home()
# await system.async_set_off()

# Print system events:
events = await system.async_get_events()
for event in events:
_LOGGER.info("Event: %s", event)

# Set PINs:
# await system.async_set_pin("Test PIN", "1235")
# await system.async_remove_pin("Test PIN")

# Interact with locks (if we have them):
for serial, lock in system.locks.items():
_LOGGER.info(
"Lock %s: (name: %s, state: %s)", serial, lock.name, lock.state
)
# await lock.async_lock()
# await lock.async_unlock()
except SimplipyError as err:
_LOGGER.error(err)


asyncio.run(main())
30 changes: 0 additions & 30 deletions script/auth

This file was deleted.

Loading