Skip to main content

A Python3, async interface to the SimpliSafe API

Project description

šŸšØ simplisafe-python: A Python3, async interface to the SimpliSafeā„¢ API

Travis CI PyPi Version License Code Coverage Maintainability Say Thanks

simplisafe-python (hereafter referred to as simplipy) is a Python3, asyncio-driven interface to the unofficial SimpliSafeā„¢ API. With it, users can get data on their system (including available sensors), set the system state, and more.

NOTE: SimpliSafeā„¢ has no official API; therefore, this library may stop working at any time without warning.

SPECIAL THANKS: Original inspiration was obtained from https://github.com/greencoder/easysafe-python; thanks to Scott Newman for all the hard work!

Python Versions

simplisafe-python is currently supported on:

  • Python 3.6
  • Python 3.7
  • Python 3.8

Standard vs. Interactive SimpliSafeā„¢ Plans

SimpliSafeā„¢ offers two different monitoring plans:

Standard: Monitoring specialists guard your home around-the-clock from our award-winning monitoring centers. In an emergency, we send the police to your home. Free cellular connection built-in.

Interactive: Standard + advanced mobile app control of your system from anywhere in the world. Get text + email alerts, monitor home activity, arm/disarm your system, control settings right on your smartphone or laptop. Bonus: Secret! Alertsā€”get secretly notified when anyone accesses private rooms, drawers, safes and more.

Please note that only Interactive plans can access sensor values and set the system state; using the API with a Standard plan will be limited to retrieving the current system state.

Installation

pip install simplisafe-python

Usage

Getting Systems Associated with an Account

simplipy starts within an aiohttp ClientSession:

import asyncio

from aiohttp import ClientSession


async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as websession:
        # YOUR CODE HERE


asyncio.get_event_loop().run_until_complete(main())

To get all SimpliSafeā„¢ systems associated with an account:

import asyncio

from aiohttp import ClientSession

from simplipy import API


async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as websession:
        simplisafe = await API.login_via_credentials(
            "<EMAIL>", "<PASSWORD>", websession
        )

        # Get a dict of systems with the system ID as the key:
        systems = await simplisafe.get_systems()
        # >>> {"1234abc": <simplipy.system.SystemV2 object at 0x10661e3c8>, ...}


asyncio.get_event_loop().run_until_complete(main())

The System Object

System objects are used to retrieve data on and control the state of SimpliSafeā„¢ systems. Two types of objects can be returned:

  • SystemV2: an object to control V2 (classic) SimpliSafeā„¢ systems
  • SystemV3: an object to control V3 (new, released in 2018) SimpliSafeā„¢ systems

Despite the differences, simplipy provides a common interface to these objects, meaning many of the same properties and methods are available to both.

Base Properties and Methods

from simplipy import API


async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as websession:
        simplisafe = await API.login_via_credentials(
            "<EMAIL>", "<PASSWORD>", websession
        )

        systems = await simplisafe.get_systems()
        for system_id, system in systems.items():
            # Return a reference to a SimpliSafeā„¢ API object (detailed later):
            system.api
            # >>> <simplipy.api.API object at 0x12aba2321>

            # Return the street address of the system:
            system.address
            # >>> 1234 Main Street

            # Return whether the alarm is currently going off:
            system.alarm_going_off
            # >>> False

            # Return the type of connection the system is using:
            system.connection_type
            # >>> "cell"

            # Return a list of sensors attached to this sytem (detailed later):
            system.sensors
            # >>> [<simplipy.sensor.SensorV2 object at 0x10661e3c8>, ...]

            # Return the system's serial number:
            system.serial
            # >>> xxxxxxxxxxxxxx

            # Return the current state of the system:
            system.state
            # >>> simplipy.system.SystemStates.away

            # Return the SimpliSafeā„¢ identifier for this system from the key:
            system_id
            # >>> 1234abc

            # ...or as a property of the system itself:
            system.system_id
            # >>> 1234abc

            # Return the average of all temperature sensors (if they exist):
            system.temperature
            # >>> 67

            # Return the SimpliSafeā„¢ version:
            system.version
            # >>> 2

            # Return a list of events for the system with an optional start timestamp and
            # number of events - omitting these parameters will return all events (max of
            # 50) stored in SimpliSafeā„¢'s cloud:
            await system.get_events(from_timestamp=1534035861, num_events=2)
            # >>> [{"eventId": 123, ...}, {"eventId": 456, ...}]

            # You can also get the latest event easily:
            await system.get_latest_event()
            # >>> {"eventId": 987, ...}

            # Set the state of the system:
            await system.set_away()
            await system.set_home()
            await system.set_off()

            # Get the latest values from the system; by default, include a refresh
            # of system info and use cached values (both can be overridden):
            await system.update(refresh_location=True, cached=True)


