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/avea/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from bleak.exc import BleakError
import voluptuous as vol

from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
Expand Down Expand Up @@ -66,6 +67,15 @@ def _is_avea_discovery(discovery_info: BluetoothServiceInfoBleak) -> bool:
return AVEA_SERVICE_UUID in discovery_info.service_uuids


def _discovery_label(discovery_info: BluetoothServiceInfoBleak) -> str:
"""Return a label for a discovered Avea bulb."""
if (
name := _normalize_name(discovery_info.name)
) and name != discovery_info.address:
return f"{name} ({discovery_info.address})"
return discovery_info.address


class AveaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Avea."""

Expand Down Expand Up @@ -150,6 +160,7 @@ async def async_step_user(
if discovery := self._discovery_info:
self._discovered_devices[discovery.address] = discovery
else:
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery in async_discovered_service_info(self.hass):
if (
Expand All @@ -165,11 +176,10 @@ async def async_step_user(

if self._discovery_info:
disc = self._discovery_info
label = f"{disc.name or disc.address} ({disc.address})"
data_schema = vol.Schema(
{
vol.Required(CONF_ADDRESS, default=disc.address): vol.In(
{disc.address: label}
{disc.address: _discovery_label(disc)}
)
}
)
Expand All @@ -178,10 +188,7 @@ async def async_step_user(
{
vol.Required(CONF_ADDRESS): vol.In(
{
service_info.address: (
f"{service_info.name or service_info.address}"
f" ({service_info.address})"
)
service_info.address: _discovery_label(service_info)
for service_info in self._discovered_devices.values()
}
),
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/bluetooth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from habluetooth import (
BaseHaRemoteScanner,
BaseHaScanner,
BluetoothReachabilityIntent,
BluetoothScannerDevice,
BluetoothScanningMode,
HaBluetoothConnector,
Expand Down Expand Up @@ -55,6 +56,7 @@
from .api import (
_get_manager,
async_address_present,
async_address_reachability_diagnostics,
async_ble_device_from_address,
async_clear_address_from_match_history,
async_clear_advertisement_history,
Expand Down Expand Up @@ -108,12 +110,14 @@
"BluetoothCallback",
"BluetoothCallbackMatcher",
"BluetoothChange",
"BluetoothReachabilityIntent",
"BluetoothScannerDevice",
"BluetoothScanningMode",
"BluetoothServiceInfo",
"BluetoothServiceInfoBleak",
"HaBluetoothConnector",
"async_address_present",
"async_address_reachability_diagnostics",
"async_ble_device_from_address",
"async_clear_address_from_match_history",
"async_clear_advertisement_history",
Expand Down
9 changes: 9 additions & 0 deletions homeassistant/components/bluetooth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from bleak import BleakScanner
from habluetooth import (
BaseHaScanner,
BluetoothReachabilityIntent,
BluetoothScannerDevice,
BluetoothScanningMode,
HaBleakScannerWrapper,
Expand Down Expand Up @@ -108,6 +109,14 @@ def async_ble_device_from_address(
return _get_manager(hass).async_ble_device_from_address(address, connectable)


@hass_callback
def async_address_reachability_diagnostics(
hass: HomeAssistant, address: str, intent: BluetoothReachabilityIntent
) -> str:
"""Return a human readable explanation of why an address may be unreachable."""
return _get_manager(hass).async_address_reachability_diagnostics(address, intent)


@hass_callback
def async_scanner_devices_by_address(
hass: HomeAssistant, address: str, connectable: bool = True
Expand Down
13 changes: 13 additions & 0 deletions homeassistant/components/esphome/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,19 @@ def _on_static_info_update(self, static_info: EntityInfo) -> None:
UpdateDeviceClass, static_info.device_class
)

def version_is_newer(self, latest_version: str, installed_version: str) -> bool:
"""Return True if latest_version is newer than installed_version.

