Skip to content

Commit

Permalink
Merge pull request #110 from pyalarmdotcom/websocket-consolidated
Browse files Browse the repository at this point in the history
Websocket consolidated
  • Loading branch information
elahd committed May 18, 2023
2 parents b156f78 + 7a46d5b commit 2d84911
Show file tree
Hide file tree
Showing 19 changed files with 509 additions and 305 deletions.
17 changes: 11 additions & 6 deletions .devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
"customizations": {
"vscode": {
"extensions": [
"github.codespaces",
"ryanluker.vscode-coverage-gutters",
"donjayamanne.githistory",
"github.vscode-pull-request-github",
"bungcip.better-toml",
"donjayamanne.python-extension-pack",
"marklarah.pre-commit-vscode",
Expand All @@ -19,15 +22,19 @@
"editor.codeActionsOnSave": {
"source.fixAll.ruff": true
// "source.organizeImports.ruff": true
}
},
"editor.defaultFormatter": "ms-python.black-formatter"
},
"[yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"python.analysis.autoSearchPaths": false,
"python.formatting.provider": "black",
"python.formatting.provider": "none",
"python.linting.enabled": true,
"python.linting.mypyEnabled": true,
"python.testing.pytestEnabled": true,
"terminal.integrated.defaultProfile.linux": "bash",
"terminal.integrated.profiles.linux": {
"zsh": {
Expand All @@ -42,8 +49,6 @@
},
"image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.10-bullseye",
"name": "pyalarmdotcomajax",
"postCreateCommand": [
"scripts/setup.sh"
],
"postCreateCommand": ["scripts/setup.sh"],
"remoteUser": "vscode"
}
28 changes: 23 additions & 5 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"[python]": {
"editor.codeActionsOnSave": {
"source.fixAll.ruff": true,
"source.organizeImports.ruff": true
},
"editor.defaultFormatter": "ms-python.black-formatter"
},
"[yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"python.analysis.autoSearchPaths": false,
"python.analysis.extraPaths": ["/workspaces/pyalarmdotcomajax"],
"python.formatting.provider": "black",
"python.linting.enabled": true,
"python.testing.pytestEnabled": true,
"python.analysis.typeCheckingMode": "off"
"terminal.integrated.defaultProfile.linux": "bash",
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "/bin/bash"
}
}
}
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ Pyalarmdotcomajax supports core features (monitoring and using actions) of the d
| Device Type | Notable Attributes | Actions | Notes |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------ |
| System | `unit_id` | (none) | |
| Partition | `uncleared_issues`, `desired_state` | arm away, arm stay, arm night, disarm | |
| Partition | `uncleared_issues` | arm away, arm stay, arm night, disarm | |
| Sensor | `device_subtype` | (none) | |
| Locks | `desired_state` | lock, unlock | |
| Locks | | lock, unlock | |
| Garage Door | (none) | open, close | |
| Gate | `supports_remote_close` | open, close | |
| Image Sensor | `images` | peek_in | |
Expand Down
77 changes: 61 additions & 16 deletions examples/tfa_totp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@

import aiohttp

from pyalarmdotcomajax import AlarmController, AuthResult
from pyalarmdotcomajax.errors import AuthenticationFailed, DataFetchFailed
from pyalarmdotcomajax import AlarmController, OtpType
from pyalarmdotcomajax.errors import (
AuthenticationFailed,
DataFetchFailed,
TwoFactor_OtpRequired,
TwoFactor_ConfigurationRequired,
)

USERNAME = "ENTER YOUR USERNAME"
PASSWORD = "ENTER YOUR PASSWORD"
USERNAME = "YOUR USERNAME"
PASSWORD = "YOUR PASSWORD"


async def main() -> None:
Expand All @@ -22,23 +27,18 @@ async def main() -> None:
alarm = AlarmController(username=USERNAME, password=PASSWORD, websession=session)

#
# LOG IN AND HANDLE TWO-FACTOR AUTHENTICATION
# OTP will be pushed to user automatically if OTP is set up via email or sms.
# LOG IN
#

try:
login_result = await alarm.async_login()
await alarm.async_login()

if login_result == AuthResult.OTP_REQUIRED:
print("Two factor authentication is enabled for this user.")
except TwoFactor_OtpRequired:
print("Two factor authentication is enabled for this user.")
await async_handle_otp_workflow(alarm)

if not (code := input("Enter One-Time Password: ")):
sys.exit("Requested OTP was not entered.")

await alarm.async_submit_otp(code=code)

elif login_result == AuthResult.ENABLE_TWO_FACTOR:
sys.exit("Unable to log in. Please set up two-factor authentication for this account.")
except TwoFactor_ConfigurationRequired:
sys.exit("Unable to log in. Please set up two-factor authentication for this account.")

except (ConnectionError, DataFetchFailed):
sys.exit("Could not connect to Alarm.com.")
Expand All @@ -56,5 +56,50 @@ async def main() -> None:
print(f"Name: {sensor.name}, Sensor Type: {sensor.device_subtype}, State: {sensor.state}")