asyncio.get_event_loop().run_until_complete(main())

V3 Properties

from simplipy import API


async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as websession:
        simplisafe = await API.login_via_credentials(
            "<EMAIL>", "<PASSWORD>", websession
        )

        systems = await simplisafe.get_systems()
        for system_id, system in systems.items():
            # Return the number of seconds an activated alarm will sound for:
            system.alarm_duration
            # >>> 240

            # Return the loudness of the alarm volume:
            system.alarm_volume
            # >>> 3

            # Return the power rating of the battery backup:
            system.battery_backup_power_level
            # >>> 5239

            # Return the number of seconds to delay when returning to an "away" alarm:
            system.entry_delay_away
            # >>> 30

            # Return the number of seconds to delay when returning to an "home" alarm:
            system.entry_delay_home
            # >>> 30

            # Return the number of seconds to delay when exiting an "away" alarm:
            system.exit_delay_away
            # >>> 60

            # Return the number of seconds to delay when exiting an "home" alarm:
            system.exit_delay_home
            # >>> 0

            # Return the signal strength of the cell antenna:
            system.gsm_strength
            # >>> -73

            # Return whether the base station light is on:
            system.light
            # >>> True

            # Return whether the system is offline:
            system.offline
            # >>> False

            # Return whether the system is experiencing a power outage:
            system.power_outage
            # >>> False

            # Return whether the base station is noticing RF jamming:
            system.rf_jamming
            # >>> False

            # Return the loudness of the voice prompt:
            system.voice_prompt_volume
            # >>> 2

            # Return the power rating of the A/C outlet:
            system.wall_power_level
            # >>> 5239

            # Return the ssid of the base station:
            system.wifi_ssid
            # >>> "My_SSID"

            # Return the signal strength of the wifi antenna:
            system.wifi_strength
            # >>> -43


asyncio.get_event_loop().run_until_complete(main())

A Note on system.update()

There are two crucial differences between V2 and V3 systems when updating:

  • V2 systems, which use only 2G cell connectivity, will be slower to update than V3 systems when those V3 systems are connected to WiFi.
  • V2 systems will audibly announce, "Your settings have been synchronized." when the update completes; V3 systems will not. Unfortunately, this cannot currently be worked around.

The Sensor Object

Sensor objects provide information about the SimpliSafeā„¢ sensors to which they relate.

NOTE: Individual sensors cannot be updated directly; instead, the update() method on their parent System object should be used. It is crucial to remember that sensor states are only as current as the last time system.update() was called.

Like their System cousins, two types of objects can be returned:

  • SensorV2: an object to view V2 (classic) SimpliSafeā„¢ sensors
  • SensorV3: an object to view V3 (new, released in 2018) SimpliSafeā„¢ sensors

Once again, simplipy provides a common interface to these objects; however, there are some properties that are either (a) specific to one version or (b) return a different meaning based on the version. These differences are outlined below.

Base Properties

from simplipy import API


async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as websession:
        simplisafe = await API.login_via_credentials(
            "<EMAIL>", "<PASSWORD>", websession
        )

        systems = await simplisafe.get_systems()
        for system_id, system in systems.items():
            for serial, sensor in system.sensors.items():
                # Return the sensor's name:
                sensor.name
                # >>> Kitchen Window

                # Return the sensor's serial number through the index:
                serial
                # >>> 1234ABCD

                # ...or through the property:
                sensor.serial
                # >>> 1234ABCD

                # Return the sensor's type:
                sensor.type
                # >>> simplipy.EntityTypes.glass_break

                # Return whether the sensor is in an error state:
                sensor.error
                # >>> False

                # Return whether the sensor has a low battery:
                sensor.low_battery
                # >>> False

                # Return whether the sensor has been triggered (open/closed, etc.):
                sensor.triggered
                # >>> False


