-
-
Notifications
You must be signed in to change notification settings - Fork 37.4k
Add Eve Online integration #168143
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
Open
ronaldvdmeer
wants to merge
32
commits into
home-assistant:dev
Choose a base branch
from
ronaldvdmeer:feat/eveonline-core-sensors
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add Eve Online integration #168143
Changes from 2 commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
ce2dc73
Add Eve Online integration with sensor platform
ronaldvdmeer c611e21
Simplify Eve Online integration to Bronze quality level
ronaldvdmeer f9b156e
Address Copilot review: fix test timing and add PR links to quality s…
ronaldvdmeer 18a2f87
Fix ship sensor to show ship type name instead of player-assigned name
ronaldvdmeer e057b84
Mark config-entry-unloading as done in quality scale
ronaldvdmeer 1e52861
Fix quality_scale: mark integration-owner, entity-unavailable, log-wh…
ronaldvdmeer dc6b02a
Address Copilot review: rename test functions to reflect unknown state
ronaldvdmeer fdb811a
Address erwindouna review: parallel location+ship fetch, Implementati…
ronaldvdmeer 58e326a
Remove test_api.py and move auth failure test to test_init.py
ronaldvdmeer e8e3d4d
Remove single-use intermediate variables in async_setup_entry
ronaldvdmeer 1350694
Remove single-use variables and docstring in async_oauth_create_entry
ronaldvdmeer 9eafa74
Remove unrealistic 'Unknown' fallback for character name in JWT decode
ronaldvdmeer 7d12819
Replace SCAN_INTERVAL constant with inline timedelta(minutes=1)
ronaldvdmeer 379bee8
Remove helper coroutines in favour of direct asyncio.gather with shar…
ronaldvdmeer c669260
Merge branch 'dev' into feat/eveonline-core-sensors
ronaldvdmeer c4764db
Address erwindouna review: move type alias to top, add slots/kw_only …
ronaldvdmeer 0dd5fe5
Fix asyncio.gather: use return_exceptions=True to preserve independen…
ronaldvdmeer 1c93ddc
Fix mypy: use BaseException instead of Exception for return_exception…
ronaldvdmeer fa3ef85
Replace asyncio.gather+isinstance with sequential try/except per opti…
ronaldvdmeer 017a002
Address Copilot review: use character-specific coordinator name, catc…
ronaldvdmeer ddfb001
Fix ruff formatting: single line function call
ronaldvdmeer b34e169
Merge branch 'dev' into feat/eveonline-core-sensors
erwindouna c9a84db
Add TimeoutError to optional endpoint except clauses in coordinator
ronaldvdmeer 6271516
Use result variable consistently in multi-flow config flow tests
ronaldvdmeer fb26507
Use STATE_UNAVAILABLE constant in sensor test
ronaldvdmeer 0209c66
Use STATE_UNKNOWN constant in sensor tests
ronaldvdmeer c5bea38
Add model and configuration_url to DeviceInfo
ronaldvdmeer 8bc0bb0
Merge branch 'dev' into feat/eveonline-core-sensors
ronaldvdmeer d99bbbb
Fix quality_scale: mark test-before-configure as exempt
ronaldvdmeer bc00048
Merge branch 'dev' into feat/eveonline-core-sensors
ronaldvdmeer e5226ac
Merge branch 'dev' into feat/eveonline-core-sensors
ronaldvdmeer 50e32ce
Add Eve Online reauth flow
ronaldvdmeer 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
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or 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,45 @@ | ||
| """The Eve Online integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from eveonline import EveOnlineClient | ||
|
|
||
| from homeassistant.const import Platform | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.helpers import aiohttp_client | ||
| from homeassistant.helpers.config_entry_oauth2_flow import ( | ||
| OAuth2Session, | ||
| async_get_config_entry_implementation, | ||
| ) | ||
|
|
||
| from .api import AsyncConfigEntryAuth | ||
| from .const import CONF_CHARACTER_ID, CONF_CHARACTER_NAME | ||
| from .coordinator import EveOnlineConfigEntry, EveOnlineCoordinator | ||
|
|
||
| PLATFORMS: list[Platform] = [Platform.SENSOR] | ||
|
|
||
|
|
||
| async def async_setup_entry(hass: HomeAssistant, entry: EveOnlineConfigEntry) -> bool: | ||
| """Set up Eve Online from a config entry.""" | ||
| implementation = await async_get_config_entry_implementation(hass, entry) | ||
| session = OAuth2Session(hass, entry, implementation) | ||
| auth = AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session) | ||
|
ronaldvdmeer marked this conversation as resolved.
Outdated
|
||
| client = EveOnlineClient(auth=auth) | ||
|
|
||
| character_id: int = entry.data[CONF_CHARACTER_ID] | ||
| character_name: str = entry.data[CONF_CHARACTER_NAME] | ||
|
|
||
| coordinator = EveOnlineCoordinator( | ||
| hass, entry, client, character_id, character_name | ||
| ) | ||
|
ronaldvdmeer marked this conversation as resolved.
Outdated
|
||
| await coordinator.async_config_entry_first_refresh() | ||
|
|
||
| entry.runtime_data = coordinator | ||
|
|
||
| await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
| return True | ||
|
|
||
|
|
||
| async def async_unload_entry(hass: HomeAssistant, entry: EveOnlineConfigEntry) -> bool: | ||
| """Unload a config entry.""" | ||
| return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) | ||
This file contains hidden or 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,41 @@ | ||
| """API helpers for the Eve Online integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import cast | ||
|
|
||
| from aiohttp import ClientError, ClientSession | ||
| from eveonline.auth import AbstractAuth | ||
|
|
||
| from homeassistant.exceptions import ( | ||
| ConfigEntryAuthFailed, | ||
| OAuth2TokenRequestReauthError, | ||
| OAuth2TokenRequestTransientError, | ||
| ) | ||
| from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session | ||
| from homeassistant.helpers.update_coordinator import UpdateFailed | ||
|
|
||
|
|
||
| class AsyncConfigEntryAuth(AbstractAuth): | ||
| """Provide Eve Online authentication tied to an OAuth2 based config entry.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| websession: ClientSession, | ||
| oauth_session: OAuth2Session, | ||
| ) -> None: | ||
| """Initialize Eve Online auth.""" | ||
| super().__init__(websession) | ||
| self._oauth_session = oauth_session | ||
|
|
||
| async def async_get_access_token(self) -> str: | ||
| """Return a valid access token.""" | ||
| try: | ||
| await self._oauth_session.async_ensure_token_valid() | ||
| except OAuth2TokenRequestReauthError as err: | ||
| raise ConfigEntryAuthFailed( | ||
| "Authentication failed, please reauthenticate" | ||
| ) from err | ||
| except (OAuth2TokenRequestTransientError, ClientError) as err: | ||
| raise UpdateFailed(f"Failed to refresh OAuth token: {err}") from err | ||
| return cast(str, self._oauth_session.token["access_token"]) |
14 changes: 14 additions & 0 deletions
14
homeassistant/components/eveonline/application_credentials.py
This file contains hidden or 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 for the Eve Online 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 the Eve Online authorization server.""" | ||
| return AuthorizationServer( | ||
| authorize_url=OAUTH2_AUTHORIZE, | ||
| token_url=OAUTH2_TOKEN, | ||
| ) |
This file contains hidden or 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,74 @@ | ||
| """Config flow for the Eve Online integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from typing import Any | ||
|
|
||
| import jwt | ||
|
|
||
| from homeassistant.config_entries import ConfigFlowResult | ||
| from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler | ||
|
|
||
| from .const import CONF_CHARACTER_ID, CONF_CHARACTER_NAME, DOMAIN, SCOPES | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class OAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): | ||
| """Handle OAuth2 config flow for Eve Online. | ||
|
|
||
| Each config entry represents one authenticated character. | ||
| Multiple characters can be added as separate entries. | ||
| """ | ||
|
|
||
| DOMAIN = DOMAIN | ||
|
|
||
| @property | ||
| def logger(self) -> logging.Logger: | ||
| """Return logger.""" | ||
| return _LOGGER | ||
|
|
||
| @property | ||
| def extra_authorize_data(self) -> dict[str, Any]: | ||
| """Extra data to include in the authorize URL.""" | ||
| return {"scope": " ".join(SCOPES)} | ||
|
|
||
| async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: | ||
| """Create an entry for the flow. | ||
|
|
||
| Decode the Eve SSO JWT access token to extract character_id and | ||
| character_name, then create a config entry for that character. | ||
|
ronaldvdmeer marked this conversation as resolved.
Outdated
|
||
| """ | ||
| try: | ||
| token = data["token"]["access_token"] | ||
| character_info = _decode_eve_jwt(token) | ||
| except ValueError, KeyError, jwt.DecodeError: | ||
| return self.async_abort(reason="oauth_error") | ||
|
|
||
| character_id = character_info[CONF_CHARACTER_ID] | ||
| character_name = character_info[CONF_CHARACTER_NAME] | ||
|
|
||
| await self.async_set_unique_id(str(character_id)) | ||
| self._abort_if_unique_id_configured() | ||
|
|
||
| data[CONF_CHARACTER_ID] = character_id | ||
| data[CONF_CHARACTER_NAME] = character_name | ||
|
|
||
| return self.async_create_entry( | ||
| title=character_name, | ||
| data=data, | ||
| ) | ||
|
ronaldvdmeer marked this conversation as resolved.
ronaldvdmeer marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| def _decode_eve_jwt(token: str) -> dict[str, Any]: | ||
| """Decode an Eve SSO JWT to extract character info.""" | ||
| decoded = jwt.decode(token, options={"verify_signature": False}) | ||
| sub = decoded.get("sub", "") | ||
| sub_parts = sub.split(":") | ||
| if len(sub_parts) != 3 or sub_parts[0] != "CHARACTER" or sub_parts[1] != "EVE": | ||
| raise ValueError(sub) | ||
|
ronaldvdmeer marked this conversation as resolved.
|
||
| return { | ||
| CONF_CHARACTER_ID: int(sub_parts[2]), | ||
| CONF_CHARACTER_NAME: decoded.get("name", "Unknown"), | ||
|
ronaldvdmeer marked this conversation as resolved.
Outdated
|
||
| } | ||
This file contains hidden or 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,17 @@ | ||
| """Constants for the Eve Online integration.""" | ||
|
|
||
| from typing import Final | ||
|
|
||
| DOMAIN: Final = "eveonline" | ||
|
|
||
| CONF_CHARACTER_ID: Final = "character_id" | ||
| CONF_CHARACTER_NAME: Final = "character_name" | ||
|
|
||
| OAUTH2_AUTHORIZE: Final = "https://login.eveonline.com/v2/oauth/authorize" | ||
| OAUTH2_TOKEN: Final = "https://login.eveonline.com/v2/oauth/token" | ||
|
|
||
| SCOPES: Final[list[str]] = [ | ||
| "esi-location.read_location.v1", | ||
| "esi-location.read_ship_type.v1", | ||
| "esi-wallet.read_character_wallet.v1", | ||
| ] |
This file contains hidden or 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,101 @@ | ||
| """Coordinator for the Eve Online integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass | ||
| from datetime import timedelta | ||
| import logging | ||
|
|
||
| import aiohttp | ||
| from eveonline import EveOnlineClient, EveOnlineError | ||
| from eveonline.models import CharacterLocation, CharacterShip | ||
|
|
||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
|
||
| from .const import DOMAIN | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
ronaldvdmeer marked this conversation as resolved.
|
||
| SCAN_INTERVAL = 60 | ||
|
ronaldvdmeer marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| @dataclass | ||
|
ronaldvdmeer marked this conversation as resolved.
Outdated
|
||
| class EveOnlineData: | ||
| """Eve Online character data.""" | ||
|
|
||
| character_id: int | ||
| character_name: str | ||
|
ronaldvdmeer marked this conversation as resolved.
|
||
| wallet_balance: float | None = None | ||
| location: CharacterLocation | None = None | ||
| solar_system_name: str | None = None | ||
| ship: CharacterShip | None = None | ||
|
|
||
|
|
||
| class EveOnlineCoordinator(DataUpdateCoordinator[EveOnlineData]): | ||
| """Coordinator for Eve Online character data.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| hass: HomeAssistant, | ||
| entry: ConfigEntry, | ||
| client: EveOnlineClient, | ||
| character_id: int, | ||
| character_name: str, | ||
| ) -> None: | ||
| """Initialize the coordinator.""" | ||
| super().__init__( | ||
| hass, | ||
| _LOGGER, | ||
| config_entry=entry, | ||
| name=DOMAIN, | ||
|
ronaldvdmeer marked this conversation as resolved.
Outdated
|
||
| update_interval=timedelta(seconds=SCAN_INTERVAL), | ||
| ) | ||
| self.client = client | ||
| self.character_id = character_id | ||
| self.character_name = character_name | ||
|
|
||
| async def _async_update_data(self) -> EveOnlineData: | ||
| """Fetch character data from ESI.""" | ||
| try: | ||
| wallet = await self.client.async_get_wallet_balance(self.character_id) | ||
| except (EveOnlineError, aiohttp.ClientError) as err: | ||
|
ronaldvdmeer marked this conversation as resolved.
Outdated
|
||
| raise UpdateFailed( | ||
| f"Error communicating with Eve Online API: {err}" | ||
| ) from err | ||
|
|
||
| location: CharacterLocation | None = None | ||
| solar_system_name: str | None = None | ||
| try: | ||
| location = await self.client.async_get_character_location(self.character_id) | ||
|
ronaldvdmeer marked this conversation as resolved.
|
||
| except (EveOnlineError, aiohttp.ClientError) as err: | ||
|
ronaldvdmeer marked this conversation as resolved.
Outdated
|
||
| _LOGGER.debug("Failed to fetch location: %s", err) | ||
|
|
||
|
ronaldvdmeer marked this conversation as resolved.
|
||
| if location: | ||
| try: | ||
| resolved = await self.client.async_resolve_names( | ||
| [location.solar_system_id] | ||
| ) | ||
| if resolved: | ||
| solar_system_name = resolved[0].name | ||
| except (EveOnlineError, aiohttp.ClientError) as err: | ||
|
ronaldvdmeer marked this conversation as resolved.
Outdated
|
||
| _LOGGER.debug("Failed to resolve solar system name: %s", err) | ||
|
|
||
| ship: CharacterShip | None = None | ||
| try: | ||
| ship = await self.client.async_get_character_ship(self.character_id) | ||
| except (EveOnlineError, aiohttp.ClientError) as err: | ||
| _LOGGER.debug("Failed to fetch ship: %s", err) | ||
|
|
||
| return EveOnlineData( | ||
| character_id=self.character_id, | ||
| character_name=self.character_name, | ||
| wallet_balance=wallet.balance, | ||
| location=location, | ||
| solar_system_name=solar_system_name, | ||
| ship=ship, | ||
| ) | ||
|
|
||
|
|
||
| type EveOnlineConfigEntry = ConfigEntry[EveOnlineCoordinator] | ||
|
ronaldvdmeer marked this conversation as resolved.
Outdated
|
||
This file contains hidden or 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,26 @@ | ||
| """Base entity for the Eve Online integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo | ||
| from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
|
||
| from .const import DOMAIN | ||
| from .coordinator import EveOnlineCoordinator | ||
|
|
||
|
|
||
| class EveOnlineCharacterEntity(CoordinatorEntity[EveOnlineCoordinator]): | ||
| """Base entity for an Eve Online character.""" | ||
|
|
||
| _attr_has_entity_name = True | ||
|
|
||
| def __init__(self, coordinator: EveOnlineCoordinator, key: str) -> None: | ||
|
ronaldvdmeer marked this conversation as resolved.
|
||
| """Initialize the entity.""" | ||
| super().__init__(coordinator) | ||
| self._attr_unique_id = f"{coordinator.character_id}_{key}" | ||
| self._attr_device_info = DeviceInfo( | ||
| entry_type=DeviceEntryType.SERVICE, | ||
| identifiers={(DOMAIN, str(coordinator.character_id))}, | ||
| manufacturer="CCP Games", | ||
| name=coordinator.character_name, | ||
| ) | ||
This file contains hidden or 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,13 @@ | ||
| { | ||
| "domain": "eveonline", | ||
| "name": "Eve Online", | ||
| "codeowners": ["@ronaldvdmeer"], | ||
| "config_flow": true, | ||
| "dependencies": ["application_credentials"], | ||
| "documentation": "https://www.home-assistant.io/integrations/eveonline", | ||
| "integration_type": "service", | ||
| "iot_class": "cloud_polling", | ||
| "loggers": ["eveonline"], | ||
| "quality_scale": "bronze", | ||
| "requirements": ["python-eveonline==0.4.0"] | ||
| } |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.