Skip to content

Commit 70e9c4e

Browse files
ludeeusCopilotzweckj
authored
Add reauth flow to the Traccar Server integration (home-assistant#148236)
Co-authored-by: Copilot <[email protected]> Co-authored-by: Josef Zweck <[email protected]>
1 parent 26de1ea commit 70e9c4e

File tree

4 files changed

+179
-4
lines changed

4 files changed

+179
-4
lines changed

homeassistant/components/traccar_server/config_flow.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22

33
from __future__ import annotations
44

5+
from collections.abc import Mapping
56
from typing import Any
67

7-
from pytraccar import ApiClient, ServerModel, TraccarException
8+
from pytraccar import (
9+
ApiClient,
10+
ServerModel,
11+
TraccarAuthenticationException,
12+
TraccarException,
13+
)
814
import voluptuous as vol
915

1016
from homeassistant import config_entries
@@ -160,6 +166,65 @@ async def async_step_user(
160166
errors=errors,
161167
)
162168

169+
async def async_step_reauth(
170+
self, _entry_data: Mapping[str, Any]
171+
) -> ConfigFlowResult:
172+
"""Handle configuration by re-auth."""
173+
return await self.async_step_reauth_confirm()
174+
175+
async def async_step_reauth_confirm(
176+
self,
177+
user_input: dict[str, Any] | None = None,
178+
) -> ConfigFlowResult:
179+
"""Handle reauth flow."""
180+
reauth_entry = self._get_reauth_entry()
181+
errors: dict[str, str] = {}
182+
183+
if user_input is not None:
184+
test_data = {
185+
**reauth_entry.data,
186+
**user_input,
187+
}
188+
try:
189+
await self._get_server_info(test_data)
190+
except TraccarAuthenticationException:
191+
LOGGER.error("Invalid credentials for Traccar Server")
192+
errors["base"] = "invalid_auth"
193+
except TraccarException as exception:
194+
LOGGER.error("Unable to connect to Traccar Server: %s", exception)
195+
errors["base"] = "cannot_connect"
196+
except Exception: # noqa: BLE001
197+
LOGGER.exception("Unexpected exception")
198+
errors["base"] = "unknown"
199+
else:
200+
return self.async_update_reload_and_abort(
201+
reauth_entry,
202+
data_updates=user_input,
203+
)
204+
username = (
205+
user_input[CONF_USERNAME]
206+
if user_input
207+
else reauth_entry.data[CONF_USERNAME]
208+
)
209+
return self.async_show_form(
210+
step_id="reauth_confirm",
211+
data_schema=vol.Schema(
212+
{
213+
vol.Required(CONF_USERNAME, default=username): TextSelector(
214+
TextSelectorConfig(type=TextSelectorType.EMAIL)
215+
),
216+
vol.Required(CONF_PASSWORD): TextSelector(
217+
TextSelectorConfig(type=TextSelectorType.PASSWORD)
218+
),
219+
}
220+
),
221+
errors=errors,
222+
description_placeholders={
223+
CONF_HOST: reauth_entry.data[CONF_HOST],
224+
CONF_PORT: reauth_entry.data[CONF_PORT],
225+
},
226+
)
227+
163228
@staticmethod
164229
@callback
165230
def async_get_options_flow(

homeassistant/components/traccar_server/coordinator.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
GeofenceModel,
1414
PositionModel,
1515
SubscriptionData,
16+
TraccarAuthenticationException,
1617
TraccarException,
1718
)
1819

1920
from homeassistant.config_entries import ConfigEntry
2021
from homeassistant.core import HomeAssistant
22+
from homeassistant.exceptions import ConfigEntryAuthFailed
2123
from homeassistant.helpers.dispatcher import async_dispatcher_send
2224
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
2325
from homeassistant.util import dt as dt_util
@@ -90,6 +92,8 @@ async def _async_update_data(self) -> TraccarServerCoordinatorData:
9092
self.client.get_positions(),
9193
self.client.get_geofences(),
9294
)
95+
except TraccarAuthenticationException:
96+
raise ConfigEntryAuthFailed from None
9397
except TraccarException as ex:
9498
raise UpdateFailed(f"Error while updating device data: {ex}") from ex
9599

@@ -236,6 +240,8 @@ async def subscribe(self) -> None:
236240
"""Subscribe to events."""
237241
try:
238242
await self.client.subscribe(self.handle_subscription_data)
243+
except TraccarAuthenticationException:
244+
raise ConfigEntryAuthFailed from None
239245
except TraccarException as ex:
240246
if self._should_log_subscription_error:
241247
self._should_log_subscription_error = False

