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
19 changes: 13 additions & 6 deletions homeassistant/components/fressnapf_tracker/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""The Fressnapf Tracker integration."""

from fressnapftracker import AuthClient
from fressnapftracker import AuthClient, FressnapfTrackerAuthenticationError

from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.httpx_client import get_async_client

from .const import CONF_USER_ID
from .const import CONF_USER_ID, DOMAIN
from .coordinator import (
FressnapfTrackerConfigEntry,
FressnapfTrackerDataUpdateCoordinator,
Expand All @@ -26,10 +27,16 @@ async def async_setup_entry(
) -> bool:
"""Set up Fressnapf Tracker from a config entry."""
auth_client = AuthClient(client=get_async_client(hass))
devices = await auth_client.get_devices(
user_id=entry.data[CONF_USER_ID],
user_access_token=entry.data[CONF_ACCESS_TOKEN],
)
try:
devices = await auth_client.get_devices(
user_id=entry.data[CONF_USER_ID],
user_access_token=entry.data[CONF_ACCESS_TOKEN],
)
except FressnapfTrackerAuthenticationError as exception:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from exception

coordinators: list[FressnapfTrackerDataUpdateCoordinator] = []
for device in devices:
Expand Down
98 changes: 76 additions & 22 deletions homeassistant/components/fressnapf_tracker/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Config flow for the Fressnapf Tracker integration."""

from collections.abc import Mapping
import logging
from typing import Any

Expand All @@ -10,7 +11,12 @@
)
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.httpx_client import get_async_client

Expand Down Expand Up @@ -136,40 +142,43 @@ async def async_step_sms_code(
errors=errors,
)

async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
async def _async_reauth_reconfigure(
self,
user_input: dict[str, Any] | None,
entry: Any,
step_id: str,
) -> ConfigFlowResult:
"""Handle reconfiguration of the integration."""
"""Request a new sms code for reauth or reconfigure flows."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()

if user_input is not None:
errors, success = await self._async_request_sms_code(
user_input[CONF_PHONE_NUMBER]
)
if success:
if reconfigure_entry.data[CONF_USER_ID] != self._context[CONF_USER_ID]:
if entry.data[CONF_USER_ID] != self._context[CONF_USER_ID]:
errors["base"] = "account_change_not_allowed"
else:
elif self.source == SOURCE_REAUTH:
return await self.async_step_reauth_sms_code()
elif self.source == SOURCE_RECONFIGURE:
return await self.async_step_reconfigure_sms_code()

return self.async_show_form(
step_id="reconfigure",
data_schema=vol.Schema(
{
vol.Required(
CONF_PHONE_NUMBER,
default=reconfigure_entry.data.get(CONF_PHONE_NUMBER),
): str,
}
step_id=step_id,
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA,
{CONF_PHONE_NUMBER: entry.data.get(CONF_PHONE_NUMBER)},
),
errors=errors,
)

async def async_step_reconfigure_sms_code(
self, user_input: dict[str, Any] | None = None
async def _async_reauth_reconfigure_sms_code(
self,
user_input: dict[str, Any] | None,
entry: Any,
step_id: str,
) -> ConfigFlowResult:
"""Handle the SMS code step during reconfiguration."""
"""Verify SMS code for reauth or reconfigure flows."""
errors: dict[str, str] = {}

if user_input is not None:
Expand All @@ -178,16 +187,61 @@ async def async_step_reconfigure_sms_code(
)
if access_token:
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data={
entry,
data_updates={
CONF_PHONE_NUMBER: self._context[CONF_PHONE_NUMBER],
CONF_USER_ID: self._context[CONF_USER_ID],
CONF_ACCESS_TOKEN: access_token,
},
)

return self.async_show_form(
step_id="reconfigure_sms_code",
step_id=step_id,
data_schema=STEP_SMS_CODE_DATA_SCHEMA,
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 confirmation step."""
return await self._async_reauth_reconfigure(
user_input,
self._get_reauth_entry(),
"reauth_confirm",
)

