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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.6
rev: v0.16.7
hooks:
- id: ruff-check
args:
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 15 additions & 3 deletions homeassistant/components/cookidoo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import logging

from cookidoo_api import CookidooAuthException, CookidooRequestException
from cookidoo_api import (
CookidooAuthException,
CookidooParseException,
CookidooRequestException,
)

from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -98,7 +102,11 @@ async def async_migrate_entry(
try:
await cookidoo.login()
user_info = await cookidoo.get_user_info()
except (CookidooRequestException, CookidooAuthException) as e:
except (
CookidooAuthException,
CookidooParseException,
CookidooRequestException,
) as e:
_LOGGER.error("Could not migrate config entry: %s", e)
return False

Expand All @@ -114,7 +122,11 @@ async def async_migrate_entry(
try:
await cookidoo.login()
user_info = await cookidoo.get_user_info()
except (CookidooRequestException, CookidooAuthException) as e:
except (
CookidooAuthException,
CookidooParseException,
CookidooRequestException,
) as e:
_LOGGER.error("Could not migrate config entry: %s", e)
return False

Expand Down
14 changes: 5 additions & 9 deletions homeassistant/components/cookidoo/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@
import logging
from typing import override

from cookidoo_api import (
CookidooAuthException,
CookidooException,
CookidooRequestException,
)
from cookidoo_api import CookidooAuthException, CookidooException
from cookidoo_api.types import CookidooCalendarDayRecipe

from homeassistant.components.calendar import CalendarEntity, CalendarEvent
Expand Down Expand Up @@ -82,14 +78,14 @@ async def _fetch_week_plan(self, week_day: date) -> list:
except CookidooAuthException:
try:
await self.coordinator.cookidoo.login()
except (CookidooAuthException, CookidooRequestException) as exc:
return await self.coordinator.cookidoo.get_recipes_in_calendar_week(
week_day
)
except CookidooException as exc:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="calendar_fetch_failed",
) from exc
return await self.coordinator.cookidoo.get_recipes_in_calendar_week(
week_day
)
except CookidooException as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
Expand Down
41 changes: 36 additions & 5 deletions homeassistant/components/cookidoo/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""Config flow for Cookidoo integration."""

from collections.abc import Mapping
from dataclasses import asdict
import logging
from typing import Any, override

from cookidoo_api import (
CookidooAuthData,
CookidooAuthException,
CookidooParseException,
CookidooRequestException,
get_country_options,
get_localization_options,
Expand All @@ -18,7 +21,14 @@
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.const import CONF_COUNTRY, CONF_EMAIL, CONF_LANGUAGE, CONF_PASSWORD
from homeassistant.const import (
CONF_COUNTRY,
CONF_EMAIL,
CONF_LANGUAGE,
CONF_PASSWORD,
CONF_TOKEN,
)
from homeassistant.core import callback
from homeassistant.helpers.selector import (
CountrySelector,
CountrySelectorConfig,
Expand Down Expand Up @@ -61,6 +71,9 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN):

user_input: dict[str, Any]
user_uuid: str
# A login whose token response carries no refresh token leaves the library
# with nothing to hand us, and the entry is then created without tokens
token: dict[str, Any] = {}

async def async_step_reconfigure(
self, user_input: dict[str, Any]
Expand Down Expand Up @@ -119,7 +132,12 @@ async def async_step_language(
):
if self.source == SOURCE_USER:
return self.async_create_entry(
title="Cookidoo", data={**self.user_input, **language_input}
title="Cookidoo",
data={
**self.user_input,
**language_input,
CONF_TOKEN: self.token,
},
)
reconfigure_entry = self._get_reconfigure_entry()
return self.async_update_reload_and_abort(
Expand All @@ -128,6 +146,7 @@ async def async_step_language(
**reconfigure_entry.data,
**self.user_input,
**language_input,
CONF_TOKEN: self.token,
},
)

Expand Down Expand Up @@ -160,7 +179,7 @@ async def async_step_reauth_confirm(
await self.async_set_unique_id(self.user_uuid)
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
reauth_entry, data_updates=user_input
reauth_entry, data_updates={**user_input, CONF_TOKEN: self.token}
)
return self.async_show_form(
step_id="reauth_confirm",
Expand Down Expand Up @@ -200,6 +219,11 @@ async def generate_language_schema(self) -> None:
),
}

@callback
def _save_token(self, auth_data: CookidooAuthData) -> None:
"""Keep the tokens the library hands us during the validation requests."""
self.token = asdict(auth_data)

async def validate_input(
self,
user_input: dict[str, Any],
Expand All @@ -222,14 +246,21 @@ async def validate_input(
await get_localization_options(country=data_input[CONF_COUNTRY].lower())
)[0].language # Pick any language to test login

cookidoo = await cookidoo_from_config_data(self.hass, data_input)
# Only this attempt's tokens may reach the entry: a login that yields
# none leaves _save_token uncalled, and an earlier attempt may have
# stored a pair, for another account in a reauth
self.token = {}
cookidoo = await cookidoo_from_config_data(
self.hass, data_input, on_auth_data_update=self._save_token
)
try:
await cookidoo.login()
user_info = await cookidoo.get_user_info()
self.user_uuid = user_info.id
if language_input:
await cookidoo.get_additional_items()
except CookidooRequestException:
except CookidooRequestException, CookidooParseException:
# login() scrapes the CIAM login page, so it can also fail to parse it
errors["base"] = "cannot_connect"
except CookidooAuthException:
errors["base"] = "invalid_auth"
Expand Down
22 changes: 19 additions & 3 deletions homeassistant/components/cookidoo/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
CookidooAuthException,
CookidooException,
CookidooIngredientItem,
CookidooParseException,
CookidooRequestException,
CookidooSubscription,
CookidooUserInfo,
Expand Down Expand Up @@ -60,11 +61,20 @@ def __init__(
)
self.cookidoo = cookidoo

async def _async_login(self) -> CookidooUserInfo:
"""Return the user info, reusing the persisted tokens while they are valid."""
if self.cookidoo.auth_data is not None:
try:
return await self.cookidoo.get_user_info()
except CookidooAuthException:
_LOGGER.debug("Stored tokens are no longer valid, logging in again")
await self.cookidoo.login()
return await self.cookidoo.get_user_info()

@override
async def _async_setup(self) -> None:
try:
await self.cookidoo.login()
self.user = await self.cookidoo.get_user_info()
self.user = await self._async_login()
except CookidooRequestException as e:
raise UpdateFailed(
translation_domain=DOMAIN,
Expand All @@ -78,6 +88,12 @@ async def _async_setup(self) -> None:
CONF_EMAIL: self.config_entry.data[CONF_EMAIL]
},
) from e
except CookidooParseException as e:
# login() scrapes the CIAM login page, so it can also fail to parse it
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="setup_request_exception",
) from e

@override
async def _async_update_data(self) -> CookidooData:
Expand All @@ -99,7 +115,7 @@ async def _async_update_data(self) -> CookidooData:
CONF_EMAIL: self.config_entry.data[CONF_EMAIL]
},
) from exc
except CookidooRequestException as exc:
except (CookidooRequestException, CookidooParseException) as exc:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="setup_request_exception",
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/cookidoo/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
from typing import Any

from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PASSWORD
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN
from homeassistant.core import HomeAssistant

from .coordinator import CookidooConfigEntry

TO_REDACT = [
CONF_PASSWORD,
CONF_TOKEN,
]


Expand Down
39 changes: 34 additions & 5 deletions homeassistant/components/cookidoo/helpers.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
"""Helpers for cookidoo."""

from collections.abc import Callable
from dataclasses import asdict
from typing import Any

from aiohttp import CookieJar
from cookidoo_api import Cookidoo, CookidooConfig, get_localization_options
from cookidoo_api import (
Cookidoo,
CookidooAuthData,
CookidooConfig,
get_localization_options,
)

from homeassistant.const import CONF_COUNTRY, CONF_EMAIL, CONF_LANGUAGE, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.const import (
CONF_COUNTRY,
CONF_EMAIL,
CONF_LANGUAGE,
CONF_PASSWORD,
CONF_TOKEN,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_create_clientsession

from .coordinator import CookidooConfigEntry


async def cookidoo_from_config_data(
hass: HomeAssistant, data: dict[str, Any]
hass: HomeAssistant,
data: dict[str, Any],
on_auth_data_update: Callable[[CookidooAuthData], None] | None = None,
) -> Cookidoo:
"""Build cookidoo from config data."""
localizations = await get_localization_options(
Expand All @@ -28,11 +43,25 @@ async def cookidoo_from_config_data(
password=data[CONF_PASSWORD],
localization=localizations[0],
),
on_auth_data_update=on_auth_data_update,
)


async def cookidoo_from_config_entry(
hass: HomeAssistant, entry: CookidooConfigEntry
) -> Cookidoo:
"""Build cookidoo from config entry."""
return await cookidoo_from_config_data(hass, dict(entry.data))

@callback
def save_auth_data(auth_data: CookidooAuthData) -> None:
"""Store the tokens, so a restart does not need a new login."""
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_TOKEN: asdict(auth_data)}
)

cookidoo = await cookidoo_from_config_data(
hass, dict(entry.data), on_auth_data_update=save_auth_data
)
if token := entry.data.get(CONF_TOKEN):
cookidoo.apply_auth_data(CookidooAuthData(**token))
return cookidoo
2 changes: 1 addition & 1 deletion homeassistant/components/cookidoo/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["cookidoo_api"],
"quality_scale": "silver",
"requirements": ["cookidoo-api==0.17.2"]
"requirements": ["cookidoo-api==0.18.4"]
}
5 changes: 5 additions & 0 deletions homeassistant/components/imap/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@ async def async_step_init(
except AbortFlow as err:
errors = {"base": err.reason}
else:
if (
CONF_CUSTOM_EVENT_DATA_TEMPLATE not in user_input
and CONF_CUSTOM_EVENT_DATA_TEMPLATE in entry_data
):
entry_data.pop(CONF_CUSTOM_EVENT_DATA_TEMPLATE)
entry_data.update(user_input)
errors = await validate_input(self.hass, entry_data)
if not errors:
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/midea/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
key="arofene_link",
translation_key="arofene_link",
device_class=BinarySensorDeviceClass.PLUG,
entity_category=EntityCategory.DIAGNOSTIC,
),
BinarySensorEntityDescription(
key="header_exist",
Expand Down
6 changes: 5 additions & 1 deletion homeassistant/components/midea/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
NumberEntity,
NumberEntityDescription,
)
from homeassistant.const import UnitOfMass, UnitOfTime, UnitOfVolume
from homeassistant.const import EntityCategory, UnitOfMass, UnitOfTime, UnitOfVolume
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

Expand Down Expand Up @@ -64,6 +64,7 @@ class MideaNumberEntityDescription(NumberEntityDescription):
native_max_value=360,
native_step=1,
native_unit_of_measurement=UnitOfTime.DAYS,
entity_category=EntityCategory.CONFIG,
),
MideaNumberEntityDescription(
key="water_hardness",
Expand All @@ -72,6 +73,7 @@ class MideaNumberEntityDescription(NumberEntityDescription):
native_min_value=0,
native_max_value=65535,
native_step=1,
entity_category=EntityCategory.CONFIG,
),
MideaNumberEntityDescription(
key="flushing_days",
Expand All @@ -82,6 +84,7 @@ class MideaNumberEntityDescription(NumberEntityDescription):
native_max_value=99,
native_step=1,
native_unit_of_measurement=UnitOfTime.DAYS,
entity_category=EntityCategory.CONFIG,
),
MideaNumberEntityDescription(
key="leak_water_protection_value",
Expand All @@ -92,6 +95,7 @@ class MideaNumberEntityDescription(NumberEntityDescription):
native_max_value=2550,
native_step=50,
native_unit_of_measurement=UnitOfVolume.LITERS,
entity_category=EntityCategory.CONFIG,
),
MideaNumberEntityDescription(
key="salt_setting",
Expand Down
Loading
Loading