Compare commits

...

23 Commits

Author SHA1 Message Date
Franck Nijhof
4168000155 Bump version to 2026.4.0b5 2026-03-30 08:56:27 +00:00
Manu
9d230b4f7c Bump habiticalib to 0.4.7 (#166772) 2026-03-30 08:56:21 +00:00
Matthias Alphart
745f32faa3 Update knx-frontend to 2026.3.28.223133 (#166764) 2026-03-30 08:56:20 +00:00
Jan Bouwhuis
112ad886c6 Revert mqtt vacuum segments support (#166761) 2026-03-30 08:56:19 +00:00
J. Nick Koston
8b0ec21a15 Bump aiohttp to 3.13.4 (#166756) 2026-03-30 08:56:18 +00:00
David Knowles
afce52a0f4 Bump pydrawise to 2026.3.0 (#166750) 2026-03-30 08:56:17 +00:00
Michael
7e4757c213 Bump aioimmich to 0.12.1 (#166746) 2026-03-30 08:56:16 +00:00
Louis Christ
d6dbcc8d82 Bump pyblu to 2.0.6 (#166738) 2026-03-30 08:56:15 +00:00
Åke Strandberg
fca87a2b8a Add missing code for miele washing machine (#166731) 2026-03-30 08:56:13 +00:00
Noah Husby
87e648b8b8 Bump aiorussound to 4.9.1 (#166718) 2026-03-30 08:56:12 +00:00
Will Moss
ada549489c Handle Oauth2 ImplementationUnavailableError in google_tasks (#166657)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 08:56:11 +00:00
Will Moss
15e13de2a6 Handle Oauth2 ImplementationUnavailableError in lyric (#166655)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 08:56:10 +00:00
Will Moss
dd74665622 Handle Oauth2 ImplementationUnavailableError in microbees (#166654)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 08:56:08 +00:00
Will Moss
ff8fc56696 Handle Oauth2 ImplementationUnavailableError in monzo (#166653)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 08:56:07 +00:00
Will Moss
2d8c903533 Handle Oauth2 ImplementationUnavailableError in iotty (#166652)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 08:56:06 +00:00
Will Moss
c1606f515b Handle Oauth2 ImplementationUnavailableError in google_sheets (#166651)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 08:56:05 +00:00
Will Moss
fac2702063 Handle Oauth2 ImplementationUnavailableError in google_mail (#166650)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 08:56:03 +00:00
Will Moss
76ae6958ed Handle Oauth2 ImplementationUnavailableError in google_assistant_sdk (#166649)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 08:56:02 +00:00
Will Moss
1876ed7d16 Handle Oauth2 ImplementationUnavailableError in geocaching (#166648)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 08:56:01 +00:00
Will Moss
08ef4e0de0 Handle Oauth2 ImplementationUnavailableError in gentex_homelink (#166646)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 08:56:00 +00:00
crash0verride11
a48db9d817 Correct Musiccast sound mode name (#166644)
Co-authored-by: crash0verride11 <3526616+crash0verride11@users.noreply.github.com>
Co-authored-by: jtjart <80978647+jtjart@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-30 08:55:59 +00:00
Will Moss
1334531740 Handle Oauth2 ImplementationUnavailableError in husqvarna_automower (#166633)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 08:55:58 +00:00
Erwin Douna
d769b16ada Add new OAuth exceptions to Neato (#166584)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-30 08:55:57 +00:00
53 changed files with 460 additions and 527 deletions

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/bluesound",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["pyblu==2.0.5"],
"requirements": ["pyblu==2.0.6"],
"zeroconf": [
{
"type": "_musc._tcp.local."

View File

@@ -7,7 +7,7 @@ from homelink.mqtt_provider import MQTTProvider
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
from . import oauth2
@@ -29,11 +29,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeLinkConfigEntry) ->
hass, DOMAIN, auth_implementation
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
)
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
authenticated_session = oauth2.AsyncConfigEntryAuth(

View File

@@ -49,5 +49,10 @@
}
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -2,11 +2,14 @@
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
from .const import DOMAIN
from .coordinator import GeocachingConfigEntry, GeocachingDataUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
@@ -14,7 +17,13 @@ PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: GeocachingConfigEntry) -> bool:
"""Set up Geocaching from a config entry."""
implementation = await async_get_config_entry_implementation(hass, entry)
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
oauth_session = OAuth2Session(hass, entry, implementation)
coordinator = GeocachingDataUpdateCoordinator(

View File

@@ -65,5 +65,10 @@
"unit_of_measurement": "souvenirs"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -12,6 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery, intent
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
@@ -47,7 +48,13 @@ async def async_setup_entry(
hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry
) -> bool:
"""Set up Google Assistant SDK from a config entry."""
implementation = await async_get_config_entry_implementation(hass, entry)
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
session = OAuth2Session(hass, entry, implementation)
try:
await session.async_ensure_token_valid()

View File

@@ -48,6 +48,9 @@
"grpc_error": {
"message": "Failed to communicate with Google Assistant"
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
},
"reauth_required": {
"message": "Credentials are invalid, re-authentication required"
}

View File

@@ -5,8 +5,10 @@ from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
@@ -34,7 +36,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: GoogleMailConfigEntry) -> bool:
"""Set up Google Mail from a config entry."""
implementation = await async_get_config_entry_implementation(hass, entry)
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
session = OAuth2Session(hass, entry, implementation)
auth = AsyncConfigEntryAuth(hass, session)
await auth.check_and_refresh_token()

View File

@@ -51,6 +51,9 @@
"exceptions": {
"missing_from_for_alias": {
"message": "Missing 'from' email when setting an alias to show. You have to provide a 'from' email"
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
},
"services": {

View File

@@ -15,6 +15,7 @@ from homeassistant.exceptions import (
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
@@ -40,7 +41,13 @@ async def async_setup_entry(
hass: HomeAssistant, entry: GoogleSheetsConfigEntry
) -> bool:
"""Set up Google Sheets from a config entry."""
implementation = await async_get_config_entry_implementation(hass, entry)
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
session = OAuth2Session(hass, entry, implementation)
try:
await session.async_ensure_token_valid()

View File

@@ -42,6 +42,11 @@
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
},
"services": {
"append_sheet": {
"description": "Appends data to a worksheet in Google Sheets.",

View File

@@ -25,11 +25,17 @@ PLATFORMS: list[Platform] = [Platform.TODO]
async def async_setup_entry(hass: HomeAssistant, entry: GoogleTasksConfigEntry) -> bool:
"""Set up Google Tasks from a config entry."""
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
)
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
auth = api.AsyncConfigEntryAuth(hass, session)
try:

View File

@@ -42,5 +42,10 @@
"title": "[%key:common::config_flow::title::reauth%]"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["habiticalib"],
"quality_scale": "platinum",
"requirements": ["habiticalib==0.4.6"]
"requirements": ["habiticalib==0.4.7"]
}

View File

@@ -11,6 +11,9 @@ from homeassistant.helpers import (
config_entry_oauth2_flow,
config_validation as cv,
)
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
@@ -42,11 +45,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: AutomowerConfigEntry) -> bool:
"""Set up this integration using UI."""
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
api_api = api.AsyncConfigEntryAuth(
aiohttp_client.async_get_clientsession(hass),

View File

@@ -491,6 +491,9 @@
"command_send_failed": {
"message": "Failed to send command: {exception}"
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
},
"work_area_not_existing": {
"message": "The selected work area does not exist."
},

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["pydrawise"],
"requirements": ["pydrawise==2025.9.0"]
"requirements": ["pydrawise==2026.3.0"]
}

View File

@@ -9,5 +9,5 @@
"iot_class": "local_polling",
"loggers": ["aioimmich"],
"quality_scale": "platinum",
"requirements": ["aioimmich==0.12.0"]
"requirements": ["aioimmich==0.12.1"]
}

View File

@@ -6,11 +6,14 @@ import logging
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
from .const import DOMAIN
from .coordinator import (
IottyConfigEntry,
IottyConfigEntryData,
@@ -26,7 +29,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: IottyConfigEntry) -> boo
"""Set up iotty from a config entry."""
_LOGGER.debug("async_setup_entry entry_id=%s", entry.entry_id)
implementation = await async_get_config_entry_implementation(hass, entry)
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
session = OAuth2Session(hass, entry, implementation)
data_update_coordinator = IottyDataUpdateCoordinator(hass, entry, session)

View File

@@ -25,5 +25,10 @@
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -13,7 +13,7 @@
"requirements": [
"xknx==3.15.0",
"xknxproject==3.8.2",
"knx-frontend==2026.3.2.183756"
"knx-frontend==2026.3.28.223133"
],
"single_config_entry": true
}

View File

@@ -6,6 +6,7 @@ from aiolyric import Lyric
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
@@ -27,11 +28,17 @@ PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: LyricConfigEntry) -> bool:
"""Set up Honeywell Lyric from a config entry."""
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
)
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
if not isinstance(implementation, LyricLocalOAuth2Implementation):
raise TypeError("Unexpected auth implementation; can't find oauth client id")

View File

@@ -64,6 +64,11 @@
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
},
"services": {
"set_hold_time": {
"description": "Sets the time period to keep the temperature and override the schedule.",

View File

@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow
from .const import PLATFORMS
from .const import DOMAIN, PLATFORMS
from .coordinator import MicroBeesUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -50,11 +50,17 @@ async def async_migrate_entry(hass: HomeAssistant, entry: MicroBeesConfigEntry)
async def async_setup_entry(hass: HomeAssistant, entry: MicroBeesConfigEntry) -> bool:
"""Set up microBees from a config entry."""
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
)
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
try:

View File

@@ -35,5 +35,10 @@
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -440,6 +440,7 @@ class WashingMachineProgramId(MieleEnum, missing_to_none=True):
no_program = 0, -1
cottons = 1, 10001
normal = 2
minimum_iron = 3
delicates = 4, 10022
woollens = 8, 10040

View File

@@ -6,13 +6,16 @@ import logging
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
from .api import AuthenticatedMonzoAPI
from .const import DOMAIN
from .coordinator import MonzoConfigEntry, MonzoCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -39,7 +42,13 @@ async def async_migrate_entry(hass: HomeAssistant, entry: MonzoConfigEntry) -> b
async def async_setup_entry(hass: HomeAssistant, entry: MonzoConfigEntry) -> bool:
"""Set up Monzo from a config entry."""
implementation = await async_get_config_entry_implementation(hass, entry)
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
session = OAuth2Session(hass, entry, implementation)

View File

@@ -50,5 +50,10 @@
"name": "Total balance"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -18,8 +18,6 @@ ABBREVIATIONS = {
"bri_stat_t": "brightness_state_topic",
"bri_tpl": "brightness_template",
"bri_val_tpl": "brightness_value_template",
"cln_segmnts_cmd_t": "clean_segments_command_topic",
"cln_segmnts_cmd_tpl": "clean_segments_command_template",
"clr_temp_cmd_tpl": "color_temp_command_template",
"clrm_stat_t": "color_mode_state_topic",
"clrm_val_tpl": "color_mode_value_template",
@@ -187,7 +185,6 @@ ABBREVIATIONS = {
"rgbww_cmd_t": "rgbww_command_topic",
"rgbww_stat_t": "rgbww_state_topic",
"rgbww_val_tpl": "rgbww_value_template",
"segmnts": "segments",
"send_cmd_t": "send_command_topic",
"send_if_off": "send_if_off",
"set_fan_spd_t": "set_fan_speed_topic",

View File

@@ -1484,7 +1484,6 @@ class MqttEntity(
self._config = config
self._setup_from_config(self._config)
self._setup_common_attributes_from_config(self._config)
self._process_entity_update()
# Prepare MQTT subscriptions
self.attributes_prepare_discovery_update(config)
@@ -1587,10 +1586,6 @@ class MqttEntity(
def _setup_from_config(self, config: ConfigType) -> None:
"""(Re)Setup the entity."""
@callback
def _process_entity_update(self) -> None:
"""Process an entity discovery update."""
@abstractmethod
@callback
def _prepare_subscribe_topics(self) -> None:

View File

@@ -10,13 +10,12 @@ import voluptuous as vol
from homeassistant.components import vacuum
from homeassistant.components.vacuum import (
ENTITY_ID_FORMAT,
Segment,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_UNIQUE_ID
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -28,7 +27,7 @@ from . import subscription
from .config import MQTT_BASE_SCHEMA
from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC
from .entity import MqttEntity, async_setup_entity_entry_helper
from .models import MqttCommandTemplate, ReceiveMessage
from .models import ReceiveMessage
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
from .util import valid_publish_topic
@@ -53,9 +52,6 @@ POSSIBLE_STATES: dict[str, VacuumActivity] = {
STATE_CLEANING: VacuumActivity.CLEANING,
}
CONF_SEGMENTS = "segments"
CONF_CLEAN_SEGMENTS_COMMAND_TOPIC = "clean_segments_command_topic"
CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE = "clean_segments_command_template"
CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES
CONF_PAYLOAD_TURN_ON = "payload_turn_on"
CONF_PAYLOAD_TURN_OFF = "payload_turn_off"
@@ -141,39 +137,8 @@ MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset(
MQTT_VACUUM_DOCS_URL = "https://www.home-assistant.io/integrations/vacuum.mqtt/"
def validate_clean_area_config(config: ConfigType) -> ConfigType:
"""Check for a valid configuration and check segments."""
if (config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC not in config) or (
not config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC in config
):
raise vol.Invalid(
f"Options `{CONF_SEGMENTS}` and "
f"`{CONF_CLEAN_SEGMENTS_COMMAND_TOPIC}` must be defined together"
)
segments: list[str]
if segments := config[CONF_SEGMENTS]:
if not config.get(CONF_UNIQUE_ID):
raise vol.Invalid(
f"Option `{CONF_SEGMENTS}` requires `{CONF_UNIQUE_ID}` to be configured"
)
unique_segments: set[str] = set()
for segment in segments:
segment_id, _, _ = segment.partition(".")
if not segment_id or segment_id in unique_segments:
raise vol.Invalid(
f"The `{CONF_SEGMENTS}` option contains an invalid or non-"
f"unique segment ID '{segment_id}'. Got {segments}"
)
unique_segments.add(segment_id)
return config
_BASE_SCHEMA = MQTT_BASE_SCHEMA.extend(
PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
{
vol.Optional(CONF_SEGMENTS, default=[]): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_CLEAN_SEGMENTS_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All(
cv.ensure_list, [cv.string]
),
@@ -199,10 +164,7 @@ _BASE_SCHEMA = MQTT_BASE_SCHEMA.extend(
}
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
PLATFORM_SCHEMA_MODERN = vol.All(_BASE_SCHEMA, validate_clean_area_config)
DISCOVERY_SCHEMA = vol.All(
_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_clean_area_config
)
DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.ALLOW_EXTRA)
async def async_setup_entry(
@@ -229,11 +191,9 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
_entity_id_format = ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED
_segments: list[Segment]
_command_topic: str | None
_set_fan_speed_topic: str | None
_send_command_topic: str | None
_clean_segments_command_topic: str
_payloads: dict[str, str | None]
def __init__(
@@ -269,23 +229,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
self._attr_supported_features = _strings_to_services(
supported_feature_strings, STRING_TO_SERVICE
)
if config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC in config:
self._attr_supported_features |= VacuumEntityFeature.CLEAN_AREA
segments: list[str] = config[CONF_SEGMENTS]
self._segments = [
Segment(id=segment_id, name=name or segment_id)
for segment_id, _, name in [
segment.partition(".") for segment in segments
]
]
self._clean_segments_command_topic = config[
CONF_CLEAN_SEGMENTS_COMMAND_TOPIC
]
self._clean_segments_command_template = MqttCommandTemplate(
config.get(CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE),
entity=self,
).async_render
self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST]
self._command_topic = config.get(CONF_COMMAND_TOPIC)
self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC)
@@ -303,20 +246,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
)
}
@callback
def _process_entity_update(self) -> None:
"""Check vacuum segments with registry entry."""
if (
self._attr_supported_features & VacuumEntityFeature.CLEAN_AREA
and (last_seen := self.last_seen_segments) is not None
and {s.id: s for s in last_seen} != {s.id: s for s in self._segments}
):
self.async_create_segments_issue()
async def mqtt_async_added_to_hass(self) -> None:
"""Check vacuum segments with registry entry."""
self._process_entity_update()
def _update_state_attributes(self, payload: dict[str, Any]) -> None:
"""Update the entity state attributes."""
self._state_attrs.update(payload)
@@ -348,19 +277,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
"""(Re)Subscribe to topics."""
subscription.async_subscribe_topics_internal(self.hass, self._sub_state)
async def async_clean_segments(self, segment_ids: list[str], **kwargs: Any) -> None:
"""Perform an area clean."""
await self.async_publish_with_config(
self._clean_segments_command_topic,
self._clean_segments_command_template(
json_dumps(segment_ids), {"value": segment_ids}
),
)
async def async_get_segments(self) -> list[Segment]:
"""Return the available segments."""
return self._segments
async def _async_publish_command(self, feature: VacuumEntityFeature) -> None:
"""Publish a command."""
if self._command_topic is None:

View File

@@ -2,14 +2,19 @@
import logging
import aiohttp
from aiohttp import ClientError
from pybotvac import Account
from pybotvac.exceptions import NeatoException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -58,10 +63,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
session = OAuth2Session(hass, entry, implementation)
try:
await session.async_ensure_token_valid()
except aiohttp.ClientResponseError as ex:
_LOGGER.debug("API error: %s (%s)", ex.code, ex.message)
if ex.code in (401, 403):
raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex
except OAuth2TokenRequestReauthError as ex:
raise ConfigEntryAuthFailed from ex
except (OAuth2TokenRequestError, ClientError) as ex:
raise ConfigEntryNotReady from ex
neato_session = api.ConfigEntryAuth(hass, entry, implementation)

View File

@@ -8,6 +8,6 @@
"iot_class": "local_push",
"loggers": ["aiorussound"],
"quality_scale": "silver",
"requirements": ["aiorussound==4.9.0"],
"requirements": ["aiorussound==4.9.1"],
"zeroconf": ["_rio._tcp.local."]
}

View File

@@ -81,8 +81,8 @@
"usa_a": "Hall in USA A",
"usa_b": "Hall in USA B",
"vienna": "Hall in Vienna",
"village_gate": "Village gate",
"village_vanguard": "Village vanguard",
"village_gate": "Village Gate",
"village_vanguard": "Village Vanguard",
"warehouse_loft": "Warehouse loft"
}
}

View File

@@ -17,7 +17,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2026
MINOR_VERSION: Final = 4
PATCH_VERSION: Final = "0b4"
PATCH_VERSION: Final = "0b5"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)

View File

@@ -6,7 +6,7 @@ aiodns==4.0.0
aiogithubapi==26.0.0
aiohttp-asyncmdnsresolver==0.1.1
aiohttp-fast-zlib==0.3.0
aiohttp==3.13.3
aiohttp==3.13.4
aiohttp_cors==0.8.1
aiousbwatcher==1.1.1
aiozoneinfo==0.2.3

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2026.4.0b4"
version = "2026.4.0b5"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."
@@ -28,7 +28,7 @@ dependencies = [
# module level in `bootstrap.py` and its requirements thus need to be in
# requirements.txt to ensure they are always installed
"aiogithubapi==26.0.0",
"aiohttp==3.13.3",
"aiohttp==3.13.4",
"aiohttp_cors==0.8.1",
"aiohttp-fast-zlib==0.3.0",
"aiohttp-asyncmdnsresolver==0.1.1",

2
requirements.txt generated
View File

@@ -7,7 +7,7 @@ aiodns==4.0.0
aiogithubapi==26.0.0
aiohttp-asyncmdnsresolver==0.1.1
aiohttp-fast-zlib==0.3.0
aiohttp==3.13.3
aiohttp==3.13.4
aiohttp_cors==0.8.1
aiozoneinfo==0.2.3
annotatedyaml==1.0.2

12
requirements_all.txt generated
View File

@@ -294,7 +294,7 @@ aiohue==4.8.0
aioimaplib==2.0.1
# homeassistant.components.immich
aioimmich==0.12.0
aioimmich==0.12.1
# homeassistant.components.apache_kafka
aiokafka==0.10.0
@@ -389,7 +389,7 @@ aioridwell==2025.09.0
aioruckus==0.42
# homeassistant.components.russound_rio
aiorussound==4.9.0
aiorussound==4.9.1
# homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0
@@ -1173,7 +1173,7 @@ ha-philipsjs==3.2.4
ha-silabs-firmware-client==0.3.0
# homeassistant.components.habitica
habiticalib==0.4.6
habiticalib==0.4.7
# homeassistant.components.bluetooth
habluetooth==5.11.1
@@ -1383,7 +1383,7 @@ kiwiki-client==0.1.1
knocki==0.4.2
# homeassistant.components.knx
knx-frontend==2026.3.2.183756
knx-frontend==2026.3.28.223133
# homeassistant.components.konnected
konnected==1.2.0
@@ -1989,7 +1989,7 @@ pybbox==0.0.5-alpha
pyblackbird==0.6
# homeassistant.components.bluesound
pyblu==2.0.5
pyblu==2.0.6
# homeassistant.components.neato
pybotvac==0.0.28
@@ -2055,7 +2055,7 @@ pydiscovergy==3.0.2
pydoods==1.0.2
# homeassistant.components.hydrawise
pydrawise==2025.9.0
pydrawise==2026.3.0
# homeassistant.components.android_ip_webcam
pydroid-ipcam==3.0.0

View File

@@ -282,7 +282,7 @@ aiohue==4.8.0
aioimaplib==2.0.1
# homeassistant.components.immich
aioimmich==0.12.0
aioimmich==0.12.1
# homeassistant.components.apache_kafka
aiokafka==0.10.0
@@ -374,7 +374,7 @@ aioridwell==2025.09.0
aioruckus==0.42
# homeassistant.components.russound_rio
aiorussound==4.9.0
aiorussound==4.9.1
# homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0
@@ -1046,7 +1046,7 @@ ha-philipsjs==3.2.4
ha-silabs-firmware-client==0.3.0
# homeassistant.components.habitica
habiticalib==0.4.6
habiticalib==0.4.7
# homeassistant.components.bluetooth
habluetooth==5.11.1
@@ -1223,7 +1223,7 @@ kegtron-ble==1.0.2
knocki==0.4.2
# homeassistant.components.knx
knx-frontend==2026.3.2.183756
knx-frontend==2026.3.28.223133
# homeassistant.components.konnected
konnected==1.2.0
@@ -1723,7 +1723,7 @@ pybalboa==1.1.3
pyblackbird==0.6
# homeassistant.components.bluesound
pyblu==2.0.5
pyblu==2.0.6
# homeassistant.components.neato
pybotvac==0.0.28
@@ -1768,7 +1768,7 @@ pydexcom==0.5.1
pydiscovergy==3.0.2
# homeassistant.components.hydrawise
pydrawise==2025.9.0
pydrawise==2026.3.0
# homeassistant.components.android_ip_webcam
pydroid-ipcam==3.0.0

View File

@@ -8,6 +8,9 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant.components.gentex_homelink.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
import homeassistant.helpers.device_registry as dr
from . import setup_integration, update_callback
@@ -72,3 +75,21 @@ async def test_load_unload_entry(
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
@pytest.mark.usefixtures("aioclient_mock_fixture")
async def test_oauth_implementation_not_available(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
mock_config_entry.add_to_hass(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -0,0 +1,28 @@
"""Tests for the Geocaching integration."""
from unittest.mock import patch
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from tests.common import MockConfigEntry
async def test_oauth_implementation_not_available(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
mock_config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.geocaching.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -16,6 +16,9 @@ from homeassistant.components.google_assistant_sdk.const import SUPPORTED_LANGUA
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import Context, HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from homeassistant.setup import async_setup_component
from .conftest import ComponentSetup, ExpectedCredentials
@@ -492,3 +495,20 @@ async def test_conversation_agent_language_changed(
mock_text_assistant.assert_has_calls([call(ExpectedCredentials(), "es-ES")])
mock_text_assistant.assert_has_calls([call().assist(text1)])
mock_text_assistant.assert_has_calls([call().assist(text2)])
async def test_oauth_implementation_not_available(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.google_assistant_sdk.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -11,9 +11,13 @@ from homeassistant.components.google_mail import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from .conftest import GOOGLE_TOKEN_URI, ComponentSetup
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
@@ -134,3 +138,20 @@ async def test_device_info(
assert device.identifiers == {(DOMAIN, entry.entry_id)}
assert device.manufacturer == "Google, Inc."
assert device.name == "example@gmail.com"
async def test_oauth_implementation_not_available(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.google_mail.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -35,6 +35,9 @@ from homeassistant.exceptions import (
OAuth2TokenRequestTransientError,
ServiceValidationError,
)
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
@@ -558,3 +561,20 @@ async def test_get_sheet_invalid_worksheet(
blocking=True,
return_response=True,
)
async def test_oauth_implementation_not_available(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.google_sheets.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -5,7 +5,7 @@ import http
from http import HTTPStatus
import json
import time
from unittest.mock import Mock
from unittest.mock import Mock, patch
from aiohttp import ClientError
from httplib2 import Response
@@ -15,6 +15,9 @@ from homeassistant.components.google_tasks import DOMAIN
from homeassistant.components.google_tasks.const import OAUTH2_TOKEN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from .conftest import LIST_TASK_LIST_RESPONSE, LIST_TASKS_RESPONSE_WATER
@@ -152,3 +155,20 @@ async def test_setup_error(
assert not await integration_setup()
assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_oauth_implementation_not_available(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.google_tasks.config_entry_oauth2_flow.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -24,6 +24,9 @@ from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERV
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from homeassistant.util import dt as dt_util
from . import setup_integration
@@ -722,3 +725,20 @@ async def test_websocket_watchdog(
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_automower_client.get_status.call_count == 2
async def test_oauth_implementation_not_available(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
mock_config_entry.add_to_hass(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -1,11 +1,14 @@
"""Tests for the iotty integration."""
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
from homeassistant.components.iotty.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from tests.common import MockConfigEntry
@@ -41,6 +44,23 @@ async def test_load_unload_coordinator_called(
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
async def test_oauth_implementation_not_available(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
mock_config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.iotty.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_load_unload_iottyproxy_called(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,

View File

@@ -0,0 +1,40 @@
"""Tests for the Honeywell Lyric integration."""
from unittest.mock import patch
from homeassistant.components.lyric.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from tests.common import MockConfigEntry
async def test_oauth_implementation_not_available(
hass: HomeAssistant,
) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
"auth_implementation": DOMAIN,
"token": {
"access_token": "mock-access-token",
"refresh_token": "mock-refresh-token",
"expires_at": 9999999999,
"token_type": "Bearer",
},
},
)
entry.add_to_hass(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -3,7 +3,11 @@
from unittest.mock import patch
from homeassistant.components.microbees.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from tests.common import MockConfigEntry
@@ -33,3 +37,20 @@ async def test_migrate_entry_minor_version_1_2(hass: HomeAssistant) -> None:
assert entry.version == 1
assert entry.minor_version == 2
assert entry.unique_id == "54321"
async def test_oauth_implementation_not_available(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
config_entry.add_to_hass(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -7613,6 +7613,7 @@
'game_pieces',
'minimum_iron',
'no_program',
'normal',
'outdoor_garments',
'outerwear',
'pillows',
@@ -7697,6 +7698,7 @@
'game_pieces',
'minimum_iron',
'no_program',
'normal',
'outdoor_garments',
'outerwear',
'pillows',
@@ -11419,6 +11421,7 @@
'game_pieces',
'minimum_iron',
'no_program',
'normal',
'outdoor_garments',
'outerwear',
'pillows',
@@ -11503,6 +11506,7 @@
'game_pieces',
'minimum_iron',
'no_program',
'normal',
'outdoor_garments',
'outerwear',
'pillows',

View File

@@ -7,8 +7,11 @@ from freezegun.api import FrozenDateTimeFactory
from monzopy import AuthorisationExpiredError
from homeassistant.components.monzo.const import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from . import setup_integration
@@ -61,3 +64,20 @@ async def test_migrate_entry_minor_version_1_2(hass: HomeAssistant) -> None:
assert entry.version == 1
assert entry.minor_version == 2
assert entry.unique_id == "600"
async def test_oauth_implementation_not_available(
hass: HomeAssistant,
polling_config_entry: MockConfigEntry,
) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
polling_config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.monzo.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(polling_config_entry.entry_id)
await hass.async_block_till_done()
assert polling_config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -3,7 +3,7 @@
from copy import deepcopy
import json
from typing import Any
from unittest.mock import call, patch
from unittest.mock import patch
import pytest
@@ -30,7 +30,6 @@ from homeassistant.components.vacuum import (
from homeassistant.const import CONF_NAME, ENTITY_MATCH_ALL, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from .common import (
help_custom_config,
@@ -64,11 +63,7 @@ from .common import (
from tests.common import async_fire_mqtt_message
from tests.components.vacuum import common
from tests.typing import (
MqttMockHAClientGenerator,
MqttMockPahoClient,
WebSocketGenerator,
)
from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient
COMMAND_TOPIC = "vacuum/command"
SEND_COMMAND_TOPIC = "vacuum/send_command"
@@ -87,27 +82,6 @@ DEFAULT_CONFIG = {
}
}
CONFIG_CLEAN_SEGMENTS_1 = {
mqtt.DOMAIN: {
vacuum.DOMAIN: {
"name": "test",
"unique_id": "veryunique",
"segments": ["Livingroom", "Kitchen"],
"clean_segments_command_topic": "vacuum/clean_segment",
}
}
}
CONFIG_CLEAN_SEGMENTS_2 = {
mqtt.DOMAIN: {
vacuum.DOMAIN: {
"name": "test",
"unique_id": "veryunique",
"segments": ["1.Livingroom", "2.Kitchen"],
"clean_segments_command_topic": "vacuum/clean_segment",
}
}
}
DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}}
CONFIG_ALL_SERVICES = help_custom_config(
@@ -320,347 +294,6 @@ async def test_command_without_command_topic(
mqtt_mock.async_publish.reset_mock()
@pytest.mark.parametrize("hass_config", [CONFIG_CLEAN_SEGMENTS_1])
async def test_clean_segments_initial_setup_without_repair_issue(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test cleanable segments initial setup does not fire repair flow."""
await mqtt_mock_entry()
issue_registry = ir.async_get(hass)
assert len(issue_registry.issues) == 0
@pytest.mark.parametrize("hass_config", [CONFIG_CLEAN_SEGMENTS_1])
async def test_clean_segments_command_without_id(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
entity_registry: er.EntityRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test cleanable segments without ID."""
config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
entity_registry.async_get_or_create(
vacuum.DOMAIN,
mqtt.DOMAIN,
"veryunique",
config_entry=config_entry,
suggested_object_id="test",
)
entity_registry.async_update_entity_options(
"vacuum.test",
vacuum.DOMAIN,
{
"area_mapping": {"Nabu Casa": ["Kitchen", "Livingroom"]},
"last_seen_segments": [
{"id": "Livingroom", "name": "Livingroom"},
{"id": "Kitchen", "name": "Kitchen"},
],
},
)
mqtt_mock = await mqtt_mock_entry()
await hass.async_block_till_done()
issue_registry = ir.async_get(hass)
# We do not expect a repair flow
assert len(issue_registry.issues) == 0
state = hass.states.get("vacuum.test")
assert state.state == STATE_UNKNOWN
await common.async_clean_area(hass, ["Nabu Casa"], entity_id="vacuum.test")
assert (
call("vacuum/clean_segment", '["Kitchen","Livingroom"]', 0, False)
in mqtt_mock.async_publish.mock_calls
)
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "vacuum/get_segments", "entity_id": "vacuum.test"}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"]["segments"] == [
{"id": "Livingroom", "name": "Livingroom", "group": None},
{"id": "Kitchen", "name": "Kitchen", "group": None},
]
@pytest.mark.parametrize("hass_config", [CONFIG_CLEAN_SEGMENTS_2])
async def test_clean_segments_command_with_id(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
entity_registry: er.EntityRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test cleanable segments with ID."""
mqtt_mock = await mqtt_mock_entry()
# Set the area mapping
entity_registry.async_update_entity_options(
"vacuum.test",
vacuum.DOMAIN,
{
"area_mapping": {"Livingroom": ["1"], "Kitchen": ["2"]},
"last_seen_segments": [
{"id": "1", "name": "Livingroom"},
{"id": "2", "name": "Kitchen"},
],
},
)
await hass.async_block_till_done()
state = hass.states.get("vacuum.test")
assert state.state == STATE_UNKNOWN
await common.async_clean_area(hass, ["Kitchen"], entity_id="vacuum.test")
assert (
call("vacuum/clean_segment", '["2"]', 0, False)
in mqtt_mock.async_publish.mock_calls
)
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "vacuum/get_segments", "entity_id": "vacuum.test"}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"]["segments"] == [
{"id": "1", "name": "Livingroom", "group": None},
{"id": "2", "name": "Kitchen", "group": None},
]
async def test_clean_segments_command_update(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
entity_registry: er.EntityRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test cleanable segments update via discovery."""
# Prepare original entity config entry
config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
entity_registry.async_get_or_create(
vacuum.DOMAIN,
mqtt.DOMAIN,
"veryunique",
config_entry=config_entry,
suggested_object_id="test",
)
entity_registry.async_update_entity_options(
"vacuum.test",
vacuum.DOMAIN,
{
"area_mapping": {"Livingroom": ["1"], "Kitchen": ["2"]},
"last_seen_segments": [
{"id": "1", "name": "Livingroom"},
{"id": "2", "name": "Kitchen"},
],
},
)
await mqtt_mock_entry()
# Do initial discovery
config1 = CONFIG_CLEAN_SEGMENTS_2[mqtt.DOMAIN][vacuum.DOMAIN]
payload1 = json.dumps(config1)
config_topic = "homeassistant/vacuum/bla/config"
async_fire_mqtt_message(hass, config_topic, payload1)
await hass.async_block_till_done()
state = hass.states.get("vacuum.test")
assert state.state == STATE_UNKNOWN
issue_registry = ir.async_get(hass)
# We do not expect a repair flow
assert len(issue_registry.issues) == 0
# Update the segments
config2 = config1.copy()
config2["segments"] = ["1.Livingroom", "2.Kitchen", "3.Diningroom"]
payload2 = json.dumps(config2)
async_fire_mqtt_message(hass, config_topic, payload2)
await hass.async_block_till_done()
# A repair flow should start
assert len(issue_registry.issues) == 1
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "vacuum/get_segments", "entity_id": "vacuum.test"}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"]["segments"] == [
{"id": "1", "name": "Livingroom", "group": None},
{"id": "2", "name": "Kitchen", "group": None},
{"id": "3", "name": "Diningroom", "group": None},
]
# Test update with a non-unique segment list fails
config3 = config1.copy()
config3["segments"] = ["1.Livingroom", "2.Kitchen", "2.Diningroom"]
payload3 = json.dumps(config3)
async_fire_mqtt_message(hass, config_topic, payload3)
await hass.async_block_till_done()
assert (
"Error 'The `segments` option contains an invalid or non-unique segment ID '2'"
in caplog.text
)
@pytest.mark.parametrize(
"hass_config",
[
{
mqtt.DOMAIN: {
vacuum.DOMAIN: {
"name": "test",
"unique_id": "veryunique",
"segments": ["Livingroom", "Kitchen", "Kitchen"],
"clean_segments_command_topic": "vacuum/clean_segment",
}
}
},
{
mqtt.DOMAIN: {
vacuum.DOMAIN: {
"name": "test",
"unique_id": "veryunique",
"segments": ["Livingroom", "Kitchen", ""],
"clean_segments_command_topic": "vacuum/clean_segment",
}
}
},
{
mqtt.DOMAIN: {
vacuum.DOMAIN: {
"name": "test",
"unique_id": "veryunique",
"segments": ["1.Livingroom", "1.Kitchen"],
"clean_segments_command_topic": "vacuum/clean_segment",
}
}
},
{
mqtt.DOMAIN: {
vacuum.DOMAIN: {
"name": "test",
"unique_id": "veryunique",
"segments": ["1.Livingroom", "1.Kitchen", ".Diningroom"],
"clean_segments_command_topic": "vacuum/clean_segment",
}
}
},
],
)
async def test_non_unique_segments(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test with non-unique list of cleanable segments with valid segment IDs."""
await mqtt_mock_entry()
assert (
"The `segments` option contains an invalid or non-unique segment ID"
in caplog.text
)
@pytest.mark.usefixtures("hass")
@pytest.mark.parametrize(
("hass_config", "error_message"),
[
(
help_custom_config(
vacuum.DOMAIN,
DEFAULT_CONFIG,
({"clean_segments_command_topic": "test-topic"},),
),
"Options `segments` and "
"`clean_segments_command_topic` must be defined together",
),
(
help_custom_config(
vacuum.DOMAIN,
DEFAULT_CONFIG,
({"segments": ["Livingroom"]},),
),
"Options `segments` and "
"`clean_segments_command_topic` must be defined together",
),
(
help_custom_config(
vacuum.DOMAIN,
DEFAULT_CONFIG,
(
{
"segments": ["Livingroom"],
"clean_segments_command_topic": "test-topic",
},
),
),
"Option `segments` requires `unique_id` to be configured",
),
],
)
async def test_clean_segments_config_validation(
mqtt_mock_entry: MqttMockHAClientGenerator,
error_message: str,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test status clean segment config validation."""
await mqtt_mock_entry()
assert error_message in caplog.text
@pytest.mark.parametrize(
"hass_config",
[
help_custom_config(
vacuum.DOMAIN,
CONFIG_CLEAN_SEGMENTS_2,
({"clean_segments_command_template": "{{ ';'.join(value) }}"},),
)
],
)
async def test_clean_segments_command_with_id_and_command_template(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
entity_registry: er.EntityRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test clean segments with command template."""
mqtt_mock = await mqtt_mock_entry()
entity_registry.async_update_entity_options(
"vacuum.test",
vacuum.DOMAIN,
{
"area_mapping": {"Livingroom": ["1"], "Kitchen": ["2"]},
"last_seen_segments": [
{"id": "1", "name": "Livingroom"},
{"id": "2", "name": "Kitchen"},
],
},
)
await hass.async_block_till_done()
state = hass.states.get("vacuum.test")
assert state.state == STATE_UNKNOWN
await common.async_clean_area(
hass, ["Livingroom", "Kitchen"], entity_id="vacuum.test"
)
assert (
call("vacuum/clean_segment", "1;2", 0, False)
in mqtt_mock.async_publish.mock_calls
)
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "vacuum/get_segments", "entity_id": "vacuum.test"}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"]["segments"] == [
{"id": "1", "name": "Livingroom", "group": None},
{"id": "2", "name": "Kitchen", "group": None},
]
@pytest.mark.parametrize("hass_config", [CONFIG_ALL_SERVICES])
async def test_status(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator