Exemplo n.º 1
0
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
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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)