def master_auth(): config = configparser.ConfigParser() try: if not os.path.exists(os.path.join(expanduser("~"), ".ghome-config")): username = input( "Google Username associated with Google Home Devices: ") password = getpass.getpass("Enter your password: "******"~"), ".ghome-config"), "w") as cfgfile: config.add_section("ghome") config.set("ghome", "master_token", mtoken) config.write(cfgfile) cfgfile.close() elif os.path.exists(os.path.join(expanduser("~"), ".ghome-config")): config.read(os.path.join(expanduser("~"), ".ghome-config")) mtoken = config["ghome"]["master_token"] return mtoken except Exception as e: print(e)
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: 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 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
It's safer/easier to generate an app password and use it instead of the actual password. It still has the same access as the regular password, but still better than using the real password while scripting. (https://myaccount.google.com/apppasswords) """ ) # Using google username and password client = GLocalAuthenticationTokens( username=GOOGLE_USERNAME, password=GOOGLE_PASSWORD, master_token=GOOGLE_MASTER_TOKEN, android_id=DEVICE_ID, verbose=True, ) # Get master token print("[*] Master token", client.get_master_token()) # Get access token (lives 1 hour) print("\n[*] Access token (lives 1 hour)", client.get_access_token()) # Get google device local authentication tokens (live about 1 day) print("\n[*] Google devices local authentication tokens") google_devices = client.get_google_devices_json() # Pretty print json data google_devices_str = json.dumps(google_devices, indent=2) print("[*] Google devices", google_devices_str)
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)