Skip to content

Commit

Permalink
v3.0.0 (#79)
Browse files Browse the repository at this point in the history
* Set to new version 3.0.0 for V6 API

* -) Updated to use new V6 API
-) Added support for lamps

* Added option to wait till garage door is opened or closed before returning call.

* Additional debug
Changed update interval to 20 seconds from 5

* Token received also has an expires value. Using half of that to refresh token instead of just default of 2 minutes.

* Token received also has an expires value. Using half of that to refresh token instead of just default of 2 minutes.

* Fixed token issue

* Added last error received in warning message when request fails.

* Fix for last refresh date of token

* -) Changed length for PKCE to 43
-) Added User-Agent = null to header for authorization page
-) Date stored for last token retrieval now based on local timezone

* -) Fixed retry attempts to retry a request 5 times
-) Reset token if a request returns 401 (unauthorized)
-) Schedule re-authentication as separate task allowing current request to go through (expiration is set half of received expiration)
-) If expires received is less then default then set to default.

* DEFAULT_TOKEN_REFRESH has to be an int now.

* -) Add #attempt to warning message in request.
-) Add last_status_update to api to get last time status was updated (UTC)
-) Add status_update for each device to state last time status was updated (UTC)
-) Added lock to status update ensuring only 1 task can execute it at a time.

* Small fix for datetime object not imported

* Other small fix in request.

* -) Will raise AuthenticationError now when 401 is received.
-) When a request fails due to 401, re-authentication will be done and request then resend

* Updated README

* Fix in authentication when request receives 401

* Set OPEN_CLOSE to true in example.py

* -) Added some more debug messages
-) Fixed not clearing authentication task upon failure

* -) Changed logging request attempts from warning to debug
-) Failing to re-authenticate will now only result in debug log unless we have to re-authentication to continue
-) Cleaned up authentication portion

* -) Added tr:except for authentication in login
-) Changed error messages for login requests to debug instead.

* -) Changed few more error log entries to debug

* -) Set flag to not re-try authentication if it failed due to faulty username/password. Try to prevent from account getting locked.

* -) Pushed something shouldn't have.

* -) Don't use web session for authentication requests ensuring new connection is made each time. Hopefully reduce connection reset by peer messages

* -) Don't use web session for authentication requests ensuring new connection is made each time. Hopefully reduce connection reset by peer messages

* -) Changed, calling authenticate will now use it's own ClientSession that will be closed at tend of authentication.

* -) Fix for resetting authentication task

* -) removed redudant debug log entry

* -) Fixed commands for turning lamps on & off

* -) Change minimum update interval to 10 seconds from 30 seconds

* -) Added typing
-) Support for device state different from what is returned from MyQ. This to support waiting until an action is completed yet still return intermediate state (i.e. opening, closing)
-) Moved wait_for_state to MyQDevice and ensure device state is reset

* -) Update example.py to show how to use wait_for_state and exception handling
-) Changed OPEN_CLOSE constant to ISSUE_COMMANDS

* -) last_update is not available for each device, fixed.

* -) last_update is not available for each device, fixed.

* Few more debug messaging

* Fix for when there is no last_update

* Fix for when there is no last_update

* Updated wait task names for open & close

* Fix state returned for device

* Add state to debug message

* Fix wait for task

* Wait task for open and closed is fixed now.

* -) Moved getting account and device portions in their own methods
-) Device update when waiting for task to be completed (open/close) is now done every 5 seconds and not part of minimum update interval.

* -) Removed sleep between open & close for cover since we now wait for state anyways.

* -) Removed duplicate debug message

* -) Fixed debug entry

* Improved web scraping from login page making it less error prone in case something is changed on it.
  • Loading branch information
ehendrix23 authored Feb 5, 2021
1 parent 642933e commit 9e3a077
Show file tree
Hide file tree
Showing 12 changed files with 1,211 additions and 350 deletions.
63 changes: 58 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Introduction

This is a Python 3.5+ module aiming to interact with the Chamberlain MyQ API.
This is a Python 3.8+ module aiming to interact with the Chamberlain MyQ API.

