-
-
Notifications
You must be signed in to change notification settings - Fork 30.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add microBees integration #99573
Merged
Merged
Add microBees integration #99573
Changes from 89 commits
Commits
Show all changes
93 commits
Select commit
Hold shift + click to select a range
aed16d1
Create a new homeassistan integration for microBees
FedDam 65df20e
black --fast homeassistant tests
FedDam 3d05e43
Switch platform
FedDam 94a3c7d
rename folder
FedDam 9f232cc
rename folder
FedDam e756caa
Update owners
marcolettieri 070c583
Merge branch 'dev' into dev
marcolettieri 96231b7
aiohttp removed in favor of hass
FedDam 9a763b1
Merge branch 'dev' of https://github.com/microBeesTech/homeassistant …
FedDam bd9d2db
Merge branch 'dev' into dev
marcolettieri 217a106
Update config_flow.py
FedDam 6b354d1
Update __init__.py
FedDam 8c3e048
Update const.py
FedDam 897b63c
Update manifest.json
FedDam 5df472b
Update string.json
FedDam 8d73ff1
Update servicesMicrobees.py
FedDam ff33f00
Update switch.py
FedDam 08a6d28
Update __init__.py
FedDam 54ca8a6
Update it.json
FedDam 5f5b06d
Merge branch 'home-assistant:dev' into dev
FedDam 9c605b4
Create a new homeassistan integration for microBees
FedDam f61bfaf
black --fast homeassistant tests
FedDam 7a81a2b
Switch platform
FedDam 83fdbd5
rename folder
FedDam 90fc8cf
rename folder
FedDam f5517f3
Update owners
marcolettieri 0451f5d
aiohttp removed in favor of hass
FedDam e5d5b8b
Update config_flow.py
FedDam 1ef2b85
Update __init__.py
FedDam f98d0b9
Update const.py
FedDam 6588370
Update manifest.json
FedDam 7cdb7ee
Update string.json
FedDam 0df28fc
Update servicesMicrobees.py
FedDam dd5f294
Update switch.py
FedDam dd7cee8
Update __init__.py
FedDam 7a8207d
Update it.json
FedDam 48903b6
Merge branch 'dev' of https://github.com/microBeesTech/homeassistant …
FedDam 630311d
Merge branch 'home-assistant:dev' into dev
FedDam 49bd1d1
Merge branch 'home-assistant:dev' into dev
FedDam 33edd81
Merge branch 'home-assistant:dev' into dev
FedDam fe1cdb5
Merge branch 'home-assistant:dev' into dev
marcolettieri 19e050c
Merge branch 'dev' into dev
FedDam 657da9e
Merge branch 'home-assistant:dev' into dev
FedDam fbb8f90
Merge branch 'dev' into dev
FedDam 5fad65f
Merge branch 'dev' into dev
FedDam 4457298
fixes review
FedDam 61dc170
Merge branch 'dev' into dev
FedDam 6706dc0
Merge branch 'dev' into dev
FedDam 1fc1464
Merge branch 'home-assistant:dev' into dev
FedDam 5fde300
fixes review
FedDam b5d871a
fixes review
FedDam e613f8a
pyproject.toml
FedDam ecc1c57
Update package_constraints.txt
FedDam bf65a12
fixes review
FedDam 01cab63
Merge remote-tracking branch 'refs/remotes/origin/microbees' into mic…
FedDam 3c98298
Merge pull request #1 from microBeesTech/microbees
marcolettieri d48374c
bug fixes
FedDam 204cc99
bug fixes
FedDam b85414c
delete microbees connector
FedDam 1230bb9
add other productID in switch
FedDam 25af133
added coordinator and enanchments
FedDam 7a22440
added coordinator and enanchments
FedDam 18f064a
fixes from suggestions
FedDam 6c34e92
fixes from suggestions
FedDam 07dcb7b
fixes from suggestions
FedDam 46fa844
fixes from suggestions
FedDam 32c2daa
fixes from suggestions
FedDam ddd04a2
fixes from suggestions
FedDam 2194566
fixes from suggestions
FedDam 6904193
fixes from suggestions
FedDam 255f6b8
fixes from suggestions
FedDam 1cf3d9f
fixes from suggestions
FedDam 4e3138f
fixes from suggestions
FedDam 934c007
Merge branch 'dev' into dev
marcolettieri 84db301
fixes from suggestions
FedDam dc531b9
add test
FedDam 0ead78f
add test
FedDam 79bb9e7
add test
FedDam 67b1445
add test
FedDam 250c45c
requested commit
FedDam 168e685
requested commit
FedDam bf26a4e
requested commit
FedDam 9d9cfb8
requested commit
FedDam c91ce62
reverting .strict-typing and added microbees to .coveragerc
FedDam 5814212
remove log
FedDam b420c13
remove log
FedDam 0c0536e
remove log
FedDam cbaca33
remove log
FedDam 604bc6a
add test for microbeesExeption and Exeption
FedDam b03e217
add test for microbeesExeption and Exeption
FedDam 3b2aa14
add test for microbeesException and Exception
FedDam 2db964b
add test for microbeesException and Exception
FedDam c952972
add test for microbeesException and Exception
FedDam File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
"""The microBees integration.""" | ||
|
||
from dataclasses import dataclass | ||
from http import HTTPStatus | ||
|
||
import aiohttp | ||
from microBeesPy.microbees import MicroBees | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_ACCESS_TOKEN | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady | ||
from homeassistant.helpers import config_entry_oauth2_flow | ||
|
||
from .const import DOMAIN, PLATFORMS | ||
from .coordinator import MicroBeesUpdateCoordinator | ||
|
||
|
||
@dataclass(frozen=True, kw_only=True) | ||
class HomeAssistantMicroBeesData: | ||
"""Microbees data stored in the Home Assistant data object.""" | ||
|
||
connector: MicroBees | ||
coordinator: MicroBeesUpdateCoordinator | ||
session: config_entry_oauth2_flow.OAuth2Session | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up microBees from a config entry.""" | ||
implementation = ( | ||
await config_entry_oauth2_flow.async_get_config_entry_implementation( | ||
hass, entry | ||
) | ||
) | ||
|
||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) | ||
try: | ||
await session.async_ensure_token_valid() | ||
except aiohttp.ClientResponseError as ex: | ||
if ex.status in ( | ||
HTTPStatus.BAD_REQUEST, | ||
HTTPStatus.UNAUTHORIZED, | ||
HTTPStatus.FORBIDDEN, | ||
): | ||
raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex | ||
raise ConfigEntryNotReady from ex | ||
microbees = MicroBees(token=session.token[CONF_ACCESS_TOKEN]) | ||
coordinator = MicroBeesUpdateCoordinator(hass, microbees) | ||
await coordinator.async_config_entry_first_refresh() | ||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantMicroBeesData( | ||
connector=microbees, | ||
coordinator=coordinator, | ||
session=session, | ||
) | ||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Unload a config entry.""" | ||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): | ||
hass.data[DOMAIN].pop(entry.entry_id) | ||
|
||
return unload_ok |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
"""API for microBees bound to Home Assistant OAuth.""" | ||
|
||
from homeassistant.const import CONF_ACCESS_TOKEN | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers import config_entry_oauth2_flow | ||
|
||
|
||
class ConfigEntryAuth: | ||
"""Provide microBees authentication tied to an OAuth2 based config entry.""" | ||
|
||
def __init__( | ||
self, | ||
hass: HomeAssistant, | ||
oauth2_session: config_entry_oauth2_flow.OAuth2Session, | ||
) -> None: | ||
"""Initialize microBees Auth.""" | ||
self.oauth_session = oauth2_session | ||
self.hass = hass | ||
|
||
@property | ||
def access_token(self) -> str: | ||
"""Return the access token.""" | ||
return self.oauth_session.token[CONF_ACCESS_TOKEN] | ||
|
||
async def check_and_refresh_token(self) -> str: | ||
"""Check the token.""" | ||
await self.oauth_session.async_ensure_token_valid() | ||
return self.access_token |
14 changes: 14 additions & 0 deletions
14
homeassistant/components/microbees/application_credentials.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
"""application_credentials platform the microBees integration.""" | ||
|
||
from homeassistant.components.application_credentials import AuthorizationServer | ||
from homeassistant.core import HomeAssistant | ||
|
||
from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN | ||
|
||
|
||
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: | ||
"""Return auth implementation.""" | ||
return AuthorizationServer( | ||
authorize_url=OAUTH2_AUTHORIZE, | ||
token_url=OAUTH2_TOKEN, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
"""Config flow for microBees integration.""" | ||
from collections.abc import Mapping | ||
import logging | ||
from typing import Any | ||
|
||
from microBeesPy.microbees import MicroBees, MicroBeesException | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN | ||
from homeassistant.data_entry_flow import FlowResult | ||
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow | ||
|
||
from .const import DOMAIN | ||
|
||
|
||
class OAuth2FlowHandler( | ||
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN | ||
): | ||
"""Handle a config flow for microBees.""" | ||
|
||
DOMAIN = DOMAIN | ||
reauth_entry: config_entries.ConfigEntry | None = None | ||
|
||
@property | ||
def logger(self) -> logging.Logger: | ||
"""Return logger.""" | ||
return logging.getLogger(__name__) | ||
|
||
@property | ||
def extra_authorize_data(self) -> dict[str, Any]: | ||
"""Extra data that needs to be appended to the authorize url.""" | ||
scopes = ["read", "write"] | ||
return {"scope": " ".join(scopes)} | ||
|
||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: | ||
"""Create an oauth config entry or update existing entry for reauth.""" | ||
|
||
microbees = MicroBees( | ||
session=aiohttp_client.async_get_clientsession(self.hass), | ||
token=data[CONF_TOKEN][CONF_ACCESS_TOKEN], | ||
) | ||
|
||
try: | ||
current_user = await microbees.getMyProfile() | ||
except MicroBeesException: | ||
return self.async_abort(reason="invalid_auth") | ||
except Exception: # pylint: disable=broad-except | ||
self.logger.exception("Unexpected error") | ||
return self.async_abort(reason="unknown") | ||
marcolettieri marked this conversation as resolved.
Show resolved
Hide resolved
marcolettieri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if not self.reauth_entry: | ||
await self.async_set_unique_id(current_user.id) | ||
marcolettieri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self._abort_if_unique_id_configured() | ||
return self.async_create_entry( | ||
title=current_user.username, | ||
data=data, | ||
) | ||
if self.reauth_entry.unique_id == current_user.id: | ||
self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) | ||
await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) | ||
return self.async_abort(reason="reauth_successful") | ||
return self.async_abort(reason="wrong_account") | ||
|
||
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: | ||
"""Perform reauth upon an API authentication error.""" | ||
self.reauth_entry = self.hass.config_entries.async_get_entry( | ||
self.context["entry_id"] | ||
) | ||
return await self.async_step_reauth_confirm() | ||
|
||
async def async_step_reauth_confirm( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> FlowResult: | ||
"""Confirm reauth dialog.""" | ||
if user_input is None: | ||
return self.async_show_form(step_id="reauth_confirm") | ||
return await self.async_step_user() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
"""Constants for the microBees integration.""" | ||
from homeassistant.const import Platform | ||
|
||
DOMAIN = "microbees" | ||
OAUTH2_AUTHORIZE = "https://dev.microbees.com/oauth/authorize" | ||
OAUTH2_TOKEN = "https://dev.microbees.com/oauth/token" | ||
PLATFORMS = [ | ||
Platform.SWITCH, | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
"""The microBees Coordinator.""" | ||
|
||
import asyncio | ||
from dataclasses import dataclass | ||
from datetime import timedelta | ||
from http import HTTPStatus | ||
import logging | ||
|
||
import aiohttp | ||
from microBeesPy.microbees import Actuator, Bee, MicroBees, MicroBeesException | ||
|
||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryAuthFailed | ||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
@dataclass | ||
class MicroBeesCoordinatorData: | ||
"""Microbees data from the Coordinator.""" | ||
|
||
bees: dict[int, Bee] | ||
actuators: dict[int, Actuator] | ||
|
||
|
||
class MicroBeesUpdateCoordinator(DataUpdateCoordinator[MicroBeesCoordinatorData]): | ||
"""MicroBees coordinator.""" | ||
|
||
def __init__(self, hass: HomeAssistant, microbees: MicroBees) -> None: | ||
"""Initialize microBees coordinator.""" | ||
super().__init__( | ||
hass, | ||
_LOGGER, | ||
name="microBees Coordinator", | ||
update_interval=timedelta(seconds=30), | ||
) | ||
self.microbees = microbees | ||
|
||
async def _async_update_data(self) -> MicroBeesCoordinatorData: | ||
"""Fetch data from API endpoint.""" | ||
async with asyncio.timeout(10): | ||
try: | ||
bees = await self.microbees.getBees() | ||
except aiohttp.ClientResponseError as err: | ||
if err.status is HTTPStatus.UNAUTHORIZED: | ||
raise ConfigEntryAuthFailed( | ||
"Token not valid, trigger renewal" | ||
) from err | ||
raise UpdateFailed(f"Error communicating with API: {err}") from err | ||
|
||
except MicroBeesException as err: | ||
raise UpdateFailed(f"Error communicating with API: {err}") from err | ||
|
||
bees_dict = {} | ||
actuators_dict = {} | ||
for bee in bees: | ||
bees_dict[bee.id] = bee | ||
for actuator in bee.actuators: | ||
actuators_dict[actuator.id] = actuator | ||
return MicroBeesCoordinatorData(bees=bees_dict, actuators=actuators_dict) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
"""Base entity for microBees.""" | ||
|
||
from microBeesPy.microbees import Actuator, Bee | ||
|
||
from homeassistant.helpers.device_registry import DeviceInfo | ||
from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
||
from .const import DOMAIN | ||
from .coordinator import MicroBeesUpdateCoordinator | ||
|
||
|
||
class MicroBeesEntity(CoordinatorEntity[MicroBeesUpdateCoordinator]): | ||
"""Base class for microBees entities.""" | ||
|
||
_attr_has_entity_name = True | ||
|
||
def __init__( | ||
self, | ||
coordinator: MicroBeesUpdateCoordinator, | ||
bee_id: int, | ||
actuator_id: int, | ||
) -> None: | ||
"""Initialize the microBees entity.""" | ||
super().__init__(coordinator) | ||
self.bee_id = bee_id | ||
self.actuator_id = actuator_id | ||
self._attr_unique_id = f"{bee_id}_{actuator_id}" | ||
self._attr_device_info = DeviceInfo( | ||
identifiers={(DOMAIN, str(bee_id))}, | ||
manufacturer="microBees", | ||
name=self.bee.name, | ||
model=self.bee.prototypeName, | ||
) | ||
|
||
@property | ||
def available(self) -> bool: | ||
"""Status of the bee.""" | ||
return ( | ||
super().available | ||
and self.bee_id in self.coordinator.data.bees | ||
and self.bee.active | ||
) | ||
|
||
@property | ||
def bee(self) -> Bee: | ||
"""Return the bee.""" | ||
return self.coordinator.data.bees[self.bee_id] | ||
|
||
@property | ||
def actuator(self) -> Actuator: | ||
"""Return the actuator.""" | ||
return self.coordinator.data.actuators[self.actuator_id] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"entity": { | ||
"switch": { | ||
"socket_eu": { | ||
"default": "mdi:power-socket-eu" | ||
}, | ||
"socket_it": { | ||
"default": "mdi:power-socket-it" | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"domain": "microbees", | ||
"name": "microBees", | ||
marcolettieri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"codeowners": ["@microBeesTech"], | ||
"config_flow": true, | ||
"dependencies": ["application_credentials"], | ||
"documentation": "https://www.home-assistant.io/integrations/microbees", | ||
"iot_class": "cloud_polling", | ||
"requirements": ["microBeesPy==0.2.5"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ | ||
"config": { | ||
"step": { | ||
"pick_implementation": { | ||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" | ||
} | ||
}, | ||
"error": { | ||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||
"unknown": "[%key:common::config_flow::error::unknown%]" | ||
}, | ||
"abort": { | ||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]", | ||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", | ||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", | ||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", | ||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", | ||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", | ||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", | ||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", | ||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", | ||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]" | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're missing strings for abort reasons for invalid_auth, unknown, and wrong_account. |
||
"create_entry": { | ||
"default": "[%key:common::config_flow::create_entry::authenticated%]" | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't used anywhere. The idea with this class is that it should inherit the library client class and help the library to refresh the access token since Home Assistant is in control of it instead of the library.
https://developers.home-assistant.io/docs/api_lib_auth#oauth2
The only time the token is refreshed currently is when the config entry is setup. So the user needs to reload the config entry to refresh the token. This is not how it's supposed to work.
Before every request the client should call async_get_access_token (check_and_refresh_token) to get a valid token.