homeassistant/components/traccar_server/strings.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,23 @@
1414
"host": "The hostname or IP address of your Traccar Server",
1515
"username": "The username (email) you use to log in to your Traccar Server"
1616
}
17+
},
18+
"reauth_confirm": {
19+
"description": "The authentication credentials for {host}:{port} need to be updated.",
20+
"data": {
21+
"username": "[%key:common::config_flow::data::username%]",
22+
"password": "[%key:common::config_flow::data::password%]"
23+
}
1724
}
1825
},
1926
"error": {
2027
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
21-
"unknown": "[%key:common::config_flow::error::unknown%]"
28+
"unknown": "[%key:common::config_flow::error::unknown%]",
29+
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
2230
},
2331
"abort": {
24-
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
32+
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
33+
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
2534
}
2635
},
2736
"options": {

tests/components/traccar_server/test_config_flow.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from unittest.mock import AsyncMock
55

66
import pytest
7-
from pytraccar import TraccarException
7+
from pytraccar import TraccarAuthenticationException, TraccarException
88

99
from homeassistant import config_entries
1010
from homeassistant.components.traccar_server.const import (
@@ -175,3 +175,98 @@ async def test_abort_already_configured(
175175

176176
assert result["type"] is FlowResultType.ABORT
177177
assert result["reason"] == "already_configured"
178+
179+
180+
async def test_reauth_flow(
181+
hass: HomeAssistant,
182+
mock_config_entry: MockConfigEntry,
183+
mock_traccar_api_client: Generator[AsyncMock],
184+
) -> None:
185+
"""Test reauth flow."""
186+
mock_config_entry.add_to_hass(hass)
187+
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
188+
189+
result = await hass.config_entries.flow.async_init(
190+
DOMAIN,
191+
context={
192+
"source": config_entries.SOURCE_REAUTH,
193+
"entry_id": mock_config_entry.entry_id,
194+
},
195+
data=mock_config_entry.data,
196+
)
197+
198+
assert result["type"] is FlowResultType.FORM
199+
assert result["step_id"] == "reauth_confirm"
200+
201+
result = await hass.config_entries.flow.async_configure(
202+
result["flow_id"],
203+
{
204+
CONF_USERNAME: "new-username",
205+
CONF_PASSWORD: "new-password",
206+
},
207+
)
208+
await hass.async_block_till_done()
209+
210+
assert result["type"] is FlowResultType.ABORT
211+
assert result["reason"] == "reauth_successful"
212+
213+
# Verify the config entry was updated
214+
assert mock_config_entry.data[CONF_USERNAME] == "new-username"
215+
assert mock_config_entry.data[CONF_PASSWORD] == "new-password"
216+
217+
218+
@pytest.mark.parametrize(
219+
("side_effect", "error"),
220+
[
221+
(TraccarAuthenticationException, "invalid_auth"),
222+
(TraccarException, "cannot_connect"),
223+
(Exception, "unknown"),
224+
],
225+
)
226+
async def test_reauth_flow_errors(
227+
hass: HomeAssistant,
228+
mock_config_entry: MockConfigEntry,
229+
mock_traccar_api_client: Generator[AsyncMock],
230+
side_effect: Exception,
231+
error: str,
232+
) -> None:
233+
"""Test reauth flow with errors."""
234+
mock_config_entry.add_to_hass(hass)
235+
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
236+
237+
result = await hass.config_entries.flow.async_init(
238+
DOMAIN,
239+
context={
240+
"source": config_entries.SOURCE_REAUTH,
241+
"entry_id": mock_config_entry.entry_id,
242+
},
243+
data=mock_config_entry.data,
244+
)
245+
246+
mock_traccar_api_client.get_server.side_effect = side_effect
247+
248+
result = await hass.config_entries.flow.async_configure(
249+
result["flow_id"],
250+
{
251+
CONF_USERNAME: "new-username",
252+
CONF_PASSWORD: "new-password",
253+
},
254+
)
255+
256+
assert result["type"] is FlowResultType.FORM
257+
assert result["errors"] == {"base": error}
258+
259+
# Test recovery after error
260+
mock_traccar_api_client.get_server.side_effect = None
261+
262+
result = await hass.config_entries.flow.async_configure(
263+
result["flow_id"],
264+
{
265+
CONF_USERNAME: "new-username",
266+
CONF_PASSWORD: "new-password",
267+
},
268+
)
269+
await hass.async_block_till_done()
270+
271+
assert result["type"] is FlowResultType.ABORT
272+
assert result["reason"] == "reauth_successful"

0 commit comments

Comments
 (0)