Code is licensed under the MIT license.

Expand Down Expand Up @@ -51,36 +51,89 @@ async def main() -> None:
devices = myq.covers
# >>> {"serial_number123": <Device>}

# Return only lamps devices:
devices = myq.lamps
# >>> {"serial_number123": <Device>}

# Return only gateway devices:
devices = myq.gateways
# >>> {"serial_number123": <Device>}

# Return *all* devices:
devices = myq.devices
# >>> {"serial_number123": <Device>, "serial_number456": <Device>}


asyncio.get_event_loop().run_until_complete(main())
```
## API Properties

* `accounts`: dictionary with all accounts
* `covers`: dictionary with all covers
* `devices`: dictionary with all devices
* `gateways`: dictionary with all gateways
* `lamps`: dictionary with all lamps
* `last_state_update`: datetime (in UTC) last state update was retrieved
* `password`: password used for authentication. Can only be set, not retrieved
* `username`: username for authentication.

## Account Properties

* `id`: ID for the account
* `name`: Name of the account

## Device Properties

* `account`: Return account associated with device
* `close_allowed`: Return whether the device can be closed unattended.
* `device_family`: Return the family in which this device lives.
* `device_id`: Return the device ID (serial number).
* `device_platform`: Return the device platform.
* `device_type`: Return the device type.
* `firmware_version`: Return the family in which this device lives.
* `href`: URI for device
* `name`: Return the device name.
* `online`: Return whether the device is online.
* `open_allowed`: Return whether the device can be opened unattended.
* `parent_device_id`: Return the device ID (serial number) of this device's parent.
* `state`: Return the current state of the device.
* `state_update`: Returns datetime when device was last updated

## API Methods

These are coroutines and need to be `await`ed – see `example.py` for examples.

* `authenticate`: Authenticate (or re-authenticate) to MyQ. Call this to
re-authenticate immediately after changing username and/or password otherwise
new username/password will only be used when token has to be refreshed.
* `update_device_info`: Retrieve info and status for accounts and devices

## Methods

## Device Methods

All of the routines on the `MyQDevice` class are coroutines and need to be
`await`ed – see `example.py` for examples.

* `close`: close the device
* `open`: open the device
* `update`: get the latest device info (state, etc.)
* `update`: get the latest device info (state, etc.). Note that
this runs api.update_device_info and thus all accounts/devices will be updated

## Cover Methods

All Device methods in addition to:
* `close`: close the cover
* `open`: open the cover

## Lamp Methods

All Device methods in addition to:
* `turnon`: turn lamp on
* `turnoff`: turn lamp off


# Acknowledgement

Huge thank you to [hjdhjd](https://github.com/hjdhjd) for figuring out the updated V6 API and
sharing his work with us.

# Disclaimer

Expand Down
137 changes: 109 additions & 28 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,32 @@

from pymyq import login
from pymyq.errors import MyQError, RequestError
from pymyq.garagedoor import STATE_OPEN, STATE_CLOSED

_LOGGER = logging.getLogger()

EMAIL = "<EMAIL>"
PASSWORD = "<PASSWORD>"
OPEN_CLOSE = False
ISSUE_COMMANDS = True

def print_info(number: int, device):
print(f" Device {number + 1}: {device.name}")
print(f" Device Online: {device.online}")
print(f" Device ID: {device.device_id}")
print(
f" Parent Device ID: {device.parent_device_id}",
)
print(f" Device Family: {device.device_family}")
print(
f" Device Platform: {device.device_platform}",
)
print(f" Device Type: {device.device_type}")
print(f" Firmware Version: {device.firmware_version}")
print(f" Open Allowed: {device.open_allowed}")
print(f" Close Allowed: {device.close_allowed}")
print(f" Current State: {device.state}")
print(" ---------")


async def main() -> None:
"""Create the aiohttp session and run the example."""
Expand All @@ -22,33 +42,94 @@ async def main() -> None:
print(f"{EMAIL} {PASSWORD}")
api = await login(EMAIL, PASSWORD, websession)

# Get the account ID:
_LOGGER.info("Account ID: %s", api.account_id)

# Get all devices listed with this account – note that you can use
# api.covers to only examine covers:
for idx, device_id in enumerate(api.devices):
device = api.devices[device_id]
_LOGGER.info("---------")
_LOGGER.info("Device %s: %s", idx + 1, device.name)
_LOGGER.info("Device Online: %s", device.online)
_LOGGER.info("Device ID: %s", device.device_id)
_LOGGER.info("Parent Device ID: %s", device.parent_device_id)
_LOGGER.info("Device Family: %s", device.device_family)
_LOGGER.info("Device Platform: %s", device.device_platform)
_LOGGER.info("Device Type: %s", device.device_type)
_LOGGER.info("Firmware Version: %s", device.firmware_version)
_LOGGER.info("Open Allowed: %s", device.open_allowed)
_LOGGER.info("Close Allowed: %s", device.close_allowed)
_LOGGER.info("Current State: %s", device.state)

if OPEN_CLOSE:
try:
await device.open()
await asyncio.sleep(15)
await device.close()
except RequestError as err:
_LOGGER.error(err)
for account in api.accounts:
print(f"Account ID: {account}")
print(f"Account Name: {api.accounts[account]}")

# Get all devices listed with this account – note that you can use
# api.covers to only examine covers or api.lamps for only lamps.
print(f" GarageDoors: {len(api.covers)}")
print(" ---------------")
if len(api.covers) != 0:
for idx, device_id in enumerate(
device_id
for device_id in api.covers
if api.devices[device_id].account == account
):
device = api.devices[device_id]
print_info(number=idx, device=device)

if ISSUE_COMMANDS:
try:
if device.open_allowed:
if device.state == STATE_OPEN:
print(f"Garage door {device.name} is already open")
else:
print(f"Opening garage door {device.name}")
try:
if await device.open(wait_for_state=True):
print(f"Garage door {device.name} has been opened.")
else:
print(f"Failed to open garage door {device.name}.")
except MyQError as err:
_LOGGER.error(f"Error when trying to open {device.name}: {str(err)}")
else:
print(f"Opening of garage door {device.name} is not allowed.")

if device.close_allowed:
if device.state == STATE_CLOSED:
print(f"Garage door {device.name} is already closed")
else:
print(f"Closing garage door {device.name}")
try:
wait_task = await device.close(wait_for_state=False)
except MyQError as err:
_LOGGER.error(f"Error when trying to close {device.name}: {str(err)}")

print(f"Device {device.name} is {device.state}")

if await wait_task:
print(f"Garage door {device.name} has been closed.")
else:
print(f"Failed to close garage door {device.name}.")

except RequestError as err:
_LOGGER.error(err)
print(" ------------------------------")
print(f" Lamps: {len(api.lamps)}")
print(" ---------")
if len(api.lamps) != 0:
for idx, device_id in enumerate(
device_id
for device_id in api.lamps
if api.devices[device_id].account == account
):
device = api.devices[device_id]
print_info(number=idx, device=device)

if ISSUE_COMMANDS:
try:
print(f"Turning lamp {device.name} on")
await device.turnon()
await asyncio.sleep(15)
print(f"Turning lamp {device.name} off")
await device.turnoff()
except RequestError as err:
_LOGGER.error(err)
print(" ------------------------------")

print(f" Gateways: {len(api.gateways)}")
print(" ------------")
if len(api.gateways) != 0:
for idx, device_id in enumerate(
device_id
for device_id in api.gateways
if api.devices[device_id].account == account
):
device = api.devices[device_id]
print_info(number=idx, device=device)

print("------------------------------")

except MyQError as err:
_LOGGER.error("There was an error: %s", err)
Expand Down
2 changes: 1 addition & 1 deletion pymyq/__version__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"""Define a version constant."""
__version__ = '2.0.15'
__version__ = '3.0.0'
Loading

0 comments on commit 9e3a077

Please sign in to comment.