asyncio.get_event_loop().run_until_complete(main())

V2 Properties

from simplipy import API


async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as websession:
        simplisafe = await API.login_via_credentials(
            "<EMAIL>", "<PASSWORD>", websession
        )

        systems = await simplisafe.get_systems()
        for system_id, system in systems.items():
            for serial, sensor in system.sensors.items():
                # Return the sensor's data as a currently non-understood integer:
                sensor.data
                # >>> 0

                # Return the sensor's settings as a currently non-understood integer:
                sensor.settings
                # >>> 1


asyncio.get_event_loop().run_until_complete(main())

V3 Properties

from simplipy import API


async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as websession:
        simplisafe = await API.login_via_credentials(
            "<EMAIL>", "<PASSWORD>", websession
        )

        systems = await simplisafe.get_systems()
        for system_id, system in systems.items():
            for sensor in system.sensors:
                # Return whether the sensor is offline:
                sensor.offline
                # >>> False

                # Return a settings dictionary for the sensor:
                sensor.settings
                # >>> {"instantTrigger": False, "away2": 1, "away": 1, ...}

                # For temperature sensors, return the current temperature:
                sensor.temperature
                # >>> 67


asyncio.get_event_loop().run_until_complete(main())

The Lock Object

Lock objects correspond to SimpliSafe locks and allows users to retrieve information on them and alter their state by locking/unlocking them.

NOTE: Individual locks cannot be updated directly; instead, the update() method on their parent System object should be used. It is crucial to remember that lock states are only as current as the last time system.update() was called. The only exception to this rule is when lock.lock() or lock.unlock() are called; both of these will automatically update the lock state.

Base Properties and Methods

from simplipy import API


async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as websession:
        simplisafe = await API.login_via_credentials(
            "<EMAIL>", "<PASSWORD>", websession
        )

        systems = await simplisafe.get_systems()
        for system_id, system in systems.items():
            for serial, lock in system.locks.items():
                # Return the lock's name:
                lock.name
                # >>> Kitchen Window

                # Return the lock's serial number through the index:
                serial
                # >>> 1234ABCD

                # ...or through the property:
                lock.serial
                # >>> 1234ABCD

                # Return the state of the lock:
                lock.state
                # >>> simplipy.lock.LockStates.locked

                # Return whether the lock is in an error state:
                lock.error
                # >>> False

                # Return whether the lock has a low battery:
                lock.low_battery
                # >>> False

                # Return whether the lock is offline:
                lock.offline
                # >>> False

                # Return a settings dictionary for the lock:
                lock.settings
                # >>> {"autoLock": 3, "away": 1, "home": 1}

                # Return whether the lock is disabled:
                lock.disabled
                # >>> False

                # Return whether the lock's battery is low:
                lock.lock_low_battery
                # >>> False

                # Return whether the lock is jammed:
                lock.jammed
                # >>> False

                # Return whether the pin pad's battery is low:
                lock.pin_pad_low_battery
                # >>> False

                # Return whether the pin pad is offline:
                lock.pin_pad_offline
                # >>> False

                # Lock the lock:
                await lock.lock()

                # Unlock the lock:
                await lock.unlock()


asyncio.get_event_loop().run_until_complete(main())

Dealing with PINs

simplipy allows users to easily retrieve, set, reset, and remove PINs associated with a SimpliSafeā„¢ account:

from simplipy import API


async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as websession:
        simplisafe = await API.login_via_credentials(
            "<EMAIL>", "<PASSWORD>", websession
        )

        systems = await simplisafe.get_systems()
        for system_id, system in systems.items():
            # Get all PINs (retrieving fresh or from the cache):
            await system.get_pins(cached=False)
            # >>> {"master": "1234", "duress": "9876"}

            # Set a new user PIN:
            await system.set_pin("My New User", "1122")
            await system.get_pins(cached=False)
            # >>> {"master": "1234", "duress": "9876", "My New User": "1122"}

            # Remove a PIN (by value or by label)
            await system.remove_pin("My New User")
            await system.get_pins(cached=False)
            # >>> {"master": "1234", "duress": "9876"}

            # Set the master PIN (works for the duress PIN, too):
            await system.set_pin("master", "9865")
            await system.get_pins(cached=False)
            # >>> {"master": "9865", "duress": "9876"}


