Skip to content

Commit ca3174d

Browse files
committed
Creation of Account class and putting
devices for the account within that account class
1 parent a05827d commit ca3174d

File tree

5 files changed

+155
-198
lines changed

5 files changed

+155
-198
lines changed

example.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ async def main() -> None:
4343
print(f"{EMAIL} {PASSWORD}")
4444
api = await login(EMAIL, PASSWORD, websession)
4545

46-
for account in api.accounts:
47-
print(f"Account ID: {account}")
48-
print(f"Account Name: {api.accounts[account]}")
46+
for account in api.accounts.values():
47+
print(f"Account ID: {account.account_id}")
48+
print(f"Account Name: {account.name}")
4949

5050
# Get all devices listed with this account – note that you can use
5151
# api.covers to only examine covers or api.lamps for only lamps.
@@ -106,7 +106,11 @@ async def main() -> None:
106106

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

109-
if wait_task and await wait_task:
109+
if (
110+
wait_task
111+
and isinstance(wait_task, asyncio.Task)
112+
and await wait_task
113+
):
110114
print(
111115
f"Garage door {device.name} has been closed."
112116
)

pymyq/api.py

Lines changed: 78 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,16 @@
33
import logging
44
from bs4 import BeautifulSoup
55
from datetime import datetime, timedelta
6-
from typing import Dict, Optional, Union, Tuple
6+
from typing import Dict, List, Optional, Union, Tuple
77
from urllib.parse import urlsplit, parse_qs
88

99
from aiohttp import ClientSession, ClientResponse
1010
from aiohttp.client_exceptions import ClientError, ClientResponseError
1111
from pkce import generate_code_verifier, get_code_challenge
12+
from yarl import URL
1213

1314
from .const import (
1415
ACCOUNTS_ENDPOINT,
15-
DEVICES_ENDPOINT,
16-
DEVICE_FAMILY_GARAGEDOOR,
17-
DEVICE_FAMILY_GATEWAY,
18-
DEVICE_FAMLY_LAMP,
1916
OAUTH_CLIENT_ID,
2017
OAUTH_CLIENT_SECRET,
2118
OAUTH_AUTHORIZE_URI,
@@ -24,7 +21,8 @@
2421
OAUTH_REDIRECT_URI,
2522
)
2623
from .device import MyQDevice
27-
from .errors import AuthenticationError, InvalidCredentialsError, RequestError
24+
from .account import MyQAccount
25+
from .errors import AuthenticationError, InvalidCredentialsError, MyQError, RequestError
2826
from .garagedoor import MyQGaragedoor
2927
from .lamp import MyQLamp
3028
from .request import MyQRequest, REQUEST_METHODS
@@ -55,36 +53,40 @@ def __init__(
5553
None,
5654
) # type: Tuple[Optional[str], Optional[datetime], Optional[datetime]]
5755

58-
self.accounts = {} # type: Dict[str, str]
59-
self.devices = {} # type: Dict[str, MyQDevice]
56+
self.accounts = {} # type: Dict[str, MyQAccount]
6057
self.last_state_update = None # type: Optional[datetime]
6158

59+
@property
60+
def devices(self) -> Dict[str, Union[MyQDevice, MyQGaragedoor, MyQLamp]]:
61+
"""Return all devices."""
62+
devices = {}
63+
for account in self.accounts.values():
64+
devices.update(account.devices)
65+
return devices
66+
6267
@property
6368
def covers(self) -> Dict[str, MyQGaragedoor]:
6469
"""Return only those devices that are covers."""
65-
return {
66-
device_id: device
67-
for device_id, device in self.devices.items()
68-
if device.device_json["device_family"] == DEVICE_FAMILY_GARAGEDOOR
69-
}
70+
covers = {}
71+
for account in self.accounts.values():
72+
covers.update(account.covers)
73+
return covers
7074

7175
@property
72-
def lamps(self) -> Dict[str, MyQDevice]:
76+
def lamps(self) -> Dict[str, MyQLamp]:
7377
"""Return only those devices that are covers."""
74-
return {
75-
device_id: device
76-
for device_id, device in self.devices.items()
77-
if device.device_json["device_family"] == DEVICE_FAMLY_LAMP
78-
}
78+
lamps = {}
79+
for account in self.accounts.values():
80+
lamps.update(account.lamps)
81+
return lamps
7982

8083
@property
8184
def gateways(self) -> Dict[str, MyQDevice]:
8285
"""Return only those devices that are covers."""
83-
return {
84-
device_id: device
85-
for device_id, device in self.devices.items()
86-
if device.device_json["device_family"] == DEVICE_FAMILY_GATEWAY
87-
}
86+
gateways = {}
87+
for account in self.accounts.values():
88+
gateways.update(account.gateways)
89+
return gateways
8890

