From dff8e5221bb50753f2858dcaca86bea06e0761d7 Mon Sep 17 00:00:00 2001 From: Jordan Harvey Date: Wed, 29 Oct 2025 14:10:47 +0000 Subject: [PATCH] Use API token authentiation in traccar_server (#155297) --- .../components/traccar/manifest.json | 2 +- .../components/traccar_server/__init__.py | 22 +++++++++++-- .../components/traccar_server/config_flow.py | 23 ++++--------- .../components/traccar_server/manifest.json | 2 +- .../components/traccar_server/strings.json | 15 +++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/traccar_server/conftest.py | 6 ++-- .../traccar_server/test_config_flow.py | 33 +++++++------------ 9 files changed, 52 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 28e37a0e9cc..5d3b8361f1b 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/traccar", "iot_class": "cloud_push", "loggers": ["pytraccar"], - "requirements": ["pytraccar==2.1.1", "stringcase==1.2.0"] + "requirements": ["pytraccar==3.0.0", "stringcase==1.2.0"] } diff --git a/homeassistant/components/traccar_server/__init__.py b/homeassistant/components/traccar_server/__init__.py index 44aeedc3376..79d328a1a93 100644 --- a/homeassistant/components/traccar_server/__init__.py +++ b/homeassistant/components/traccar_server/__init__.py @@ -9,6 +9,7 @@ from pytraccar import ApiClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_API_TOKEN, CONF_HOST, CONF_PASSWORD, CONF_PORT, @@ -18,6 +19,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.event import async_track_time_interval @@ -33,6 +35,11 @@ PLATFORMS: list[Platform] = [ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Traccar Server from a config entry.""" + if CONF_API_TOKEN not in entry.data: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="migrate_to_api_token", + ) client_session = async_create_clientsession( hass, cookie_jar=CookieJar( @@ -46,8 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client_session=client_session, host=entry.data[CONF_HOST], port=entry.data[CONF_PORT], - username=entry.data[CONF_USERNAME], - password=entry.data[CONF_PASSWORD], + token=entry.data[CONF_API_TOKEN], ssl=entry.data[CONF_SSL], verify_ssl=entry.data[CONF_VERIFY_SSL], ), @@ -90,3 +96,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle an options update.""" await hass.config_entries.async_reload(entry.entry_id) + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old entry.""" + + if entry.version < 2: + # Version 2: Remove username and password, only keep API token + data = dict(entry.data) + data.pop(CONF_USERNAME, None) + data.pop(CONF_PASSWORD, None) + hass.config_entries.async_update_entry(entry, data=data, version=2) + return True diff --git a/homeassistant/components/traccar_server/config_flow.py b/homeassistant/components/traccar_server/config_flow.py index ae2f01e698b..a7d91582339 100644 --- a/homeassistant/components/traccar_server/config_flow.py +++ b/homeassistant/components/traccar_server/config_flow.py @@ -16,11 +16,10 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( + CONF_API_TOKEN, CONF_HOST, - CONF_PASSWORD, CONF_PORT, CONF_SSL, - CONF_USERNAME, CONF_VERIFY_SSL, ) from homeassistant.core import callback @@ -61,10 +60,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( vol.Optional(CONF_PORT, default="8082"): TextSelector( TextSelectorConfig(type=TextSelectorType.TEXT) ), - vol.Required(CONF_USERNAME): TextSelector( - TextSelectorConfig(type=TextSelectorType.EMAIL) - ), - vol.Required(CONF_PASSWORD): TextSelector( + vol.Required(CONF_API_TOKEN): TextSelector( TextSelectorConfig(type=TextSelectorType.PASSWORD) ), vol.Optional(CONF_SSL, default=False): BooleanSelector(BooleanSelectorConfig()), @@ -120,16 +116,17 @@ OPTIONS_FLOW = { class TraccarServerConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Traccar Server.""" + VERSION = 2 + async def _get_server_info(self, user_input: dict[str, Any]) -> ServerModel: """Get server info.""" client = ApiClient( client_session=async_get_clientsession(self.hass), host=user_input[CONF_HOST], port=user_input[CONF_PORT], - username=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], ssl=user_input[CONF_SSL], verify_ssl=user_input[CONF_VERIFY_SSL], + token=user_input[CONF_API_TOKEN], ) return await client.get_server() @@ -201,19 +198,11 @@ class TraccarServerConfigFlow(ConfigFlow, domain=DOMAIN): reauth_entry, data_updates=user_input, ) - username = ( - user_input[CONF_USERNAME] - if user_input - else reauth_entry.data[CONF_USERNAME] - ) return self.async_show_form( step_id="reauth_confirm", data_schema=vol.Schema( { - vol.Required(CONF_USERNAME, default=username): TextSelector( - TextSelectorConfig(type=TextSelectorType.EMAIL) - ), - vol.Required(CONF_PASSWORD): TextSelector( + vol.Required(CONF_API_TOKEN): TextSelector( TextSelectorConfig(type=TextSelectorType.PASSWORD) ), } diff --git a/homeassistant/components/traccar_server/manifest.json b/homeassistant/components/traccar_server/manifest.json index 18c30e52233..c2a17e67547 100644 --- a/homeassistant/components/traccar_server/manifest.json +++ b/homeassistant/components/traccar_server/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/traccar_server", "iot_class": "local_push", - "requirements": ["pytraccar==2.1.1"] + "requirements": ["pytraccar==3.0.0"] } diff --git a/homeassistant/components/traccar_server/strings.json b/homeassistant/components/traccar_server/strings.json index 3c12a4525e1..c3571d251b5 100644 --- a/homeassistant/components/traccar_server/strings.json +++ b/homeassistant/components/traccar_server/strings.json @@ -12,23 +12,21 @@ "step": { "reauth_confirm": { "data": { - "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]" + "api_token": "[%key:common::config_flow::data::api_token%]" }, "description": "The authentication credentials for {host}:{port} need to be updated." }, "user": { "data": { + "api_token": "[%key:common::config_flow::data::api_token%]", "host": "[%key:common::config_flow::data::host%]", - "password": "[%key:common::config_flow::data::password%]", "port": "[%key:common::config_flow::data::port%]", "ssl": "[%key:common::config_flow::data::ssl%]", - "username": "[%key:common::config_flow::data::username%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, "data_description": { - "host": "The hostname or IP address of your Traccar Server", - "username": "The username (email) you use to log in to your Traccar Server" + "api_token": "The API token generated from your account on your Traccar Server", + "host": "The hostname or IP address of your Traccar Server" } } } @@ -62,6 +60,11 @@ } } }, + "exceptions": { + "migrate_to_api_token": { + "message": "To continue using Traccar Server, you need to migrate to API token based authentication." + } + }, "options": { "step": { "init": { diff --git a/requirements_all.txt b/requirements_all.txt index 0e6c4aa649a..afcac8ffd9f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2605,7 +2605,7 @@ pytouchlinesl==0.5.0 # homeassistant.components.traccar # homeassistant.components.traccar_server -pytraccar==2.1.1 +pytraccar==3.0.0 # homeassistant.components.tradfri pytradfri[async]==9.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ae9b08ce40..f1f4e1dde11 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2166,7 +2166,7 @@ pytouchlinesl==0.5.0 # homeassistant.components.traccar # homeassistant.components.traccar_server -pytraccar==2.1.1 +pytraccar==3.0.0 # homeassistant.components.tradfri pytradfri[async]==9.0.1 diff --git a/tests/components/traccar_server/conftest.py b/tests/components/traccar_server/conftest.py index 0013b3249bd..776708723f4 100644 --- a/tests/components/traccar_server/conftest.py +++ b/tests/components/traccar_server/conftest.py @@ -14,11 +14,10 @@ from homeassistant.components.traccar_server.const import ( DOMAIN, ) from homeassistant.const import ( + CONF_API_TOKEN, CONF_HOST, - CONF_PASSWORD, CONF_PORT, CONF_SSL, - CONF_USERNAME, CONF_VERIFY_SSL, ) @@ -74,8 +73,7 @@ def mock_config_entry() -> MockConfigEntry: data={ CONF_HOST: "1.1.1.1", CONF_PORT: "8082", - CONF_USERNAME: "test@example.org", - CONF_PASSWORD: "ThisIsNotThePasswordYouAreL00kingFor", + CONF_API_TOKEN: "ThisIsNotThePasswordYouAreL00kingFor", CONF_SSL: False, CONF_VERIFY_SSL: True, }, diff --git a/tests/components/traccar_server/test_config_flow.py b/tests/components/traccar_server/test_config_flow.py index 7270a77fef1..a9c60344ec7 100644 --- a/tests/components/traccar_server/test_config_flow.py +++ b/tests/components/traccar_server/test_config_flow.py @@ -16,11 +16,10 @@ from homeassistant.components.traccar_server.const import ( ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( + CONF_API_TOKEN, CONF_HOST, - CONF_PASSWORD, CONF_PORT, CONF_SSL, - CONF_USERNAME, CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant @@ -44,8 +43,7 @@ async def test_form( result["flow_id"], { CONF_HOST: "1.1.1.1", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", + CONF_API_TOKEN: "test-token", }, ) await hass.async_block_till_done() @@ -55,8 +53,7 @@ async def test_form( assert result["data"] == { CONF_HOST: "1.1.1.1", CONF_PORT: "8082", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", + CONF_API_TOKEN: "test-token", CONF_SSL: False, CONF_VERIFY_SSL: True, } @@ -87,8 +84,7 @@ async def test_form_cannot_connect( result["flow_id"], { CONF_HOST: "1.1.1.1", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", + CONF_API_TOKEN: "test-token", }, ) @@ -101,8 +97,7 @@ async def test_form_cannot_connect( result["flow_id"], { CONF_HOST: "1.1.1.1", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", + CONF_API_TOKEN: "test-token", }, ) await hass.async_block_till_done() @@ -112,8 +107,7 @@ async def test_form_cannot_connect( assert result["data"] == { CONF_HOST: "1.1.1.1", CONF_PORT: "8082", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", + CONF_API_TOKEN: "test-token", CONF_SSL: False, CONF_VERIFY_SSL: True, } @@ -168,8 +162,7 @@ async def test_abort_already_configured( { CONF_HOST: "1.1.1.1", CONF_PORT: "8082", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", + CONF_API_TOKEN: "test-token", }, ) @@ -201,8 +194,7 @@ async def test_reauth_flow( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_USERNAME: "new-username", - CONF_PASSWORD: "new-password", + CONF_API_TOKEN: "new-token", }, ) await hass.async_block_till_done() @@ -211,8 +203,7 @@ async def test_reauth_flow( assert result["reason"] == "reauth_successful" # Verify the config entry was updated - assert mock_config_entry.data[CONF_USERNAME] == "new-username" - assert mock_config_entry.data[CONF_PASSWORD] == "new-password" + assert mock_config_entry.data[CONF_API_TOKEN] == "new-token" @pytest.mark.parametrize( @@ -248,8 +239,7 @@ async def test_reauth_flow_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_USERNAME: "new-username", - CONF_PASSWORD: "new-password", + CONF_API_TOKEN: "new-token", }, ) @@ -262,8 +252,7 @@ async def test_reauth_flow_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_USERNAME: "new-username", - CONF_PASSWORD: "new-password", + CONF_API_TOKEN: "new-token", }, ) await hass.async_block_till_done()