asyncio.get_event_loop().run_until_complete(main())

Note that the above note re: V2 systems ā€“ specifically, their propensity to audibly announce that settings synchronization has occurred ā€“ applies to getting/setting PINs.

The API Object

Each System object has a reference to an API object. This object contains properties and a method useful for authentication and ongoing access.

VERY IMPORTANT NOTE: the API object contains references to SimpliSafeā„¢ access and refresh tokens. It is vitally important that you do not let these tokens leave your control. If exposed, savvy attackers could use them to view and alter your system's state. You have been warned; proper usage of these properties is solely your responsibility.

from simplipy import API


async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as websession:
        simplisafe = await API.login_via_credentials(
            "<EMAIL>", "<PASSWORD>", websession
        )

        systems = await simplisafe.get_systems()
        for system_id, system in systems.items():
            # Return the current access token:
            system.api._access_token
            # >>> 7s9yasdh9aeu21211add

            # Return the current refresh token:
            system.api.refresh_token
            # >>> 896sad86gudas87d6asd

            # Return the SimpliSafeā„¢ user ID associated with this account:
            system.api.user_id
            # >>> 1234567


asyncio.get_event_loop().run_until_complete(main())

Errors/Exceptions

simplipy exposes several useful error types:

  • simplipy.errors.SimplipyError: a base error that all other simplipy errors inherit from
  • simplipy.errors.InvalidCredentialsError: an error related to an invalid username/password combo
  • simplipy.errors.PinError: an error related to an invalid PIN operation, such as attempting to delete a reserved PIN (e.g., "master"), adding too many PINs, etc.
  • simplipy.errors.RequestError: an error related to HTTP requests that return something other than a 200 response code

Refreshing the Access Token

General Notes

During usage, simplipy will automatically refresh the access token as needed. At any point, the "dirtiness" of the token can be checked:

from simplipy import API


async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as websession:
        simplisafe = await API.login_via_token(
            "<REFRESH TOKEN>", websession
        )

        systems = await simplisafe.get_systems()
        for system in systems.items():
            # Assuming the access token was automatically refreshed:
            primary_system.api.refresh_token_dirty
            # >>> True

            # Once the dirtiness is confirmed, the dirty bit resets:
            primary_system.api.refresh_token_dirty
            # >>> False


asyncio.get_event_loop().run_until_complete(main())

Restarting with a Refresh Token

It may be desirable to re-authenticate against the SimpliSafeā„¢ API at some point in the future (and without using a user's email and password). In that case, it is recommended that you save the refresh_token property somewhere; when it comes time to re-authenticate, simply:

from simplipy import API


async def main() -> None:
"""Create the aiohttp session and run."""
    async with ClientSession() as websession:
        simplisafe = await API.login_via_token("<REFRESH TOKEN>", websession)
        systems = await simplisafe.get_systems()
        # ...


asyncio.get_event_loop().run_until_complete(main())

Although no official documentation exists, basic testing appears to confirm the hypothesis that the refresh token is both long-lived and single-use. This means that theoretically, it should be possible to use it to create an access token long into the future. If login_via_token() should throw an error, however, the system object(s) will need to be recreated via login_via_credentials().

Contributing

  1. Check for open features/bugs or initiate a discussion on one.
  2. Fork the repository.
  3. Install the dev environment: make init.
  4. Enter the virtual environment: pipenv shell
  5. Code your new feature or bug fix.
  6. Write a test that covers your new functionality.
  7. Update README.md with any new documentation.
  8. Run tests and ensure 100% code coverage: make coverage
  9. Ensure you have no linting errors: make lint
  10. Ensure you have no typed your code correctly: make typing
  11. Add yourself to AUTHORS.md.
  12. Submit a pull request!

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

simplisafe-python-5.2.0.tar.gz (27.3 kB view hashes)

Uploaded Source

Built Distribution

simplisafe_python-5.2.0-py3-none-any.whl (25.5 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page