コード例 #1
0
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)
コード例 #2
0
ファイル: api.py プロジェクト: KapJI/ha-google-home
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
コード例 #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
コード例 #4
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
コード例 #5
0
    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)
コード例 #6
0
ファイル: test_client.py プロジェクト: samapriya/glocaltokens
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)