Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions homeassistant/components/nordpool/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
},
"date": {
"name": "Date",
"description": "Only dates two months in the past and one day in the future is allowed."
"description": "Only dates in the range from two months in the past to one day in the future are allowed."
},
"areas": {
"name": "Areas",
Expand All @@ -120,20 +120,20 @@
"description": "Retrieves the price indices for a specific date.",
"fields": {
"config_entry": {
"name": "Config entry",
"description": "The Nord Pool configuration entry for this action."
"name": "[%key:component::nordpool::services::get_prices_for_date::fields::config_entry::name%]",
"description": "[%key:component::nordpool::services::get_prices_for_date::fields::config_entry::description%]"
},
"date": {
"name": "Date",
"description": "Only dates two months in the past and one day in the future is allowed."
"name": "[%key:component::nordpool::services::get_prices_for_date::fields::date::name%]",
"description": "[%key:component::nordpool::services::get_prices_for_date::fields::date::description%]"
},
"areas": {
"name": "Areas",
"description": "One or multiple areas to get prices for. If left empty it will use the areas already configured."
"name": "[%key:component::nordpool::services::get_prices_for_date::fields::areas::name%]",
"description": "[%key:component::nordpool::services::get_prices_for_date::fields::areas::description%]"
},
"currency": {
"name": "Currency",
"description": "Currency to get prices in. If left empty it will use the currency already configured."
"name": "[%key:component::nordpool::services::get_prices_for_date::fields::currency::name%]",
"description": "[%key:component::nordpool::services::get_prices_for_date::fields::currency::description%]"
},
"resolution": {
"name": "Resolution",
Expand Down
67 changes: 66 additions & 1 deletion homeassistant/components/traccar_server/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@

from __future__ import annotations

from collections.abc import Mapping
from typing import Any

from pytraccar import ApiClient, ServerModel, TraccarException
from pytraccar import (
ApiClient,
ServerModel,
TraccarAuthenticationException,
TraccarException,
)
import voluptuous as vol

from homeassistant import config_entries
Expand Down Expand Up @@ -160,6 +166,65 @@ async def async_step_user(
errors=errors,
)

async def async_step_reauth(
self, _entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
return await self.async_step_reauth_confirm()

async def async_step_reauth_confirm(
self,
user_input: dict[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle reauth flow."""
reauth_entry = self._get_reauth_entry()
errors: dict[str, str] = {}

if user_input is not None:
test_data = {
**reauth_entry.data,
**user_input,
}
try:
await self._get_server_info(test_data)
except TraccarAuthenticationException:
LOGGER.error("Invalid credentials for Traccar Server")
errors["base"] = "invalid_auth"
except TraccarException as exception:
LOGGER.error("Unable to connect to Traccar Server: %s", exception)
errors["base"] = "cannot_connect"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
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(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
}
),
errors=errors,
description_placeholders={
CONF_HOST: reauth_entry.data[CONF_HOST],
CONF_PORT: reauth_entry.data[CONF_PORT],
},
)

@staticmethod
@callback
def async_get_options_flow(
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/traccar_server/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
GeofenceModel,
PositionModel,
SubscriptionData,
TraccarAuthenticationException,
TraccarException,
)

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
Expand Down Expand Up @@ -90,6 +92,8 @@ async def _async_update_data(self) -> TraccarServerCoordinatorData:
self.client.get_positions(),
self.client.get_geofences(),
)
except TraccarAuthenticationException:
raise ConfigEntryAuthFailed from None
except TraccarException as ex:
raise UpdateFailed(f"Error while updating device data: {ex}") from ex

Expand Down Expand Up @@ -236,6 +240,8 @@ async def subscribe(self) -> None:
"""Subscribe to events."""
try:
await self.client.subscribe(self.handle_subscription_data)
except TraccarAuthenticationException:
raise ConfigEntryAuthFailed from None
except TraccarException as ex:
if self._should_log_subscription_error:
self._should_log_subscription_error = False
Expand Down
13 changes: 11 additions & 2 deletions homeassistant/components/traccar_server/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,23 @@
"host": "The hostname or IP address of your Traccar Server",
"username": "The username (email) you use to log in to your Traccar Server"
}
},
"reauth_confirm": {
"description": "The authentication credentials for {host}:{port} need to be updated.",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
"unknown": "[%key:common::config_flow::error::unknown%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"options": {
Expand Down
97 changes: 96 additions & 1 deletion tests/components/traccar_server/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from unittest.mock import AsyncMock

import pytest
from pytraccar import TraccarException
from pytraccar import TraccarAuthenticationException, TraccarException

from homeassistant import config_entries
from homeassistant.components.traccar_server.const import (
Expand Down Expand Up @@ -175,3 +175,98 @@ async def test_abort_already_configured(

assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"


async def test_reauth_flow(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_traccar_api_client: Generator[AsyncMock],
) -> None:
"""Test reauth flow."""
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)

result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config_entry.entry_id,
},
data=mock_config_entry.data,
)

assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"

result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: "new-username",
CONF_PASSWORD: "new-password",
},
)
await hass.async_block_till_done()

assert result["type"] is FlowResultType.ABORT
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"


@pytest.mark.parametrize(
("side_effect", "error"),
[
(TraccarAuthenticationException, "invalid_auth"),
(TraccarException, "cannot_connect"),
(Exception, "unknown"),
],
)
async def test_reauth_flow_errors(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_traccar_api_client: Generator[AsyncMock],
side_effect: Exception,
error: str,
) -> None:
"""Test reauth flow with errors."""
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)

result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config_entry.entry_id,
},
data=mock_config_entry.data,
)

mock_traccar_api_client.get_server.side_effect = side_effect

result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: "new-username",
CONF_PASSWORD: "new-password",
},
)

assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": error}

# Test recovery after error
mock_traccar_api_client.get_server.side_effect = None

result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: "new-username",
CONF_PASSWORD: "new-password",
},
)
await hass.async_block_till_done()

assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
Loading