Skip to content

Commit 9e3a077

Browse files
authored
v3.0.0 (#79)
* 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.
1 parent 642933e commit 9e3a077

File tree

12 files changed

+1211
-350
lines changed

12 files changed

+1211
-350
lines changed

README.md

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Introduction
22

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

55
Code is licensed under the MIT license.
66

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

54+
# Return only lamps devices:
55+
devices = myq.lamps
56+
# >>> {"serial_number123": <Device>}
57+
58+
# Return only gateway devices:
59+
devices = myq.gateways
60+
# >>> {"serial_number123": <Device>}
61+
5462
# Return *all* devices:
5563
devices = myq.devices
5664
# >>> {"serial_number123": <Device>, "serial_number456": <Device>}
5765

5866

5967
asyncio.get_event_loop().run_until_complete(main())
6068
```
69+
## API Properties
70+
71+
* `accounts`: dictionary with all accounts
72+
* `covers`: dictionary with all covers
73+
* `devices`: dictionary with all devices
74+
* `gateways`: dictionary with all gateways
75+
* `lamps`: dictionary with all lamps
76+
* `last_state_update`: datetime (in UTC) last state update was retrieved
77+
* `password`: password used for authentication. Can only be set, not retrieved
78+
* `username`: username for authentication.
79+
80+
## Account Properties
81+
82+
* `id`: ID for the account
83+
* `name`: Name of the account
6184

6285
## Device Properties
6386

87+
* `account`: Return account associated with device
6488
* `close_allowed`: Return whether the device can be closed unattended.
6589
* `device_family`: Return the family in which this device lives.
6690
* `device_id`: Return the device ID (serial number).
6791
* `device_platform`: Return the device platform.
6892
* `device_type`: Return the device type.
6993
* `firmware_version`: Return the family in which this device lives.
94+
* `href`: URI for device
7095
* `name`: Return the device name.
7196
* `online`: Return whether the device is online.
7297
* `open_allowed`: Return whether the device can be opened unattended.
7398
* `parent_device_id`: Return the device ID (serial number) of this device's parent.
7499
* `state`: Return the current state of the device.
100+
* `state_update`: Returns datetime when device was last updated
101+
102+
## API Methods
103+
104+
These are coroutines and need to be `await`ed – see `example.py` for examples.
105+
106+
* `authenticate`: Authenticate (or re-authenticate) to MyQ. Call this to
107+
re-authenticate immediately after changing username and/or password otherwise
108+
new username/password will only be used when token has to be refreshed.
109+
* `update_device_info`: Retrieve info and status for accounts and devices
75110

76-
## Methods
111+
112+
## Device Methods
77113

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

81-
* `close`: close the device
82-
* `open`: open the device
83-
* `update`: get the latest device info (state, etc.)
117+
* `update`: get the latest device info (state, etc.). Note that
118+
this runs api.update_device_info and thus all accounts/devices will be updated
119+
120+
## Cover Methods
121+
122+
All Device methods in addition to:
123+
* `close`: close the cover
124+
* `open`: open the cover
125+
126+
## Lamp Methods
127+
128+
All Device methods in addition to:
129+
* `turnon`: turn lamp on
130+
* `turnoff`: turn lamp off
131+
132+
133+
# Acknowledgement
134+
135+
Huge thank you to [hjdhjd](https://github.com/hjdhjd) for figuring out the updated V6 API and
136+
sharing his work with us.
84137

85138
# Disclaimer
86139

example.py

Lines changed: 109 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,32 @@
66

77
from pymyq import login
88
from pymyq.errors import MyQError, RequestError
9+
from pymyq.garagedoor import STATE_OPEN, STATE_CLOSED
910

1011
_LOGGER = logging.getLogger()
1112

1213
EMAIL = "<EMAIL>"
1314
PASSWORD = "<PASSWORD>"
14-
OPEN_CLOSE = False
15+
ISSUE_COMMANDS = True
16+
17+
def print_info(number: int, device):
18+
print(f" Device {number + 1}: {device.name}")
19+
print(f" Device Online: {device.online}")
20+
print(f" Device ID: {device.device_id}")
21+
print(
22+
f" Parent Device ID: {device.parent_device_id}",
23+
)
24+
print(f" Device Family: {device.device_family}")
25+
print(
26+
f" Device Platform: {device.device_platform}",
27+
)
28+
print(f" Device Type: {device.device_type}")
29+
print(f" Firmware Version: {device.firmware_version}")
30+
print(f" Open Allowed: {device.open_allowed}")
31+
print(f" Close Allowed: {device.close_allowed}")
32+
print(f" Current State: {device.state}")
33+
print(" ---------")
34+
1535

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

25-
# Get the account ID:
26-
_LOGGER.info("Account ID: %s", api.account_id)
27-
28-
# Get all devices listed with this account – note that you can use
29-
# api.covers to only examine covers:
30-
for idx, device_id in enumerate(api.devices):
31-
device = api.devices[device_id]
32-
_LOGGER.info("---------")
33-
_LOGGER.info("Device %s: %s", idx + 1, device.name)
34-
_LOGGER.info("Device Online: %s", device.online)
35-
_LOGGER.info("Device ID: %s", device.device_id)
36-
_LOGGER.info("Parent Device ID: %s", device.parent_device_id)
37-
_LOGGER.info("Device Family: %s", device.device_family)
38-
_LOGGER.info("Device Platform: %s", device.device_platform)
39-
_LOGGER.info("Device Type: %s", device.device_type)
40-
_LOGGER.info("Firmware Version: %s", device.firmware_version)
41-
_LOGGER.info("Open Allowed: %s", device.open_allowed)
42-
_LOGGER.info("Close Allowed: %s", device.close_allowed)
43-
_LOGGER.info("Current State: %s", device.state)
44-
45-
if OPEN_CLOSE:
46-
try:
47-
await device.open()
48-
await asyncio.sleep(15)
49-
await device.close()
50-
except RequestError as err:
51-
_LOGGER.error(err)
45+
for account in api.accounts:
46+
print(f"Account ID: {account}")
47+
print(f"Account Name: {api.accounts[account]}")
48+
49+
# Get all devices listed with this account – note that you can use
50+
# api.covers to only examine covers or api.lamps for only lamps.
51+
print(f" GarageDoors: {len(api.covers)}")
52+
print(" ---------------")
53+
if len(api.covers) != 0:
54+
for idx, device_id in enumerate(
55+
device_id
56+
for device_id in api.covers
57+
if api.devices[device_id].account == account
58+
):
59+
device = api.devices[device_id]
60+
print_info(number=idx, device=device)
61+
62+
if ISSUE_COMMANDS:
63+
try:
64+
if device.open_allowed:
65+
if device.state == STATE_OPEN:
66+
print(f"Garage door {device.name} is already open")
67+
else:
68+
print(f"Opening garage door {device.name}")
69+
try:
70+
if await device.open(wait_for_state=True):
71+
print(f"Garage door {device.name} has been opened.")
72+
else:
73+
print(f"Failed to open garage door {device.name}.")
74+
except MyQError as err:
75+
_LOGGER.error(f"Error when trying to open {device.name}: {str(err)}")
76+
else:
77+
print(f"Opening of garage door {device.name} is not allowed.")
78+
79+
if device.close_allowed:
80+
if device.state == STATE_CLOSED:
81+
print(f"Garage door {device.name} is already closed")
82+
else:
83+
print(f"Closing garage door {device.name}")
84+
try:
85+
wait_task = await device.close(wait_for_state=False)
86+
except MyQError as err:
87+
_LOGGER.error(f"Error when trying to close {device.name}: {str(err)}")
88+
89+
print(f"Device {device.name} is {device.state}")
90+
91+
if await wait_task:
92+
print(f"Garage door {device.name} has been closed.")
93+
else:
94+
print(f"Failed to close garage door {device.name}.")
95+
96+
except RequestError as err:
97+
_LOGGER.error(err)
98+
print(" ------------------------------")
99+
print(f" Lamps: {len(api.lamps)}")
100+
print(" ---------")
101+
if len(api.lamps) != 0:
102+
for idx, device_id in enumerate(
103+
device_id
104+
for device_id in api.lamps
105+
if api.devices[device_id].account == account
106+
):
107+
device = api.devices[device_id]
108+
print_info(number=idx, device=device)
109+
110+
if ISSUE_COMMANDS:
111+
try:
112+
print(f"Turning lamp {device.name} on")
113+
await device.turnon()
114+
await asyncio.sleep(15)
115+
print(f"Turning lamp {device.name} off")
116+
await device.turnoff()
117+
except RequestError as err:
118+
_LOGGER.error(err)
119+
print(" ------------------------------")
120+
121+
print(f" Gateways: {len(api.gateways)}")
122+
print(" ------------")
123+
if len(api.gateways) != 0:
124+
for idx, device_id in enumerate(
125+
device_id
126+
for device_id in api.gateways
127+
if api.devices[device_id].account == account
128+
):
129+
device = api.devices[device_id]
130+
print_info(number=idx, device=device)
131+
132+
print("------------------------------")
52133

53134
except MyQError as err:
54135
_LOGGER.error("There was an error: %s", err)

pymyq/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""Define a version constant."""
2-
__version__ = '2.0.15'
2+
__version__ = '3.0.0'

0 commit comments

Comments
 (0)