async def async_step_reauth_sms_code(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the SMS code step during reauth."""
return await self._async_reauth_reconfigure_sms_code(
user_input,
self._get_reauth_entry(),
"reauth_sms_code",
)

async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the integration."""
return await self._async_reauth_reconfigure(
user_input,
self._get_reconfigure_entry(),
"reconfigure",
)

async def async_step_reconfigure_sms_code(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the SMS code step during reconfiguration."""
return await self._async_reauth_reconfigure_sms_code(
user_input,
self._get_reconfigure_entry(),
"reconfigure_sms_code",
)
14 changes: 13 additions & 1 deletion homeassistant/components/fressnapf_tracker/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
from datetime import timedelta
import logging

from fressnapftracker import ApiClient, Device, FressnapfTrackerError, Tracker
from fressnapftracker import (
ApiClient,
Device,
FressnapfTrackerError,
FressnapfTrackerInvalidDeviceTokenError,
Tracker,
)

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

Expand Down Expand Up @@ -46,5 +53,10 @@ def __init__(
async def _async_update_data(self) -> Tracker:
try:
return await self.client.get_tracker()
except FressnapfTrackerInvalidDeviceTokenError as exception:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from exception
except FressnapfTrackerError as exception:
raise UpdateFailed(exception) from exception
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ rules:
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
reauthentication-flow: done
test-coverage: done

# Gold
Expand Down
23 changes: 22 additions & 1 deletion homeassistant/components/fressnapf_tracker/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
Expand All @@ -11,7 +12,7 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reconfigure": {
"reauth_confirm": {
"data": {
"phone_number": "[%key:component::fressnapf_tracker::config::step::user::data::phone_number%]"
},
Expand All @@ -20,6 +21,23 @@
},
"description": "Re-authenticate with your Fressnapf Tracker account to refresh your credentials."
},
"reauth_sms_code": {
"data": {
"sms_code": "[%key:component::fressnapf_tracker::config::step::sms_code::data::sms_code%]"
},
"data_description": {
"sms_code": "[%key:component::fressnapf_tracker::config::step::sms_code::data_description::sms_code%]"
}
},
"reconfigure": {
"data": {
"phone_number": "[%key:component::fressnapf_tracker::config::step::user::data::phone_number%]"
},
"data_description": {
"phone_number": "[%key:component::fressnapf_tracker::config::step::user::data_description::phone_number%]"
},
"description": "Update your Fressnapf Tracker account configuration."
},
"reconfigure_sms_code": {
"data": {
"sms_code": "[%key:component::fressnapf_tracker::config::step::sms_code::data::sms_code%]"
Expand Down Expand Up @@ -62,6 +80,9 @@
"charging": {
"message": "The flashlight cannot be activated while charging."
},
"invalid_auth": {
"message": "Your authentication with the Fressnapf Tracker API expired. Please re-authenticate to refresh your credentials."
},
"low_battery": {
"message": "The flashlight cannot be activated due to low battery."
},
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/kostal_plenticore/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ class PlenticoreSensorEntityDescription(SensorEntityDescription):
name="Battery SoC",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
formatter="format_round",
),
PlenticoreSensorEntityDescription(
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/sonos/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import dispatcher_send

from .const import SONOS_SPEAKER_ACTIVITY
Expand Down Expand Up @@ -135,6 +136,7 @@ class UnjoinData:

speakers: list[SonosSpeaker] = field(default_factory=list)
event: asyncio.Event = field(default_factory=asyncio.Event)
exception: HomeAssistantError | OSError | SoCoException | None = None


@dataclass
Expand Down
17 changes: 13 additions & 4 deletions homeassistant/components/sonos/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
PLAY_MODES,
)
from soco.data_structures import DidlFavorite, DidlMusicTrack
from soco.exceptions import SoCoException
from soco.ms_data_structures import MusicServiceItem
from sonos_websocket.exception import SonosWebsocketError

Expand Down Expand Up @@ -853,10 +854,14 @@ async def async_process_unjoin(now: datetime.datetime) -> None:
_LOGGER.debug(
"Processing unjoins for %s", [x.zone_name for x in unjoin_data.speakers]
)
await SonosSpeaker.unjoin_multi(
self.hass, self.config_entry, unjoin_data.speakers
)
unjoin_data.event.set()
try:
await SonosSpeaker.unjoin_multi(
self.hass, self.config_entry, unjoin_data.speakers
)
except (HomeAssistantError, SoCoException, OSError) as err:
unjoin_data.exception = err
finally:
unjoin_data.event.set()

if unjoin_data := sonos_data.unjoin_data.get(household_id):
unjoin_data.speakers.append(self.speaker)
Expand All @@ -868,3 +873,7 @@ async def async_process_unjoin(now: datetime.datetime) -> None:

_LOGGER.debug("Requesting unjoin for %s", self.speaker.zone_name)
await unjoin_data.event.wait()

# Re-raise any exception that occurred during processing
if unjoin_data.exception:
raise unjoin_data.exception
15 changes: 11 additions & 4 deletions homeassistant/components/sonos/speaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,10 @@ def _unjoin_all(speakers: list[SonosSpeaker]) -> None:
async with config_entry.runtime_data.topology_condition:
await hass.async_add_executor_job(_unjoin_all, speakers)
await SonosSpeaker.wait_for_groups(
hass, config_entry, [[s] for s in speakers]
hass,
config_entry,
[[s] for s in speakers],
action="unjoin",
)

@soco_error()
Expand Down Expand Up @@ -1204,6 +1207,7 @@ async def wait_for_groups(
hass: HomeAssistant,
config_entry: SonosConfigEntry,
groups: list[list[SonosSpeaker]],
action: str = "join",
) -> None:
"""Wait until all groups are present, or timeout."""

Expand All @@ -1228,14 +1232,17 @@ def _test_groups(groups: list[list[SonosSpeaker]]) -> bool:
while not _test_groups(groups):
await config_entry.runtime_data.topology_condition.wait()
except TimeoutError:
group_description = [
group_description = "; ".join(
f"{group[0].zone_name}: {', '.join(speaker.zone_name for speaker in group)}"
for group in groups
]
)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="timeout_join",
translation_placeholders={"group_description": str(group_description)},
translation_placeholders={
"group_description": group_description,
"action": action,
},
) from TimeoutError
any_speaker = next(iter(config_entry.runtime_data.discovered.values()))
any_speaker.soco.zone_group_state.clear_cache()
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/sonos/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
"message": "{entity_id} is not a known Sonos speaker."
},
"timeout_join": {
"message": "Timeout while waiting for Sonos player to join the group {group_description}"
"message": "Timeout while waiting for Sonos player to {action} the group {group_description}"
}
},
"issues": {
Expand Down
Loading
Loading