class GlocaltokensApiClient: """API client""" def __init__( self, hass: HomeAssistant, session: ClientSession, username: Optional[str] = None, password: Optional[str] = None, master_token: Optional[str] = None, android_id: Optional[str] = None, zeroconf_instance: Optional[Zeroconf] = None, ): """Sample API Client.""" self.hass = hass self._username = username self._password = password self._session = session self._android_id = android_id verbose = _LOGGER.level == logging.DEBUG self._client = GLocalAuthenticationTokens( username=username, password=password, master_token=master_token, android_id=android_id, verbose=verbose, ) self.google_devices: List[GoogleHomeDevice] = [] self.zeroconf_instance = zeroconf_instance async def async_get_master_token(self) -> str: """Get master API token""" def _get_master_token() -> Optional[str]: return self._client.get_master_token() master_token = await self.hass.async_add_executor_job(_get_master_token ) if master_token is None or is_aas_et(master_token) is False: raise InvalidMasterToken return master_token async def get_google_devices(self) -> List[GoogleHomeDevice]: """Get google device authentication tokens. Note this method will fetch necessary access tokens if missing""" if not self.google_devices: def _get_google_devices() -> List[Device]: return self._client.get_google_devices( zeroconf_instance=self.zeroconf_instance, force_homegraph_reload=True, ) google_devices = await self.hass.async_add_executor_job( _get_google_devices) self.google_devices = [ GoogleHomeDevice( name=device.device_name, auth_token=device.local_auth_token, ip_address=device.ip_address, hardware=device.hardware, ) for device in google_devices ] return self.google_devices async def get_android_id(self) -> Optional[str]: """Generate random android_id""" def _get_android_id() -> Optional[str]: return self._client.get_android_id() return await self.hass.async_add_executor_job(_get_android_id) @staticmethod def create_url(ip_address: str, port: int, api_endpoint: str) -> str: """Creates url to endpoint. Note: port argument is unused because all request must be done to 8443""" return f"https://{ip_address}:{port}/{api_endpoint}" async def get_alarms_and_timers(self, device: GoogleHomeDevice, ip_address: str, auth_token: str) -> GoogleHomeDevice: """Fetches timers and alarms from google device""" url = self.create_url(ip_address, PORT, API_ENDPOINT_ALARMS) _LOGGER.debug( "Fetching data from Google Home device %s - %s", device.name, url, ) HEADERS[HEADER_CAST_LOCAL_AUTH] = auth_token resp = None try: async with self._session.get(url, headers=HEADERS, timeout=TIMEOUT) as response: if response.status == HTTP_OK: resp = await response.json() device.available = True if resp: if JSON_TIMER in resp or JSON_ALARM in resp: device.set_timers(resp.get(JSON_TIMER)) device.set_alarms(resp.get(JSON_ALARM)) _LOGGER.debug( "Succesfully retrieved data from %s.", device.name) else: _LOGGER.error( ("Failed to parse fetched data for device %s - %s. " "Received = %s"), device.name, API_RETURNED_UNKNOWN, resp, ) elif response.status == HTTP_UNAUTHORIZED: # If token is invalid - force reload homegraph providing new token # and rerun the task. _LOGGER.debug( ("Failed to fetch data from %s due to invalid token. " "Will refresh the token and try again."), device.name, ) # We need to retry the update task instead of just cleaning the list self.google_devices = [] device.available = False elif response.status == HTTP_NOT_FOUND: _LOGGER.debug( ("Failed to fetch data from %s, API returned %d. " "The device(hardware='%s') is possibly not Google Home " "compatible and has no alarms/timers. " "Will retry later."), device.name, response.status, device.hardware, ) device.available = False else: _LOGGER.error( "Failed to fetch %s data, API returned %d: %s", device.name, response.status, response, ) device.available = False except ClientConnectorError: _LOGGER.debug( ("Failed to connect to %s device. " "The device is probably offline. Will retry later."), device.name, ) device.available = False except ClientError as ex: # Make sure that we log the exception if one occurred. # The only reason we do this broad is so we easily can # debug the application. _LOGGER.error( "Request error from %s device: %s", device.name, ex, ) device.available = False except asyncio.TimeoutError: _LOGGER.debug( "%s device timed out while trying to get alarms and timers.", device.name, ) device.available = False return device async def collect_data_from_endpoints(self, device: GoogleHomeDevice, ip_address: str, auth_token: str) -> GoogleHomeDevice: """Collect data from different endpoints.""" device = await self.get_alarms_and_timers(device, ip_address, auth_token) device = await self.update_do_not_disturb(device) return device async def update_google_devices_information( self) -> List[GoogleHomeDevice]: """Retrieves devices from glocaltokens and fetches alarm/timer data from each of the device""" devices = await self.get_google_devices() # Gives the user a warning if the device is offline for device in devices: if not device.ip_address and device.available: device.available = False _LOGGER.debug( ("Failed to fetch timers/alarms information " "from device %s. We could not determine it's IP address, " "the device is either offline or is not compatible " "Google Home device. Will try again later."), device.name, ) coordinator_data = await asyncio.gather(*[ self.collect_data_from_endpoints( device=device, ip_address=device.ip_address, auth_token=device.auth_token, ) for device in devices if device.ip_address and device.auth_token ]) return coordinator_data async def delete_alarm_or_timer(self, device: GoogleHomeDevice, item_to_delete: str) -> None: """Deletes a timer or alarm. Can also delete multiple if a list is provided (Not implemented yet).""" data = {"ids": [item_to_delete]} item_type = item_to_delete.split("/")[0] _LOGGER.debug( "Deleting %s from Google Home device %s - Raw data: %s", item_type, device.name, data, ) response = await self.post(endpoint=API_ENDPOINT_DELETE, data=data, device=device) if response: if "success" in response: if response["success"]: _LOGGER.debug( "Successfully deleted %s for %s", item_type, device.name, ) else: _LOGGER.error( "Couldn't delete %s for %s - %s", item_type, device.name, response, ) else: _LOGGER.error( ("Failed to get a confirmation that the %s" "was deleted for device %s. " "Received = %s"), item_type, device.name, response, ) async def reboot_google_device(self, device: GoogleHomeDevice) -> None: """Reboots a Google Home device if it supports this.""" # "now" means reboot and "fdr" means factory reset (Not implemented). data = {"params": "now"} _LOGGER.debug( "Trying to reboot Google Home device %s", device.name, ) response = await self.post(endpoint=API_ENDPOINT_REBOOT, data=data, device=device) if response: # It will return true even if the device does not support rebooting. _LOGGER.info( "Successfully asked %s to reboot.", device.name, ) async def update_do_not_disturb( self, device: GoogleHomeDevice, enable: Optional[bool] = None) -> GoogleHomeDevice: """Gets or sets the do not disturb setting on a Google Home device.""" data = None if enable is None: _LOGGER.debug( "Getting Do Not Disturb setting from Google Home device %s", device.name, ) else: data = {"notifications_enabled": not enable} _LOGGER.debug( "Setting Do Not Disturb setting to %s on Google Home device %s", not enable, device.name, ) response = await self.post(endpoint=API_ENDPOINT_DO_NOT_DISTURB, data=data, device=device) if response: if "notifications_enabled" in response: notifications_enabled = bool(response["notifications_enabled"]) _LOGGER.debug( "Received Do Not Disturb setting from Google Home device %s" " - Enabled: %s", device.name, not notifications_enabled, ) device.set_do_not_disturb(not notifications_enabled) else: _LOGGER.debug( "Response not expected from Google Home device %s - %s", device.name, response, ) return device async def post(self, endpoint: str, data: Optional[JsonDict], device: GoogleHomeDevice) -> Optional[Dict[str, str]]: """Shared post request""" if device.ip_address is None: _LOGGER.warning("Device %s doesn't have an IP address!", device.name) return None if device.auth_token is None: _LOGGER.warning("Device %s doesn't have an auth token!", device.name) return None url = self.create_url(device.ip_address, PORT, endpoint) HEADERS[HEADER_CAST_LOCAL_AUTH] = device.auth_token _LOGGER.debug( "Requesting endpoint %s for Google Home device %s - %s", endpoint, device.name, url, ) resp = None try: async with self._session.post(url, data=data, headers=HEADERS, timeout=TIMEOUT) as response: if response.status == HTTP_OK: try: resp = await response.json() except ContentTypeError: resp = True elif response.status == HTTP_NOT_FOUND: _LOGGER.debug( ("Failed to post data to %s, API returned %d. " "The device(hardware='%s') is possibly not Google Home " "compatible and has no alarms/timers. " "Will retry later."), device.name, response.status, device.hardware, ) else: _LOGGER.error( "Failed to access %s, API returned" " %d: %s", device.name, response.status, response, ) except ClientConnectorError: _LOGGER.warning( "Failed to connect to %s device. The device is probably offline.", device.name, ) except ClientError as ex: # Make sure that we log the exception from the client if one occurred. _LOGGER.error( "Request error: %s", ex, ) except asyncio.TimeoutError: _LOGGER.debug( "%s device timed out while trying to post data to it - Raw data: %s", device.name, data, ) return resp
class GlocaltokensApiClient: """API client""" def __init__( self, hass: HomeAssistant, session: ClientSession, username: str | None = None, password: str | None = None, master_token: str | None = None, android_id: str | None = None, zeroconf_instance: Zeroconf | None = None, ): """Sample API Client.""" self.hass = hass self._username = username self._password = password self._session = session self._android_id = android_id verbose = _LOGGER.level == logging.DEBUG self._client = GLocalAuthenticationTokens( username=username, password=password, master_token=master_token, android_id=android_id, verbose=verbose, ) self.google_devices: list[GoogleHomeDevice] = [] self.zeroconf_instance = zeroconf_instance async def async_get_master_token(self) -> str: """Get master API token""" def _get_master_token() -> str | None: return self._client.get_master_token() master_token = await self.hass.async_add_executor_job(_get_master_token ) if master_token is None or is_aas_et(master_token) is False: raise InvalidMasterToken return master_token async def get_google_devices(self) -> list[GoogleHomeDevice]: """Get google device authentication tokens. Note this method will fetch necessary access tokens if missing""" if not self.google_devices: def _get_google_devices() -> list[Device]: return self._client.get_google_devices( zeroconf_instance=self.zeroconf_instance, force_homegraph_reload=True, ) google_devices = await self.hass.async_add_executor_job( _get_google_devices) self.google_devices = [ GoogleHomeDevice( device_id=device.device_id, name=device.device_name, auth_token=device.local_auth_token, ip_address=device.ip_address, hardware=device.hardware, ) for device in google_devices ] return self.google_devices async def get_android_id(self) -> str: """Generate random android_id""" def _get_android_id() -> str: return self._client.get_android_id() return await self.hass.async_add_executor_job(_get_android_id) @staticmethod def create_url(ip_address: str, port: int, api_endpoint: str) -> str: """Creates url to endpoint. Note: port argument is unused because all request must be done to 8443""" return f"https://{ip_address}:{port}/{api_endpoint}" async def update_google_devices_information( self) -> list[GoogleHomeDevice]: """Retrieves devices from glocaltokens and fetches alarm/timer data from each of the device""" devices = await self.get_google_devices() # Gives the user a warning if the device is offline for device in devices: if not device.ip_address and device.available: device.available = False _LOGGER.debug( ("Failed to fetch timers/alarms information " "from device %s. We could not determine its IP address, " "the device is either offline or is not compatible " "Google Home device. Will try again later."), device.name, ) coordinator_data = await asyncio.gather(*[ self.collect_data_from_endpoints(device) for device in devices if device.ip_address and device.auth_token ]) return coordinator_data async def collect_data_from_endpoints( self, device: GoogleHomeDevice) -> GoogleHomeDevice: """Collect data from different endpoints.""" device = await self.update_alarms_and_timers(device) device = await self.update_alarm_volume(device) device = await self.update_do_not_disturb(device) return device async def update_alarms_and_timers( self, device: GoogleHomeDevice) -> GoogleHomeDevice: """Fetches timers and alarms from google device""" response = await self.request(method="GET", endpoint=API_ENDPOINT_ALARMS, device=device, polling=True) if response is not None: if JSON_TIMER in response and JSON_ALARM in response: device.set_timers( cast(List[TimerJsonDict], response[JSON_TIMER])) device.set_alarms( cast(List[AlarmJsonDict], response[JSON_ALARM])) _LOGGER.debug( "Successfully retrieved alarms and timers from %s. Response: %s", device.name, response, ) else: _LOGGER.error( ("Failed to parse fetched alarms and timers for device %s - " "API returned unknown json structure. " "Received = %s"), device.name, response, ) return device async def delete_alarm_or_timer(self, device: GoogleHomeDevice, item_to_delete: str) -> None: """Deletes a timer or alarm. Can also delete multiple if a list is provided (Not implemented yet).""" data = {"ids": [item_to_delete]} item_type = item_to_delete.split("/")[0] _LOGGER.debug( "Deleting %s from Google Home device %s - Raw data: %s", item_type, device.name, data, ) response = await self.request(method="POST", endpoint=API_ENDPOINT_ALARM_DELETE, device=device, data=data) if response is not None: if "success" in response: if response["success"]: _LOGGER.debug( "Successfully deleted %s for %s", item_type, device.name, ) else: _LOGGER.error( "Couldn't delete %s for %s - %s", item_type, device.name, response, ) else: _LOGGER.error( ("Failed to get a confirmation that the %s" "was deleted for device %s. " "Received = %s"), item_type, device.name, response, ) async def reboot_google_device(self, device: GoogleHomeDevice) -> None: """Reboots a Google Home device if it supports this.""" # "now" means reboot and "fdr" means factory reset (Not implemented). data = {"params": "now"} _LOGGER.debug( "Trying to reboot Google Home device %s", device.name, ) response = await self.request(method="POST", endpoint=API_ENDPOINT_REBOOT, device=device, data=data) if response is not None: # It will return true even if the device does not support rebooting. _LOGGER.info( "Successfully asked %s to reboot.", device.name, ) async def update_do_not_disturb( self, device: GoogleHomeDevice, enable: bool | None = None) -> GoogleHomeDevice: """Gets or sets the do not disturb setting on a Google Home device.""" data = None polling = False if enable is not None: # Setting is inverted on device data = {JSON_NOTIFICATIONS_ENABLED: not enable} _LOGGER.debug( "Setting Do Not Disturb setting to %s on Google Home device %s", enable, device.name, ) else: polling = True _LOGGER.debug( "Getting Do Not Disturb setting from Google Home device %s", device.name, ) response = await self.request( method="POST", endpoint=API_ENDPOINT_DO_NOT_DISTURB, device=device, data=data, polling=polling, ) if response is not None: if JSON_NOTIFICATIONS_ENABLED in response: enabled = not bool(response[JSON_NOTIFICATIONS_ENABLED]) _LOGGER.debug( "Received Do Not Disturb setting from Google Home device %s" " - Enabled: %s", device.name, enabled, ) device.set_do_not_disturb(enabled) else: _LOGGER.debug( ("Unexpected response from Google Home device '%s' " "when fetching DND status - %s"), device.name, response, ) return device async def update_alarm_volume( self, device: GoogleHomeDevice, volume: int | None = None) -> GoogleHomeDevice: """Gets or sets the alarm volume setting on a Google Home device.""" data: JsonDict | None = None polling = False if volume is not None: # Setting is inverted on device volume_float = float(volume / 100) data = {JSON_ALARM_VOLUME: volume_float} _LOGGER.debug( "Setting alarm volume to %d(float=%f) on Google Home device %s", volume, volume_float, device.name, ) else: polling = True _LOGGER.debug( "Getting alarm volume from Google Home device %s", device.name, ) response = await self.request( method="POST", endpoint=API_ENDPOINT_ALARM_VOLUME, device=device, data=data, polling=polling, ) if response: if JSON_ALARM_VOLUME in response: if polling: volume_raw = str(response[JSON_ALARM_VOLUME]) volume_int = round(float(volume_raw) * 100) _LOGGER.debug( "Received alarm volume from Google Home device %s" " - Volume: %d(raw=%s)", device.name, volume_int, volume_raw, ) else: volume_int = volume # type: ignore _LOGGER.debug( "Successfully set alarm volume to %d " "on Google Home device %s", volume, device.name, ) device.set_alarm_volume(volume_int) else: _LOGGER.debug( ("Unexpected response from Google Home device '%s' " "when fetching alarm volume setting - %s"), device.name, response, ) return device async def request( self, method: Literal["GET", "POST"], endpoint: str, device: GoogleHomeDevice, data: JsonDict | None = None, polling: bool = False, ) -> JsonDict | None: """Shared request method""" if device.ip_address is None: _LOGGER.warning("Device %s doesn't have an IP address!", device.name) return None if device.auth_token is None: _LOGGER.warning("Device %s doesn't have an auth token!", device.name) return None url = self.create_url(device.ip_address, PORT, endpoint) headers: dict[str, str] = { HEADER_CAST_LOCAL_AUTH: device.auth_token, HEADER_CONTENT_TYPE: "application/json", } _LOGGER.debug( "Requesting endpoint %s for Google Home device %s - %s", endpoint, device.name, url, ) resp = None try: async with self._session.request(method, url, json=data, headers=headers, timeout=TIMEOUT) as response: if response.status == HTTPStatus.OK: try: resp = await response.json() except ContentTypeError: resp = {} device.available = True elif response.status == HTTPStatus.UNAUTHORIZED: # If token is invalid - force reload homegraph providing new token # and rerun the task. if polling: _LOGGER.debug( ("Failed to fetch data from %s due to invalid token. " "Will refresh the token and try again."), device.name, ) else: _LOGGER.warning( "Failed to send the request to %s due to invalid token. " "Token will be refreshed, please try again later.", device.name, ) # We need to retry the update task instead of just cleaning the list self.google_devices = [] device.available = False elif response.status == HTTPStatus.NOT_FOUND: _LOGGER.debug( ("Failed to perform request to %s, API returned %d. " "The device(hardware='%s') is possibly not Google Home " "compatible and has no alarms/timers. " "Will retry later."), device.name, response.status, device.hardware, ) device.available = False else: _LOGGER.error( "Failed to access %s, API returned" " %d: %s", device.name, response.status, response, ) device.available = False except ClientConnectorError: logger_func = _LOGGER.debug if polling else _LOGGER.warning logger_func( "Failed to connect to %s device. The device is probably offline.", device.name, ) device.available = False except ClientError as ex: # Make sure that we log the exception from the client if one occurred. _LOGGER.error( "Request from %s device error: %s", device.name, ex, ) device.available = False except asyncio.TimeoutError: _LOGGER.debug( "%s device timed out while performing a request to it - Raw data: %s", device.name, data, ) device.available = False return resp
class GlocaltokensApiClient: """API client""" def __init__( self, hass: HomeAssistant, session: ClientSession, username: Optional[str] = None, password: Optional[str] = None, master_token: Optional[str] = None, android_id: Optional[str] = None, zeroconf_instance: Optional[Zeroconf] = None, ): """Sample API Client.""" self.hass = hass self._username = username self._password = password self._session = session self._android_id = android_id verbose = _LOGGER.level == logging.DEBUG self._client = GLocalAuthenticationTokens( username=username, password=password, master_token=master_token, android_id=android_id, verbose=verbose, ) self.google_devices: List[GoogleHomeDevice] = [] self.zeroconf_instance = zeroconf_instance async def async_get_master_token(self) -> str: """Get master API token""" def _get_master_token() -> Optional[str]: return self._client.get_master_token() master_token = await self.hass.async_add_executor_job(_get_master_token ) if master_token is None or is_aas_et(master_token) is False: raise InvalidMasterToken return master_token async def get_google_devices(self) -> List[GoogleHomeDevice]: """Get google device authentication tokens. Note this method will fetch necessary access tokens if missing""" if not self.google_devices: def _get_google_devices() -> List[Device]: return self._client.get_google_devices( zeroconf_instance=self.zeroconf_instance, force_homegraph_reload=True, ) google_devices = await self.hass.async_add_executor_job( _get_google_devices) self.google_devices = [ GoogleHomeDevice( name=device.device_name, auth_token=device.local_auth_token, ip_address=device.ip, hardware=device.hardware, ) for device in google_devices ] return self.google_devices async def get_android_id(self) -> Optional[str]: """Generate random android_id""" def _get_android_id() -> Optional[str]: return self._client.get_android_id() return await self.hass.async_add_executor_job(_get_android_id) @staticmethod def create_url(ip_address: str, port: int, api_endpoint: str) -> str: """Creates url to endpoint. Note: port argument is unused because all request must be done to 8443""" url = "https://{ip_address}:{port}/{endpoint}".format( ip_address=ip_address, port=str(port), endpoint=api_endpoint) return url async def get_alarms_and_timers(self, device: GoogleHomeDevice, ip_address: str, auth_token: str) -> GoogleHomeDevice: """Fetches timers and alarms from google device""" url = self.create_url(ip_address, PORT, API_ENDPOINT_ALARMS) _LOGGER.debug( "Fetching data from Google Home device %s - %s", device.name, url, ) HEADERS[HEADER_CAST_LOCAL_AUTH] = auth_token resp = None try: async with self._session.get(url, headers=HEADERS, timeout=TIMEOUT) as response: if response.status == HTTP_OK: resp = await response.json() device.available = True if resp: if JSON_TIMER in resp or JSON_ALARM in resp: device.set_timers(resp.get(JSON_TIMER)) device.set_alarms(resp.get(JSON_ALARM)) else: _LOGGER.error( ("Failed to parse fetched data for device %s - %s. " "Received = %s"), device.name, API_RETURNED_UNKNOWN, resp, ) elif response.status == HTTP_UNAUTHORIZED: # If token is invalid - force reload homegraph providing new token # and rerun the task. _LOGGER.debug( ("Failed to fetch data from %s due to invalid token. " "Will refresh the token and try again."), device.name, ) # We need to retry the update task instead of just cleaning the list self.google_devices = [] device.available = False elif response.status == HTTP_NOT_FOUND: _LOGGER.debug( ("Failed to fetch data from %s, API returned %d. " "The device(hardware='%s') is possibly not Google Home " "compatable and has no alarms/timers. " "Will retry later."), device.name, response.status, device.hardware, ) device.available = False else: _LOGGER.error( "Failed to fetch %s data, API returned %d: %s", device.name, response.status, response, ) device.available = False except ClientConnectorError: _LOGGER.debug( ("Failed to connect to %s device. " "The device is probably offline. Will retry later."), device.name, ) device.available = False except ClientError as ex: # Make sure that we log the exception if one occurred. # The only reason we do this broad is so we easily can # debug the application. _LOGGER.error( "Request error: %s", ex, ) device.available = False return device async def update_google_devices_information( self) -> List[GoogleHomeDevice]: """Retrieves devices from glocaltokens and fetches alarm/timer data from each of the device""" devices = await self.get_google_devices() # Gives the user a warning if the device is offline for device in devices: if not device.ip_address and device.available: device.available = False _LOGGER.debug( ("Failed to fetch timers/alarms information " "from device %s. We could not determine it's IP address, " "the device is either offline or is not compatable " "Google Home device. Will try again later."), device.name, ) coordinator_data = await gather(*[ self.get_alarms_and_timers(device, device.ip_address, device.auth_token) for device in devices if device.ip_address and device.auth_token ]) return coordinator_data
class GLocalAuthenticationTokensClientTests(DeviceAssertions, TypeAssertions, TestCase): def setUp(self): """Setup method run before every test""" self.client = GLocalAuthenticationTokens(username=faker.word(), password=faker.word()) def tearDown(self): """Teardown method run after every test""" pass def test_initialization(self): username = faker.word() password = faker.word() master_token = faker.master_token() android_id = faker.word() client = GLocalAuthenticationTokens( username=username, password=password, master_token=master_token, android_id=android_id, ) self.assertEqual(username, client.username) self.assertEqual(password, client.password) self.assertEqual(master_token, client.master_token) self.assertEqual(android_id, client.android_id) self.assertIsString(client.username) self.assertIsString(client.password) self.assertIsString(client.master_token) self.assertIsString(client.android_id) self.assertIsNone(client.access_token) self.assertIsNone(client.homegraph) self.assertIsNone(client.access_token_date) self.assertIsNone(client.homegraph_date) self.assertIsAASET(client.master_token) @patch("glocaltokens.client.LOGGER.error") def test_initialization__valid(self, m_log): # With username and password GLocalAuthenticationTokens(username=faker.word(), password=faker.word()) self.assertEqual(m_log.call_count, 0) # With master_token GLocalAuthenticationTokens(master_token=faker.master_token()) self.assertEqual(m_log.call_count, 0) @patch("glocaltokens.client.LOGGER.setLevel") def test_initialization__valid_verbose_logger(self, m_set_level): # Non verbose GLocalAuthenticationTokens(username=faker.word(), password=faker.word()) self.assertEqual(m_set_level.call_count, 0) # Verbose GLocalAuthenticationTokens(username=faker.word(), password=faker.word(), verbose=True) m_set_level.assert_called_once_with(logging.DEBUG) @patch("glocaltokens.client.LOGGER.error") def test_initialization__invalid(self, m_log): # Without username GLocalAuthenticationTokens(password=faker.word()) self.assertEqual(m_log.call_count, 1) # Without password GLocalAuthenticationTokens(username=faker.word()) self.assertEqual(m_log.call_count, 2) # Without username and password GLocalAuthenticationTokens() self.assertEqual(m_log.call_count, 3) # With invalid master_token GLocalAuthenticationTokens(master_token=faker.word()) self.assertEqual(m_log.call_count, 4) def test_get_android_id(self): android_id = self.client.get_android_id() self.assertTrue(len(android_id) == ANDROID_ID_LENGTH) self.assertIsString(android_id) # Make sure we get the same ID when called further self.assertEqual(android_id, self.client.get_android_id()) def test_generate_mac_string(self): mac_string = GLocalAuthenticationTokens._generate_mac_string() self.assertTrue(len(mac_string) == ANDROID_ID_LENGTH) # Make sure we get different generated mac string self.assertNotEqual(mac_string, GLocalAuthenticationTokens._generate_mac_string()) def test_has_expired(self): duration_sec = 60 now = datetime.now() token_dt__expired = now - timedelta(seconds=duration_sec + 1) token_dt__non_expired = now - timedelta(seconds=duration_sec - 1) # Expired self.assertTrue( GLocalAuthenticationTokens._has_expired(token_dt__expired, duration_sec)) # Non expired self.assertFalse( GLocalAuthenticationTokens._has_expired(token_dt__non_expired, duration_sec)) @patch("glocaltokens.client.LOGGER.error") @patch("glocaltokens.client.perform_master_login") def test_get_master_token(self, m_perform_master_login, m_log): # No token in response self.assertIsNone(self.client.get_master_token()) m_perform_master_login.assert_called_once_with( self.client.username, self.client.password, self.client.get_android_id()) self.assertEqual(m_log.call_count, 1) # Reset mocks m_perform_master_login.reset_mock() m_log.reset_mock() # With token in response expected_master_token = faker.master_token() m_perform_master_login.return_value = {"Token": expected_master_token} master_token = self.client.get_master_token() m_perform_master_login.assert_called_once_with( self.client.username, self.client.password, self.client.get_android_id()) self.assertEqual(expected_master_token, master_token) self.assertEqual(m_log.call_count, 0) # Another request - must return the same token all the time master_token = self.client.get_master_token() self.assertEqual(expected_master_token, master_token) @patch("glocaltokens.client.LOGGER.error") @patch("glocaltokens.client.perform_master_login") @patch("glocaltokens.client.perform_oauth") def test_get_access_token(self, m_perform_oauth, m_get_master_token, m_log): master_token = faker.master_token() m_get_master_token.return_value = {"Token": master_token} # No token in response self.assertIsNone(self.client.get_access_token()) m_perform_oauth.assert_called_once_with( self.client.username, master_token, self.client.get_android_id(), app=ACCESS_TOKEN_APP_NAME, service=ACCESS_TOKEN_SERVICE, client_sig=ACCESS_TOKEN_CLIENT_SIGNATURE, ) self.assertEqual(m_log.call_count, 1) # Reset mocks m_perform_oauth.reset_mock() m_log.reset_mock() # With token in response expected_access_token = faker.access_token() m_perform_oauth.return_value = {"Auth": expected_access_token} access_token = self.client.get_access_token() m_perform_oauth.assert_called_once_with( self.client.username, master_token, self.client.get_android_id(), app=ACCESS_TOKEN_APP_NAME, service=ACCESS_TOKEN_SERVICE, client_sig=ACCESS_TOKEN_CLIENT_SIGNATURE, ) self.assertEqual(expected_access_token, access_token) self.assertEqual(m_log.call_count, 0) # Reset mocks m_perform_oauth.reset_mock() m_log.reset_mock() # Another request with non expired token must return the same token # (no new requests) access_token = self.client.get_access_token() self.assertEqual(expected_access_token, access_token) self.assertEqual(m_perform_oauth.call_count, 0) # Another request with expired token must return new token (new request) self.client.access_token_date = self.client.access_token_date - timedelta( ACCESS_TOKEN_DURATION + 1) access_token = self.client.get_access_token() self.assertEqual(m_perform_oauth.call_count, 1) @patch("glocaltokens.client.grpc.ssl_channel_credentials") @patch("glocaltokens.client.grpc.access_token_call_credentials") @patch("glocaltokens.client.grpc.composite_channel_credentials") @patch("glocaltokens.client.grpc.secure_channel") @patch("glocaltokens.client.v1_pb2_grpc.StructuresServiceStub") @patch("glocaltokens.client.v1_pb2.GetHomeGraphRequest") @patch("glocaltokens.client.GLocalAuthenticationTokens.get_access_token") def test_get_homegraph( self, m_get_access_token, m_get_home_graph_request, m_structure_service_stub, m_secure_channel, m_composite_channel_credentials, m_access_token_call_credentials, m_ssl_channel_credentials, ): # New homegraph self.client.get_homegraph() self.assertEqual(m_ssl_channel_credentials.call_count, 1) self.assertEqual(m_access_token_call_credentials.call_count, 1) self.assertEqual(m_composite_channel_credentials.call_count, 1) self.assertEqual(m_secure_channel.call_count, 1) self.assertEqual(m_structure_service_stub.call_count, 1) self.assertEqual(m_get_home_graph_request.call_count, 1) # Another request with non expired homegraph must return the same homegraph # (no new requests) self.client.get_homegraph() self.assertEqual(m_ssl_channel_credentials.call_count, 1) self.assertEqual(m_access_token_call_credentials.call_count, 1) self.assertEqual(m_composite_channel_credentials.call_count, 1) self.assertEqual(m_secure_channel.call_count, 1) self.assertEqual(m_structure_service_stub.call_count, 1) self.assertEqual(m_get_home_graph_request.call_count, 1) # Expired homegraph self.client.homegraph_date = self.client.homegraph_date - timedelta( HOMEGRAPH_DURATION + 1) self.client.get_homegraph() self.assertEqual(m_ssl_channel_credentials.call_count, 2) self.assertEqual(m_access_token_call_credentials.call_count, 2) self.assertEqual(m_composite_channel_credentials.call_count, 2) self.assertEqual(m_secure_channel.call_count, 2) self.assertEqual(m_structure_service_stub.call_count, 2) self.assertEqual(m_get_home_graph_request.call_count, 2) @patch("glocaltokens.client.GLocalAuthenticationTokens.get_homegraph") def test_get_google_devices(self, m_get_homegraph): # With just one device returned from homegraph homegraph_device = faker.homegraph_device() m_get_homegraph.return_value.home.devices = [homegraph_device] # With no discover_devices, with no model_list google_devices = self.client.get_google_devices(disable_discovery=True) self.assertEqual(len(google_devices), 1) google_device = google_devices[0] self.assertEqual(type(google_device), Device) self.assertDevice(google_device, homegraph_device) # With two devices returned from homegraph # but one device having the invalid token homegraph_device_valid = faker.homegraph_device() homegraph_device_invalid = faker.homegraph_device() homegraph_device_invalid.local_auth_token = ( faker.word()) # setting invalid token intentionally # Note that we initialize the list with homegraph_device_invalid # which should be ignored m_get_homegraph.return_value.home.devices = [ homegraph_device_invalid, homegraph_device_valid, ] google_devices = self.client.get_google_devices(disable_discovery=True) self.assertEqual(len(google_devices), 1) self.assertDevice(google_devices[0], homegraph_device_valid) @patch("glocaltokens.client.GLocalAuthenticationTokens.get_google_devices") def test_get_google_devices_json(self, m_get_google_devices): device_name = faker.word() local_auth_token = faker.local_auth_token() ip = faker.ipv4() port = faker.port_number() hardware = faker.word() google_device = Device( device_name=device_name, local_auth_token=local_auth_token, ip=ip, port=port, hardware=hardware, ) m_get_google_devices.return_value = [google_device] json_string = self.client.get_google_devices_json( disable_discovery=True) self.assertEqual(m_get_google_devices.call_count, 1) self.assertIsString(json_string) received_json = json.loads(json_string) received_device = received_json[0] self.assertEqual(received_device[JSON_KEY_DEVICE_NAME], device_name) self.assertEqual(received_device[JSON_KEY_HARDWARE], hardware) self.assertEqual(received_device[JSON_KEY_LOCAL_AUTH_TOKEN], local_auth_token) self.assertEqual( received_device[JSON_KEY_GOOGLE_DEVICE][JSON_KEY_PORT], port) self.assertEqual(received_device[JSON_KEY_GOOGLE_DEVICE][JSON_KEY_IP], ip)