33import logging
44from bs4 import BeautifulSoup
55from datetime import datetime , timedelta
6- from typing import Dict , Optional , Union , Tuple
6+ from typing import Dict , List , Optional , Union , Tuple
77from urllib .parse import urlsplit , parse_qs
88
99from aiohttp import ClientSession , ClientResponse
1010from aiohttp .client_exceptions import ClientError , ClientResponseError
1111from pkce import generate_code_verifier , get_code_challenge
12+ from yarl import URL
1213
1314from .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 ,
2421 OAUTH_REDIRECT_URI ,
2522)
2623from .device import MyQDevice
27- from .errors import AuthenticationError , InvalidCredentialsError , RequestError
24+ from .account import MyQAccount
25+ from .errors import AuthenticationError , InvalidCredentialsError , MyQError , RequestError
2826from .garagedoor import MyQGaragedoor
2927from .lamp import MyQLamp
3028from .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
641565async 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