-
-
Notifications
You must be signed in to change notification settings - Fork 37.2k
Add eveonline integration #166674
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
Closed
ronaldvdmeer
wants to merge
49
commits into
home-assistant:dev
from
ronaldvdmeer:add-eveonline-integration
Closed
Add eveonline integration #166674
Changes from all commits
Commits
Show all changes
49 commits
Select commit
Hold shift + click to select a range
e1e2178
Add Eve Online integration
ronaldvdmeer e867344
Add binary sensor platform to Eve Online integration
ronaldvdmeer f5a61e8
Upgrade Eve Online integration to platinum quality scale
ronaldvdmeer 18b0066
Change quality scale to bronze for new integration
ronaldvdmeer 9b77a3c
Catch JWT decoding failures in config flow
ronaldvdmeer 2557a70
Use reconfigure-specific abort reason for account mismatch
ronaldvdmeer 0b5f7c1
Redact character_id in diagnostics entry_data
ronaldvdmeer 2dc2ed3
Catch aiohttp.ClientError in optional endpoint helpers
ronaldvdmeer 65793f4
Use generator-based count instead of intermediate list
ronaldvdmeer a0796bb
Fix total_sp state class and re-raise auth errors in helpers
ronaldvdmeer 98d8777
Handle OAuth2 token errors and add binascii.Error to JWT catch
ronaldvdmeer 2ac10c5
Add comprehensive error path tests for config flow and coordinator
ronaldvdmeer c3547d4
Achieve 100% test coverage on all component files
ronaldvdmeer 3331cb7
Address Copilot review: translation_placeholders and ConfigEntryAuthF…
ronaldvdmeer 1f938ee
Address Copilot review: server entity dedup and stricter JWT validation
ronaldvdmeer 2ab1597
Round wallet balance to 2 decimals to avoid floating-point artifacts …
ronaldvdmeer a688ec4
Move token extraction into try block, track server entity ownership i…
ronaldvdmeer b5cadff
Remove binary_sensor platform to comply with single-platform rule for…
ronaldvdmeer ba3e069
Remove diagnostics and reauth flow per reviewer request
ronaldvdmeer a680f3c
Fix unload ownership guard and add authentication_failed translation
ronaldvdmeer 13149d7
Bump python-eveonline to 0.2.2
ronaldvdmeer 2d0de0f
Bump python-eveonline to 0.4.0
ronaldvdmeer 3b52d0d
Use token_refresh_failed translation key for OAuth token errors in ap…
ronaldvdmeer 1d4180a
Add expires_at to mock OAuth tokens in test fixtures
ronaldvdmeer 743c74b
Remove unused mock_eveonline_client parameter from _setup_integration…
ronaldvdmeer b04cc9e
Reload remaining entry to recreate server sensors when owning entry i…
ronaldvdmeer 9a316c0
Use constants for character_id and character_name config entry data keys
ronaldvdmeer f9234b7
Remove reconfigure support — defer to a later PR
ronaldvdmeer 813d198
Use translated units of measurement for custom sensor units
ronaldvdmeer a61c60e
Remove reconfigure tests that were left over after removing reconfigu…
ronaldvdmeer 2afea39
Remove ownership mechanism and available_fn — simplify to HA native b…
ronaldvdmeer 2a263d9
Add all quality scale rules with appropriate status
ronaldvdmeer 79e6928
Remove DOMAIN prefix from unique_id
ronaldvdmeer c000714
Use PyJWT to decode Eve SSO JWT token
ronaldvdmeer 513d8fa
Simplify: cleaner JWT decode, parametrized tests
ronaldvdmeer 4a0a010
Fix quality_scale entries and token_type in tests
ronaldvdmeer 0eab8d0
Remove server sensors to avoid unique_id collision across entries
ronaldvdmeer e673c02
Restore sell_orders in strings.json and icons.json
ronaldvdmeer 9450b9e
Replace server_status connectivity check with character_online call
ronaldvdmeer 7c766b5
Mark docs quality scale rules as done
ronaldvdmeer 2b02f4b
Mark docs-supported-devices as exempt — cloud service, no physical de…
ronaldvdmeer f5bb950
Parametrize setup error tests in test_init.py
ronaldvdmeer 85f2769
Improve code quality: integration_type, quality_scale, CONF_ constant…
ronaldvdmeer 94c2651
Improve code quality based on self-review
ronaldvdmeer 920f868
Fix UpdateFailed for transient auth errors, simplify sensor class hie…
ronaldvdmeer e415e90
Split coordinator by ESI cache interval and skip location/ship when o…
ronaldvdmeer 35465c0
Address Copilot review: parallelism, cleanup, quality scale
ronaldvdmeer 95d0320
Address Copilot review round 2: docstrings, offline caching, test qua…
ronaldvdmeer d503fcc
Fix quality scale, config flow syntax error, and stale comment
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
There are no files selected for viewing
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
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,74 @@ | ||
| """The Eve Online integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
|
|
||
| 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, | ||
| EveOnlineIndustryCoordinator, | ||
| EveOnlineMarketCoordinator, | ||
| EveOnlineRuntimeData, | ||
| EveOnlineSkillsCoordinator, | ||
| ) | ||
|
|
||
| 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) | ||
| 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 | ||
| ) | ||
| industry_coordinator = EveOnlineIndustryCoordinator( | ||
| hass, entry, client, character_id, character_name | ||
| ) | ||
| market_coordinator = EveOnlineMarketCoordinator( | ||
| hass, entry, client, character_id, character_name | ||
| ) | ||
| skills_coordinator = EveOnlineSkillsCoordinator( | ||
| hass, entry, client, character_id, character_name | ||
| ) | ||
|
|
||
| await asyncio.gather( | ||
| coordinator.async_config_entry_first_refresh(), | ||
| industry_coordinator.async_config_entry_first_refresh(), | ||
| market_coordinator.async_config_entry_first_refresh(), | ||
| skills_coordinator.async_config_entry_first_refresh(), | ||
| ) | ||
|
|
||
| entry.runtime_data = EveOnlineRuntimeData( | ||
| coordinator=coordinator, | ||
| industry_coordinator=industry_coordinator, | ||
| market_coordinator=market_coordinator, | ||
| skills_coordinator=skills_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,48 @@ | ||
| """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 | ||
|
|
||
| from .const import DOMAIN | ||
|
|
||
|
|
||
| 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( | ||
| translation_domain=DOMAIN, | ||
| translation_key="authentication_failed", | ||
| ) from err | ||
ronaldvdmeer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| except (OAuth2TokenRequestTransientError, ClientError) as err: | ||
| raise UpdateFailed( | ||
| translation_domain=DOMAIN, | ||
| translation_key="token_refresh_failed", | ||
| translation_placeholders={"error": str(err)}, | ||
| ) from err | ||
| return cast(str, self._oauth_session.token["access_token"]) | ||
ronaldvdmeer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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]: | ||
ronaldvdmeer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """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. | ||
| """ | ||
| try: | ||
| token = data["token"]["access_token"] | ||
| character_info = _decode_eve_jwt(token) | ||
| except ValueError, KeyError, jwt.DecodeError: | ||
ronaldvdmeer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return self.async_abort(reason="oauth_error") | ||
ronaldvdmeer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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() | ||
|
|
||
ronaldvdmeer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| 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) | ||
| return { | ||
| CONF_CHARACTER_ID: int(sub_parts[2]), | ||
| CONF_CHARACTER_NAME: decoded.get("name", "Unknown"), | ||
| } | ||
ronaldvdmeer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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,24 @@ | ||
| """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-characters.read_fatigue.v1", | ||
| "esi-industry.read_character_jobs.v1", | ||
| "esi-location.read_location.v1", | ||
| "esi-location.read_online.v1", | ||
| "esi-location.read_ship_type.v1", | ||
| "esi-mail.read_mail.v1", | ||
| "esi-markets.read_character_orders.v1", | ||
| "esi-skills.read_skillqueue.v1", | ||
| "esi-skills.read_skills.v1", | ||
| "esi-wallet.read_character_wallet.v1", | ||
| ] |
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.