8991
@property
9092
def _code_verifier(self) -> str:
@@ -97,32 +99,32 @@ def username(self) -> str:
9799
return self.__credentials["username"]
98100

99101
@username.setter
100-
def username(self, username: str) -> None:
102+
def username(self, username: str):
101103
self._invalid_credentials = False
102104
self.__credentials["username"] = username
103105

104106
@property
105-
def password(self) -> None:
107+
def password(self) -> Optional[str]:
106108
return None
107109

108110
@password.setter
109-
def password(self, password: str) -> None:
111+
def password(self, password: str):
110112
self._invalid_credentials = False
111113
self.__credentials["password"] = password
112114

113115
async def request(
114116
self,
115117
method: str,
116118
returns: str,
117-
url: str,
119+
url: Union[URL, str],
118120
websession: ClientSession = None,
119121
headers: dict = None,
120122
params: dict = None,
121123
data: dict = None,
122124
json: dict = None,
123125
allow_redirects: bool = True,
124126
login_request: bool = False,
125-
) -> Tuple[ClientResponse, Union[dict, str, None]]:
127+
) -> Tuple[ClientResponse, Optional[Union[dict, str]]]:
126128
"""Make a request."""
127129

128130
# Determine the method to call based on what is to be returned.
@@ -414,6 +416,11 @@ async def _oauth_authenticate(self) -> Tuple[str, int]:
414416
login_request=True,
415417
)
416418

419+
if not isinstance(data, dict):
420+
raise MyQError(
421+
f"Received object data of type {type(data)} but expecting type dict"
422+
)
423+
417424
token = f"{data.get('token_type')} {data.get('access_token')}"
418425
try:
419426
expires = int(data.get("expires_in", DEFAULT_TOKEN_REFRESH))
@@ -482,7 +489,7 @@ async def _authenticate(self) -> None:
482489
datetime.now(),
483490
)
484491

485-
async def _get_accounts(self) -> Optional[dict]:
492+
async def _get_accounts(self) -> List:
486493

487494
_LOGGER.debug("Retrieving account information")
488495

@@ -491,104 +498,14 @@ async def _get_accounts(self) -> Optional[dict]:
491498
method="get", returns="json", url=ACCOUNTS_ENDPOINT
492499
)
493500

494-
if accounts_resp is not None and accounts_resp.get("accounts") is not None:
495-
accounts = {}
496-
for account in accounts_resp["accounts"]:
497-
account_id = account.get("id")
498-
if account_id is not None:
499-
_LOGGER.debug(
500-
f"Got account {account_id} with name {account.get('name')}"
501-
)
502-
accounts.update({account_id: account.get("name")})
503-
else:
504-
_LOGGER.debug(f"No accounts found")
505-
accounts = None
506-
507-
return accounts
508-
509-
async def _get_devices_for_account(self, account) -> None:
510-
511-
_LOGGER.debug(f"Retrieving devices for account {self.accounts[account]}")
512-
513-
_, devices_resp = await self.request(
514-
method="get",
515-
returns="json",
516-
url=DEVICES_ENDPOINT.format(account_id=account),
517-
)
518-
519-
state_update_timestmp = datetime.utcnow()
520-
if devices_resp is not None and devices_resp.get("items") is not None:
521-
for device in devices_resp.get("items"):
522-
serial_number = device.get("serial_number")
523-
if serial_number is None:
524-
_LOGGER.debug(
525-
f"No serial number for device with name {device.get('name')}."
526-
)
527-
continue
528-
529-
if serial_number in self.devices:
530-
_LOGGER.debug(
531-
f"Updating information for device with serial number {serial_number}"
532-
)
533-
myqdevice = self.devices[serial_number]
534-
535-
# When performing commands we might update the state temporary, need to ensure
536-
# that the state is not set back to something else if MyQ does not yet have updated
537-
# state
538-
last_update = myqdevice.device_json["state"].get("last_update")
539-
myqdevice.device_json = device
540-
541-
if (
542-
myqdevice.device_json["state"].get("last_update") is not None
543-
and myqdevice.device_json["state"].get("last_update")
544-
!= last_update
545-
):
546-
# MyQ has updated device state, reset ours ensuring we have the one from MyQ.
547-
myqdevice.state = None
548-
_LOGGER.debug(
549-
f"State for device {myqdevice.name} was updated to {myqdevice.state}"
550-
)
501+
if accounts_resp is not None and not isinstance(accounts_resp, dict):
502+
raise MyQError(
503+
f"Received object accounts_resp of type {type(accounts_resp)} but expecting type dict"
504+
)
551505

552-
myqdevice.state_update = state_update_timestmp
553-
else:
554-
if device.get("device_family") == DEVICE_FAMILY_GARAGEDOOR:
555-
_LOGGER.debug(
556-
f"Adding new garage door with serial number {serial_number}"
557-
)
558-
self.devices[serial_number] = MyQGaragedoor(
559-
api=self,
560-
account=account,
561-
device_json=device,
562-
state_update=state_update_timestmp,
563-
)
564-
elif device.get("device_family") == DEVICE_FAMLY_LAMP:
565-
_LOGGER.debug(
566-
f"Adding new lamp with serial number {serial_number}"
567-
)
568-
self.devices[serial_number] = MyQLamp(
569-
api=self,
570-
account=account,
571-
device_json=device,
572-
state_update=state_update_timestmp,
573-
)
574-
elif device.get("device_family") == DEVICE_FAMILY_GATEWAY:
575-
_LOGGER.debug(
576-
f"Adding new gateway with serial number {serial_number}"
577-
)
578-
self.devices[serial_number] = MyQDevice(
579-
api=self,
580-
account=account,
581-
device_json=device,
582-
state_update=state_update_timestmp,
583-
)
584-
else:
585-
_LOGGER.warning(
586-
f"Unknown device family {device.get('device_family')}"
587-
)
588-
else:
589-
_LOGGER.debug(f"No devices found for account {self.accounts[account]}")
506+
return accounts_resp.get("accounts", []) if accounts_resp is not None else []
590507

591-
async def update_device_info(self, for_account: str = None) -> None:
508+
async def update_device_info(self) -> None:
592509
"""Get up-to-date device info."""
593510
# The MyQ API can time out if state updates are too frequent; therefore,
594511
# if back-to-back requests occur within a threshold, respond to only the first
@@ -602,40 +519,47 @@ async def update_device_info(self, for_account: str = None) -> None:
602519
)
603520

604521
# Ensure we're within our minimum update interval AND update request is not for a specific device
605-
if call_dt < next_available_call_dt and for_account is None:
606-
_LOGGER.debug(
607-
"Ignoring device update request as it is within throttle window"
608-
)
522+
if call_dt < next_available_call_dt:
523+
_LOGGER.debug("Ignoring update request as it is within throttle window")
609524
return
610525

611-
_LOGGER.debug("Updating device information")
526+
_LOGGER.debug("Updating account information")
612527
# If update request is for a specific account then do not retrieve account information.
613-
if for_account is None:
614-
self.accounts = await self._get_accounts()
528+
accounts = await self._get_accounts()
615529

616-
if self.accounts is None:
617-
_LOGGER.debug(f"No accounts found")
618-
self.devices = {}
619-
accounts = {}
620-
else:
621-
accounts = self.accounts
622-
else:
623-
# Request is for specific account, thus restrict retrieval to the 1 account.
624-
if self.accounts.get(for_account) is None:
625-
# Checking to ensure we know the account, but this should never happen.
626-
_LOGGER.debug(
627-
f"Unable to perform update request for account {for_account} as it is not known."
628-
)
629-
accounts = {}
630-
else:
631-
accounts = {for_account: self.accounts.get(for_account)}
530+
if len(accounts) == 0:
531+
_LOGGER.debug("No accounts found")
532+
self.accounts = {}
533+
return
632534

633535
for account in accounts:
634-
await self._get_devices_for_account(account=account)
536+
print(account)
537+
account_id = account.get("id")
538+
if account_id is not None:
539+
if self.accounts.get(account_id):
540+
# Account already existed, update information.
541+
_LOGGER.debug(
542+
"Updating account %s with name %s",
543+
account_id,
544+
account.get("name"),
545+
)
635546

636-
# Update our last update timestamp UNLESS this is for a specific account
637-
if for_account is None:
638-
self.last_state_update = datetime.utcnow()
547+
self.accounts.get(account_id).account_json = account
548+
else:
549+
# This is a new account.
550+
_LOGGER.debug(
551+
"New account %s with name %s",
552+
account_id,
553+
account.get("name"),
554+
)
555+
self.accounts.update(
556+
{account_id: MyQAccount(api=self, account_json=account)}
557+
)
558+
559+
# Perform a device update for this account.
560+
await self.accounts.get(account_id).update()
561+
562+
self.last_state_update = datetime.utcnow()
639563

640564

641565
async def login(username: str, password: str, websession: ClientSession = None) -> API:
@@ -647,9 +571,7 @@ async def login(username: str, password: str, websession: ClientSession = None)
647571
try:
648572
await api.authenticate(wait=True)
649573
except InvalidCredentialsError as err:
650-
_LOGGER.error(
651-
f"Username and/or password are invalid. Update username/password."
652-
)
574+
_LOGGER.error("Username and/or password are invalid. Update username/password.")
653575
raise err
654576
except AuthenticationError as err:
655577
_LOGGER.error(f"Authentication failed: {str(err)}")

0 commit comments

Comments
 (0)