ESPHome project versions can carry a build suffix (e.g.
2025.11.5_c51f7548) that AwesomeVersion cannot parse. Without stripping
it the base comparison raises and the entity is forced on for every
build mismatch. Drop the suffix so the versions compare cleanly and we
only report genuinely newer firmware.
"""
return super().version_is_newer(
latest_version.partition("_")[0], installed_version.partition("_")[0]
)

@property
@esphome_state_property
def installed_version(self) -> str:
Expand Down
46 changes: 44 additions & 2 deletions homeassistant/components/immich/media_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
from aioimmich.assets.models import ImmichAsset
from aioimmich.exceptions import ImmichError
from aioimmich.exceptions import ImmichError, ImmichForbiddenError

from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_player import BrowseError, MediaClass
Expand Down Expand Up @@ -79,7 +79,7 @@ async def async_browse_media(
],
)

async def _async_build_immich(
async def _async_build_immich( # noqa: C901
self, item: MediaSourceItem, entries: list[ConfigEntry]
) -> list[BrowseMediaSource]:
"""Handle browsing different immich instances."""
Expand Down Expand Up @@ -137,6 +137,12 @@ async def _async_build_immich(
LOGGER.debug("Render all albums for %s", entry.title)
try:
albums = await immich_api.albums.async_get_all_albums()
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="missing_api_permission",
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []

Expand All @@ -158,6 +164,12 @@ async def _async_build_immich(
LOGGER.debug("Render all tags for %s", entry.title)
try:
tags = await immich_api.tags.async_get_all_tags()
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="missing_api_permission",
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []

Expand All @@ -178,6 +190,12 @@ async def _async_build_immich(
LOGGER.debug("Render all people for %s", entry.title)
try:
people = await immich_api.people.async_get_all_people()
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="missing_api_permission",
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []

Expand Down Expand Up @@ -211,6 +229,12 @@ async def _async_build_immich(
identifier.collection_id
)
assets = album_info.assets
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="missing_api_permission",
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []

Expand All @@ -223,6 +247,12 @@ async def _async_build_immich(
assets = await immich_api.search.async_get_all_by_tag_ids(
[identifier.collection_id]
)
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="missing_api_permission",
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []

Expand All @@ -235,12 +265,24 @@ async def _async_build_immich(
assets = await immich_api.search.async_get_all_by_person_ids(
[identifier.collection_id]
)
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="missing_api_permission",
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
elif identifier.collection == "favorites":
LOGGER.debug("Render all assets for favorites collection")
try:
assets = await immich_api.search.async_get_all_favorites()
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="missing_api_permission",
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []

Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/immich/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@
"identifier_unresolvable": {
"message": "Could not parse identifier: {identifier}"
},
"missing_api_permission": {
"message": "Missing API permission ({msg})."
},
"not_configured": {
"message": "Immich is not configured."
},
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/lifx/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@
"iot_class": "local_polling",
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
"requirements": [
"aiolifx==1.2.1",
"aiolifx==1.2.2",
"aiolifx-effects==0.3.2",
"aiolifx-themes==1.0.2"
"aiolifx-themes==1.0.4"
]
}
13 changes: 8 additions & 5 deletions homeassistant/components/proxmoxve/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,14 @@

def _get_nodes_data(data: dict[str, Any]) -> list[dict[str, Any]]:
"""Validate the user input and fetch data (sync, for executor)."""
auth_kwargs = {
"password": data.get(CONF_PASSWORD),
}
if data.get(CONF_TOKEN):
auth_kwargs = {
auth_kwargs = (
{
"token_name": data[CONF_TOKEN_ID],
"token_value": data[CONF_TOKEN_SECRET],
}
if data.get(CONF_TOKEN)
else {"password": data.get(CONF_PASSWORD)}
)
data = sanitize_config_entry(data)
try:
client = ProxmoxAPI(
Expand Down Expand Up @@ -122,6 +122,9 @@ def _get_nodes_data(data: dict[str, Any]) -> list[dict[str, Any]]:
except requests.exceptions.ConnectionError as err:
raise ProxmoxConnectionError from err

if not nodes:
raise ProxmoxNoNodesFound("No nodes found")

nodes_data: list[dict[str, Any]] = []
for node in nodes:
if node.get("status") != NODE_ONLINE:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/thread/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/thread",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.10.0", "pyroute2==0.7.5"],
"requirements": ["python-otbr-api==2.10.0", "pyroute2==0.9.6"],
"single_config_entry": true,
"zeroconf": ["_meshcop._udp.local."]
}
6 changes: 3 additions & 3 deletions requirements_all.txt

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

Loading
Loading