async def async_handle_otp_workflow(alarm: AlarmController) -> None:
"""Handle two-factor authentication workflow."""

selected_otp_method: OtpType

#
# Determine which OTP method to use
#

# Get list of enabled OTP methods.
if len(enabled_2fa_methods := await alarm.async_get_enabled_2fa_methods()) == 1:
# If only one OTP method is enabled, use it without prompting user.
selected_otp_method = enabled_2fa_methods[0]
print(f"Using {selected_otp_method.value} for One-Time Password.")
else:
# If multiple OTP methods are enabled, let the user pick.
print("\nAvailable one-time password methods:")
for otp_method in enabled_2fa_methods:
print(f"{otp_method.value}: {otp_method.name}")
if not (
selected_otp_method := OtpType(
int(input("\nWhich OTP method would you like to use? Enter the method's number: "))
)
):
sys.exit("Valid OTP method was not entered.")

#
# Request OTP
#

if selected_otp_method in (OtpType.EMAIL, OtpType.SMS):
# Ask Alarm.com to send OTP if selected method is EMAIL or SMS.
print(f"Requesting One-Time Password via {selected_otp_method.name}...")
await alarm.async_request_otp(selected_otp_method)

#
# Prompt user for OTP
#

if not (code := input("Enter One-Time Password: ")):
sys.exit("Requested OTP was not entered.")

await alarm.async_submit_otp(code=code, method=selected_otp_method)


loop = asyncio.get_event_loop()
loop.run_until_complete(main())
74 changes: 0 additions & 74 deletions examples/websocket-monitor.py

This file was deleted.

115 changes: 115 additions & 0 deletions examples/websocket_monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Basic example for logging in using time-based one-time password via pyalarmdotcomajax."""

import asyncio
import logging
import os
import sys

import aiohttp

from pyalarmdotcomajax import AlarmController, OtpType
from pyalarmdotcomajax.errors import TwoFactor_ConfigurationRequired, TwoFactor_OtpRequired
from pyalarmdotcomajax.errors import AuthenticationFailed, DataFetchFailed

USERNAME = os.environ.get("ADC_USERNAME")
PASSWORD = os.environ.get("ADC_PASSWORD")
TWOFA_COOKIE = os.environ.get("ADC_2FA_TOKEN")

log = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)


async def main() -> None:
"""Request Alarm.com sensor data."""

if not (USERNAME and PASSWORD):
sys.exit("Missing account credentials.")

async with aiohttp.ClientSession() as session:
#
# CREATE ALARM CONTROLLER
#

alarm = AlarmController(
username=USERNAME,
password=PASSWORD,
websession=session,
twofactorcookie=TWOFA_COOKIE,
)

try:
await alarm.async_login()

except TwoFactor_OtpRequired:
print("Two factor authentication is enabled for this user.")
await async_handle_otp_workflow(alarm)

except TwoFactor_ConfigurationRequired:
sys.exit("Unable to log in. Please set up two-factor authentication for this account.")

except (ConnectionError, DataFetchFailed):
sys.exit("Could not connect to Alarm.com.")

except AuthenticationFailed:
sys.exit("Invalid credentials.")

#
# PULL DEVICE DATA FROM ALARM.COM
#

await alarm.async_update()

#
# OPEN WEBSOCKET CONNECTION
#

ws_client = alarm.get_websocket_client()
await ws_client.async_connect()


async def async_handle_otp_workflow(alarm: AlarmController) -> None:
"""Handle two-factor authentication workflow."""

selected_otp_method: OtpType

#
# Determine which OTP method to use
#

# Get list of enabled OTP methods.
if len(enabled_2fa_methods := await alarm.async_get_enabled_2fa_methods()) == 1:
# If only one OTP method is enabled, use it without prompting user.
selected_otp_method = enabled_2fa_methods[0]
print(f"Using {selected_otp_method.value} for One-Time Password.")
else:
# If multiple OTP methods are enabled, let the user pick.
print("\nAvailable one-time password methods:")
for otp_method in enabled_2fa_methods:
print(f"{otp_method.value}: {otp_method.name}")
if not (
selected_otp_method := OtpType(
int(input("\nWhich OTP method would you like to use? Enter the method's number: "))
)
):
sys.exit("Valid OTP method was not entered.")

#
# Request OTP
#

if selected_otp_method in (OtpType.EMAIL, OtpType.SMS):
# Ask Alarm.com to send OTP if selected method is EMAIL or SMS.
print(f"Requesting One-Time Password via {selected_otp_method.name}...")
await alarm.async_request_otp(selected_otp_method)

#
# Prompt user for OTP
#

if not (code := input("Enter One-Time Password: ")):
sys.exit("Requested OTP was not entered.")

await alarm.async_submit_otp(code=code, method=selected_otp_method)


asyncio.run(main())
Loading

0 comments on commit 2d84911

Please sign in to comment.