Compare commits

..

94 Commits

Author SHA1 Message Date
Franck Nijhof 7e62ff35fd 2026.6.4 (#174308) 2026-06-19 22:57:25 +02:00
TheJulianJES 0a5c1ef8eb Bump dsmr-parser to 1.9.0 (#174307) 2026-06-19 18:59:16 +00:00
Michael a88093afd2 Bump aioimmich to 0.15.0 (#174305) 2026-06-19 18:55:13 +00:00
J. Nick Koston 9fd90283b3 Bump pyroute2 to 0.9.6 (#172521) 2026-06-19 18:55:11 +00:00
Franck Nijhof 3159242b68 Bump version to 2026.6.4 2026-06-19 18:26:51 +00:00
Simone Chemelli f5985b03e4 Remove event entities from virtual groups for Alexa Devices (#174303) 2026-06-19 18:25:53 +00:00
Franck Nijhof 040a3bcb10 Cast Xiaomi Gateway sub-device firmware version to string (#174294) 2026-06-19 18:25:52 +00:00
Bram Kragten 5aaf6704a9 Update frontend to 20260527.7 (#174285) 2026-06-19 18:25:50 +00:00
Franck Nijhof 2fcd00b301 Fix econet fan mode select returning int instead of str (#174274) 2026-06-19 18:25:48 +00:00
Franck Nijhof 0b439e6e4c Re-raise non-401 HTTP errors in Tank Utility setup (#174272) 2026-06-19 18:25:46 +00:00
Franck Nijhof d13a5b7eec Handle Weheat API errors during config flow entry creation (#174234) 2026-06-19 18:25:45 +00:00
Franck Nijhof de49716ec1 Skip fill sensor for Rituals diffusers without fill data (#174232) 2026-06-19 18:25:43 +00:00
Franck Nijhof 67c6921847 Include Sonos favorites in source list and gate SELECT_SOURCE dynamically (#174231) 2026-06-19 18:25:41 +00:00
Franck Nijhof 002b638013 Fix Elk-M1 reconfigure failing when entry has no unique_id (#174230) 2026-06-19 18:25:39 +00:00
Josef Zweck 4b60ed30c7 Update max prebrew numbers in lamarzocco (#174204) 2026-06-19 18:25:37 +00:00
Simone Chemelli 6f1deec507 Bump aiocomelit to 2.0.7 (#174183) 2026-06-19 18:25:35 +00:00
Franck Nijhof 227ba8032f Bump aiocomelit to 2.0.5 (#173800) 2026-06-19 18:25:34 +00:00
Bernát Gábor 7da3ecf033 Cast SwitchBot Cloud device sw_version to string (#174167) 2026-06-19 18:14:20 +00:00
Simone Chemelli 8b293a18d3 Bump aioamazondevices to 14.1.3 (#174158) 2026-06-19 18:14:18 +00:00
Simone Chemelli 3dc077f280 Fix stale routine entities removal for Alexa Devices (#174138) 2026-06-19 18:14:16 +00:00
Simone Chemelli d368a95323 Bump aioamazondevices to 14.1.2 (#174114)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-06-19 18:14:14 +00:00
Franck Nijhof 495f41a742 Filter out closed sites in Amber Electric config flow (#174084) 2026-06-19 18:12:01 +00:00
Franck Nijhof 9f7529706d Cast system version to string for simplisafe device model (#174081) 2026-06-19 18:11:59 +00:00
Franck Nijhof 8f6b1dff9c Bump opower to 0.18.5 (#174080) 2026-06-19 18:11:58 +00:00
Franck Nijhof f260a1bb7b Retry webdav setup on connection errors (#174077) 2026-06-19 18:11:56 +00:00
Franck Nijhof 157e137ea9 Cast numeric firmware to string for squeezebox hw_version (#174076) 2026-06-19 18:11:54 +00:00
Jan Bouwhuis b2e1a296d4 Fix MQTT discovery option unjustly added to entry data (#174073) 2026-06-19 18:11:52 +00:00
Franck Nijhof e78a2c9f01 Add missing subentry flow translations in scrape (#174006) 2026-06-19 18:11:50 +00:00
Franck Nijhof 9011225a42 Add missing flow form field translations in tractive (#174005) 2026-06-19 18:11:48 +00:00
Franck Nijhof 81ef9b99c2 Add missing flow form field translation in iskra (#174004) 2026-06-19 18:11:46 +00:00
Franck Nijhof fa0207698a Add missing flow form field translations in ecobee (#174002) 2026-06-19 18:11:44 +00:00
Franck Nijhof 275883a95a Add missing flow form field translation in airvisual (#174000) 2026-06-19 18:11:42 +00:00
Franck Nijhof ebd252a225 Fix flow form field translations in modem_callerid (#173999) 2026-06-19 18:11:40 +00:00
Franck Nijhof 2de6c0281d Fix flow form field translation key in sia (#173998) 2026-06-19 18:11:38 +00:00
Franck Nijhof f95671f0f4 Fix flow form field translations in local_calendar (#173997) 2026-06-19 18:11:36 +00:00
Franck Nijhof 5fcae9ecf7 Add missing flow form field translation in honeywell (#173996) 2026-06-19 18:11:35 +00:00
Franck Nijhof 0b86cfa496 Add missing flow form field translation in otp (#173994) 2026-06-19 18:11:33 +00:00
Franck Nijhof d45bdf37d5 Fix flow form field translations in hlk_sw16 (#173993) 2026-06-19 18:11:31 +00:00
Franck Nijhof a9205df4a3 Fix flow form field translation keys in here_travel_time (#173992) 2026-06-19 18:11:29 +00:00
John Pettitt c333744fd2 Add API_GEN_4 support to Subaru integration (#173956) 2026-06-19 18:11:27 +00:00
Assaf Inbal 2f64601990 Bump pyituran to 0.1.6 (#173833) 2026-06-19 18:11:25 +00:00
Franck Nijhof cbd35be271 Bump pyrainbird to 6.3.1 (#173786) 2026-06-19 18:07:34 +00:00
Franck Nijhof 92ac14f42a Bump aioamazondevices to 14.0.4 (#173761) 2026-06-19 18:07:32 +00:00
Franck Nijhof 45e568c73e Add missing flow form field translation in snooz (#173760) 2026-06-19 18:07:30 +00:00
Franck Nijhof a121b8d146 Add missing flow form field translation in motionblinds_ble (#173758) 2026-06-19 18:07:28 +00:00
Franck Nijhof a2bd7d5857 Add missing flow form field translation in blink (#173756) 2026-06-19 18:07:26 +00:00
Franck Nijhof a6e639377b Fix options flow form field translation key in plaato (#173755) 2026-06-19 18:07:24 +00:00
Franck Nijhof 2147a851c3 Fix flow form field translation key in meteoclimatic (#173754) 2026-06-19 18:07:22 +00:00
Franck Nijhof 9034afd29e Add missing flow form field translation in gogogate2 (#173753) 2026-06-19 18:07:20 +00:00
Franck Nijhof 5c5d259f63 Add missing flow form field translation in melnor (#173752) 2026-06-19 18:07:18 +00:00
Franck Nijhof cc16a9086f Fix flow form field translation key in lookin (#173751) 2026-06-19 18:07:16 +00:00
Franck Nijhof 5d1f8f770c Add missing flow form field translation in lacrosse_view (#173750) 2026-06-19 18:07:14 +00:00
Franck Nijhof cea6b9b0b7 Add missing flow form field translations in islamic_prayer_times (#173749) 2026-06-19 18:07:12 +00:00
G Johansson 77f7c26399 Bump lxml to 6.1.1 (#173748) 2026-06-19 18:07:10 +00:00
Franck Nijhof 8e0a5b258c Add missing flow form field translation in hue (#173747) 2026-06-19 18:07:08 +00:00
Franck Nijhof f8b942818c Add missing flow form field translation in flux_led (#173746) 2026-06-19 18:07:06 +00:00
Franck Nijhof 9660d12c77 Add missing flow form field translation in tuya (#173745) 2026-06-19 18:07:04 +00:00
Franck Nijhof 7f1533a6e1 Skip Miele fan set_percentage when already at the target step (#173725) 2026-06-19 18:07:02 +00:00
J. Nick Koston 336d9e9126 Bump aiodiscover to 3.3.2 (#173705) 2026-06-19 18:07:00 +00:00
J. Nick Koston 1dde2d918e Bump aiodiscover to 3.3.1 (#172882) 2026-06-19 18:06:58 +00:00
Åke Strandberg 34a6b0ca61 Add missing Miele dishwasher codes (#173662) 2026-06-19 17:50:28 +00:00
Raman Gupta e92286ecd6 Stop validating # of slots in zwave_js.set_credential action (#173644) 2026-06-19 17:50:26 +00:00
Franck Nijhof 82bb9748db Avoid leaking Immich API key in error logs (#173541) 2026-06-19 17:50:24 +00:00
Rob Bierbooms 68e5e58a1c Solve issue with double slash in url when writing data to InfluxDB (#173395) 2026-06-19 17:50:22 +00:00
johanzander f3e8403e9a Fix Growatt total_output_power 1000x too low with V1 API (#172474)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-19 17:50:20 +00:00
Franck Nijhof 28076bcad6 2026.6.3 (#173633) 2026-06-12 21:49:56 +02:00
Franck Nijhof ff25428e56 Fix undefined DOMAIN in image upload tests 2026-06-12 19:12:56 +00:00
Franck Nijhof 608acd422f Bump version to 2026.6.3 2026-06-12 18:33:03 +00:00
Franck Nijhof c860e83ec9 Disambiguate duplicate channel names in LG Netcast source list (#173560) 2026-06-12 18:32:03 +00:00
Franck Nijhof c9f3f4a265 Sort aliases in LLM prompts for stable prefix caching (#173558) 2026-06-12 18:32:01 +00:00
Franck Nijhof e346a801d1 Return enum values from config_entry_attr template function (#173554) 2026-06-12 18:31:59 +00:00
Franck Nijhof a5c193931f Fix Rituals Perfume Genie sw_version dict passed to device registry (#173552) 2026-06-12 18:31:57 +00:00
Franck Nijhof d273350db1 Suppress InsecureKeyLengthWarning in HTML5 push notifications (#173551) 2026-06-12 18:31:55 +00:00
Franck Nijhof 45f27b8b6e Fix Yale Smart Living panic button unique_id for multiple hubs (#173547) 2026-06-12 18:31:53 +00:00
Franck Nijhof d3208a420f Convert OpenGarage sw_version to string for device registry (#173546) 2026-06-12 18:31:51 +00:00
Franck Nijhof d0d35e380f Convert RainMachine hw_version to string for device registry (#173545) 2026-06-12 18:31:49 +00:00
Franck Nijhof 2735e58d7f Convert JPEG-incompatible image modes to RGB in image upload thumbnail generation (#173538) 2026-06-12 18:31:47 +00:00
Franck Nijhof ad3eab80c3 Fix iCloud RuntimeError on unload by running cancel in executor (#173537) 2026-06-12 18:31:45 +00:00
Franck Nijhof 18e5d284b4 Fix Hue grouped light icon by adding translation_key (#173536) 2026-06-12 18:31:43 +00:00
Franck Nijhof e5052eaf44 Fix Hue light level sensor crash on None value (#173532) 2026-06-12 18:31:41 +00:00
Ernst Klamer 62c2e8d2fd Bump bthome-ble to 3.23.4 (#173526) 2026-06-12 18:31:39 +00:00
Bram Kragten 1f505067dd Update frontend to 20260527.6 (#173522) 2026-06-12 18:31:37 +00:00
Stefan Agner 72875b3b5e Refresh preferred Thread border agent address on OTBR reconnect (#173508)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: puddly <32534428+puddly@users.noreply.github.com>
2026-06-12 18:31:35 +00:00
renovate[bot] 3be755e496 Update hassil to 3.7.0 (#173484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-12 18:31:33 +00:00
Michael Hansen 5285798052 Bump hassil to 3.6.0 (#173031) 2026-06-12 18:31:31 +00:00
Diogo Gomes da49e37946 Bump pytrydan to 1.0.2 (#173479) 2026-06-12 18:28:13 +00:00
Simone Chemelli 2f9de98f2d Bump aioamazondevices to 14.0.3 (#173478) 2026-06-12 18:28:09 +00:00
starkillerOG 383a6426fc Bump reolink_aio to 0.21.0 (#173477) 2026-06-12 18:28:06 +00:00
Robert Resch 5ed60cd057 Revert "Unify query token auth in http views" (#173466) 2026-06-12 18:28:04 +00:00
Tom Cassady a1250b7bfb Fix UniFi Protect ufp_set debug log printing UndefinedType for translation-key entities (#173460) 2026-06-12 18:28:02 +00:00
Simone Chemelli 240e5219ad Redact more fields in diagnostics for Alexa devices (#173446) 2026-06-12 18:28:00 +00:00
Simone Chemelli 418f352ce7 Change update interval for UptimeRobot (#173435) 2026-06-12 18:27:58 +00:00
Jan Bouwhuis 599967b1d8 Do not enable MQTT entities though discovery that were disabled by user (#173404) 2026-06-12 18:27:56 +00:00
Nikolai Rahimi ad82729357 Add debug logging for Mitsubishi Comfort polling failures (#173364) 2026-06-12 18:27:54 +00:00
155 changed files with 1567 additions and 620 deletions
@@ -37,6 +37,9 @@
"title": "Re-authenticate AirVisual"
},
"user": {
"data": {
"type": "Integration type"
},
"description": "Pick what type of AirVisual data you want to monitor.",
"title": "Configure AirVisual"
}
@@ -190,8 +190,11 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
await self._async_remove_device_stale(stale_devices)
self.previous_devices = current_devices
current_routines = {slugify(routine) for routine in self.api.routines}
if stale_routines := self.previous_routines - current_routines:
current_routines = {
f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}"
for routine in self.api.routines
}
if stale_routines := (self.previous_routines - current_routines):
await self._async_remove_routine_stale(stale_routines)
self.previous_routines = current_routines
@@ -225,17 +228,19 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
"""Remove stale routine."""
entity_registry = er.async_get(self.hass)
for routine in stale_routines:
_LOGGER.debug(
"Detected change in routines: routine %s removed",
routine,
)
for routine_unique_id in stale_routines:
entity_id = entity_registry.async_get_entity_id(
Platform.BUTTON,
DOMAIN,
f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}",
routine_unique_id,
)
if entity_id:
_LOGGER.debug(
"Detected change in routines: routine %s removed",
routine_unique_id.replace(
f"{slugify(self.config_entry.unique_id)}-", ""
),
)
entity_registry.async_remove(entity_id)
async def sync_history_state(self) -> None:
@@ -12,7 +12,18 @@ from homeassistant.helpers.device_registry import DeviceEntry
from .coordinator import AmazonConfigEntry
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME, CONF_NAME, "title"}
TO_REDACT = {
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
"access_token",
"adp_token",
"device_private_key",
"refresh_token",
"store_authentication_cookie",
"title",
"website_cookies",
}
async def async_get_config_entry_diagnostics(
@@ -2,13 +2,20 @@
from typing import Final
from homeassistant.components.event import EventEntity, EventEntityDescription
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
from homeassistant.components.event import (
DOMAIN as EVENT_DOMAIN,
EventEntity,
EventEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import _LOGGER
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .entity import AmazonEntity
from .utils import async_remove_entity_from_virtual_group
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -31,6 +38,11 @@ async def async_setup_entry(
"""Set up Alexa Devices events based on a config entry."""
coordinator = entry.runtime_data
# Remove voice event from virtual groups
await async_remove_entity_from_virtual_group(
hass, coordinator, EVENT_DOMAIN, "voice_event"
)
known_devices: set[str] = set()
def _check_device() -> None:
@@ -42,6 +54,7 @@ async def async_setup_entry(
AlexaVoiceEvent(coordinator, serial_num, event_desc)
for event_desc in EVENTS
for serial_num in new_devices
if coordinator.data[serial_num].device_family != SPEAKER_GROUP_FAMILY
)
_check_device()
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==14.0.0"]
"requirements": ["aioamazondevices==14.1.3"]
}
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry, alexa_api_call
from .entity import AmazonEntity
from .utils import async_remove_dnd_from_virtual_group, async_update_unique_id
from .utils import async_remove_entity_from_virtual_group, async_update_unique_id
PARALLEL_UPDATES = 1
@@ -58,7 +58,9 @@ async def async_setup_entry(
new_key = "dnd"
# Remove old DND switch from virtual groups
await async_remove_dnd_from_virtual_group(hass, coordinator, old_key)
await async_remove_entity_from_virtual_group(
hass, coordinator, SWITCH_DOMAIN, old_key
)
# Replace unique id for DND switch
await async_update_unique_id(hass, coordinator, SWITCH_DOMAIN, old_key, new_key)
@@ -8,7 +8,6 @@ from aioamazondevices.const.schedules import (
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.core import HomeAssistant
import homeassistant.helpers.entity_registry as er
@@ -38,23 +37,22 @@ async def async_update_unique_id(
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
async def async_remove_dnd_from_virtual_group(
async def async_remove_entity_from_virtual_group(
hass: HomeAssistant,
coordinator: AmazonDevicesCoordinator,
platform: str,
key: str,
) -> None:
"""Remove entity DND from virtual group."""
"""Remove entity from virtual group."""
entity_registry = er.async_get(hass)
for serial_num in coordinator.data:
unique_id = f"{serial_num}-{key}"
entity_id = entity_registry.async_get_entity_id(
DOMAIN, SWITCH_DOMAIN, unique_id
)
entity_id = entity_registry.async_get_entity_id(DOMAIN, platform, unique_id)
is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY
if entity_id and is_group:
entity_registry.async_remove(entity_id)
_LOGGER.debug("Removed DND switch from virtual group %s", entity_id)
_LOGGER.debug("Removed entity '%s' from virtual group", entity_id)
async def async_remove_unsupported_notification_sensors(
@@ -34,11 +34,13 @@ def generate_site_selector_name(site: Site) -> str:
def filter_sites(sites: list[Site]) -> list[Site]:
"""Deduplicates the list of sites."""
"""Filter out closed sites and deduplicate the list of sites."""
filtered: list[Site] = []
filtered_nmi: set[str] = set()
for site in sorted(sites, key=lambda site: site.status):
if site.status == SiteStatus.CLOSED:
continue
if site.status == SiteStatus.ACTIVE or site.nmi not in filtered_nmi:
filtered.append(site)
filtered_nmi.add(site.nmi)
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.5.0"]
"requirements": ["hassil==3.7.0"]
}
@@ -26,6 +26,12 @@
"description": "The credentials for {username} need to be updated",
"title": "Re-authenticate Blink"
},
"reconfigure": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
}
},
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
+18 -10
View File
@@ -1,19 +1,18 @@
"""The Brands integration."""
from collections import deque
from collections.abc import Container, Mapping
from http import HTTPStatus
import logging
from pathlib import Path
from random import SystemRandom
import time
from typing import Any, Final, override
from typing import Any, Final
from aiohttp import ClientError, web
from aiohttp import ClientError, hdrs, web
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.core import HomeAssistant, callback, valid_domain
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -109,18 +108,23 @@ def _read_brand_file(brand_dir: Path, image: str) -> bytes | None:
class _BrandsBaseView(HomeAssistantView):
"""Base view for serving brand images."""
use_query_token_for_auth = True
requires_auth = False
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the view."""
self._hass = hass
self._cache_dir = Path(hass.config.cache_path(DOMAIN))
@callback
@override
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
"""Return valid auth tokens, which can be used for query token authentication."""
return self._hass.data[DOMAIN]
def _authenticate(self, request: web.Request) -> None:
"""Authenticate the request using Bearer token or query token."""
access_tokens: deque[str] = self._hass.data[DOMAIN]
authenticated = (
request[KEY_AUTHENTICATED] or request.query.get("token") in access_tokens
)
if not authenticated:
if hdrs.AUTHORIZATION in request.headers:
raise web.HTTPUnauthorized
raise web.HTTPForbidden
async def _serve_from_custom_integration(
self,
@@ -236,6 +240,8 @@ class BrandsIntegrationView(_BrandsBaseView):
image: str,
) -> web.Response:
"""Handle GET request for an integration brand image."""
self._authenticate(request)
if not valid_domain(domain) or image not in ALLOWED_IMAGES:
return web.Response(status=HTTPStatus.NOT_FOUND)
@@ -268,6 +274,8 @@ class BrandsHardwareView(_BrandsBaseView):
image: str,
) -> web.Response:
"""Handle GET request for a hardware brand image."""
self._authenticate(request)
if not CATEGORY_RE.match(category):
return web.Response(status=HTTPStatus.NOT_FOUND)
# Hardware images have dynamic names like "manufacturer_model.png"
@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==3.23.2"]
"requirements": ["bthome-ble==3.23.4"]
}
+18 -14
View File
@@ -2,7 +2,7 @@
import asyncio
import collections
from collections.abc import Awaitable, Callable, Container, Coroutine, Mapping
from collections.abc import Awaitable, Callable, Coroutine
from contextlib import suppress
from dataclasses import asdict, dataclass
from datetime import datetime, timedelta
@@ -12,16 +12,16 @@ import logging
import os
from random import SystemRandom
import time
from typing import Any, Final, final, override
from typing import Any, Final, final
from aiohttp import web
from aiohttp import hdrs, web
import attr
from propcache.api import cached_property, under_cached_property
import voluptuous as vol
from webrtc_models import RTCIceCandidateInit
from homeassistant.components import websocket_api
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.components.media_player import (
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
@@ -776,26 +776,30 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
class CameraView(HomeAssistantView):
"""Base CameraView."""
use_query_token_for_auth = True
requires_auth = False
def __init__(self, component: EntityComponent[Camera]) -> None:
"""Initialize a basic camera view."""
self.component = component
@callback
@override
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
"""Return valid auth tokens, which can be used for query token authentication."""
if (camera := self.component.get_entity(match_info["entity_id"])) is None:
return ()
return camera.access_tokens
async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse:
"""Start a GET request."""
if (camera := self.component.get_entity(entity_id)) is None:
raise web.HTTPNotFound
authenticated = (
request[KEY_AUTHENTICATED]
or request.query.get("token") in camera.access_tokens
)
if not authenticated:
# Attempt with invalid bearer token, raise unauthorized
# so ban middleware can handle it.
if hdrs.AUTHORIZATION in request.headers:
raise web.HTTPUnauthorized
# Invalid sigAuth or camera access token
raise web.HTTPForbidden
if not camera.is_on:
_LOGGER.debug("Camera is off")
raise web.HTTPServiceUnavailable
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "platinum",
"requirements": ["aiocomelit==2.0.3"]
"requirements": ["aiocomelit==2.0.7"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.6.1"]
"requirements": ["hassil==3.7.0", "home-assistant-intents==2026.6.1"]
}
+2 -2
View File
@@ -16,7 +16,7 @@
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.7",
"aiodiscover==3.2.4",
"cached-ipaddress==1.1.1"
"aiodiscover==3.3.2",
"cached-ipaddress==1.1.2"
]
}
+1 -1
View File
@@ -8,5 +8,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["dsmr_parser"],
"requirements": ["dsmr-parser==1.7.0"]
"requirements": ["dsmr-parser==1.9.0"]
}
+3 -1
View File
@@ -30,7 +30,9 @@
},
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
"api_key": "[%key:common::config_flow::data::api_key%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"description": "Please enter the API key obtained from ecobee.com."
}
+2 -2
View File
@@ -39,12 +39,12 @@ class EconetFanModeSelect(EcoNetEntity[Thermostat], SelectEntity):
@property
def options(self) -> list[str]:
"""Return available select options."""
return [e.value for e in self._econet.fan_modes]
return [e.name for e in self._econet.fan_modes]
@property
def current_option(self) -> str:
"""Return current select option."""
return self._econet.fan_mode.value
return self._econet.fan_mode.name
def select_option(self, option: str) -> None:
"""Set the selected option."""
@@ -246,8 +246,8 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN):
)
if device is not None and device.mac_address:
await self.async_set_unique_id(dr.format_mac(device.mac_address))
# aborts if user tried to switch devices
self._abort_if_unique_id_mismatch()
if reconfigure_entry.unique_id is not None:
self._abort_if_unique_id_mismatch()
else:
# If we cannot confirm identity, keep existing
# behavior (don't block reconfigure)
@@ -255,6 +255,7 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_update_reload_and_abort(
reconfigure_entry,
unique_id=self.unique_id,
data_updates={
**reconfigure_entry.data,
CONF_HOST: info[CONF_HOST],
@@ -13,6 +13,11 @@
"discovery_confirm": {
"description": "Do you want to set up {model} {id} ({ipaddr})?"
},
"pick_device": {
"data": {
"device": "[%key:common::config_flow::data::device%]"
}
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260527.5"]
"requirements": ["home-assistant-frontend==20260527.7"]
}
@@ -11,6 +11,7 @@
"step": {
"user": {
"data": {
"device": "[%key:common::config_flow::data::device%]",
"ip_address": "[%key:common::config_flow::data::ip%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
@@ -189,7 +189,8 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
) from err
total_info["todayEnergy"] = total_info["today_energy"]
total_info["totalEnergy"] = total_info["total_energy"]
total_info["invTodayPpv"] = total_info["current_power"]
# V1 API returns current_power in kW, convert to W
total_info["invTodayPpv"] = total_info["current_power"] * 1000
else:
# Classic API: use plant_info as before.
# Copy the response to avoid mutating the dict returned by the library
@@ -15,7 +15,7 @@
},
"title": "[%key:component::here_travel_time::config::step::destination_menu::title%]"
},
"destination_entity_id": {
"destination_entity": {
"data": {
"destination_entity_id": "Destination using an entity"
},
@@ -34,7 +34,7 @@
},
"title": "[%key:component::here_travel_time::config::step::origin_menu::title%]"
},
"origin_entity_id": {
"origin_entity": {
"data": {
"origin_entity_id": "Origin using an entity"
},
@@ -12,8 +12,7 @@
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of your Hi-Link HLK-SW-16 device."
@@ -10,7 +10,8 @@
"step": {
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"description": "The Honeywell integration needs to re-authenticate your account",
"title": "[%key:common::config_flow::title::reauth%]"
+7 -2
View File
@@ -9,10 +9,12 @@ import time
from typing import TYPE_CHECKING, Any, cast
from urllib.parse import urlparse
import uuid
import warnings
from aiohttp import ClientError, ClientResponse, ClientSession, web
from aiohttp.hdrs import AUTHORIZATION
import jwt
from jwt.warnings import InsecureKeyLengthWarning
from py_vapid import Vapid
from pywebpush import WebPusher, WebPushException, webpush_async
import voluptuous as vol
@@ -325,7 +327,8 @@ class HTML5PushCallbackView(HomeAssistantView):
if target_check.get(ATTR_TARGET) in self.registrations:
possible_target = self.registrations[target_check[ATTR_TARGET]]
key = possible_target["subscription"]["keys"]["auth"]
with suppress(jwt.exceptions.DecodeError):
with suppress(jwt.exceptions.DecodeError), warnings.catch_warnings():
warnings.simplefilter("ignore", InsecureKeyLengthWarning)
return jwt.decode(token, key, algorithms=["ES256", "HS256"])
return self.json_message(
@@ -585,7 +588,9 @@ def add_jwt(timestamp: int, target: str, tag: str, jwt_secret: str) -> str:
ATTR_TARGET: target,
ATTR_TAG: tag,
}
return jwt.encode(jwt_claims, jwt_secret)
with warnings.catch_warnings():
warnings.simplefilter("ignore", InsecureKeyLengthWarning)
return jwt.encode(jwt_claims, jwt_secret)
async def async_setup_entry(
+2 -1
View File
@@ -18,7 +18,8 @@
"step": {
"init": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
"host": "[%key:common::config_flow::data::host%]",
"id": "Hue bridge"
},
"data_description": {
"host": "The hostname or IP address of your Hue bridge."
+1
View File
@@ -85,6 +85,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
entity_description = LightEntityDescription(
key="hue_grouped_light",
translation_key="hue_grouped_light",
has_entity_name=True,
name=None,
)
+3 -1
View File
@@ -166,8 +166,10 @@ class HueLightLevelSensor(HueSensorBase):
)
@property
def native_value(self) -> int:
def native_value(self) -> int | None:
"""Return the value reported by the sensor."""
if self.resource.light.value is None:
return None
# Light level in 10000 log10 (lux) +1 measured by sensor. Logarithm
# scale used because the human eye adjusts to light levels and small
# changes at low lux levels are more noticeable than at high lux
+3 -2
View File
@@ -59,7 +59,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bo
await hass.async_add_executor_job(account.setup)
entry.runtime_data = account
entry.async_on_unload(account.cancel_fetch)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -68,4 +67,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bo
async def async_unload_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
await hass.async_add_executor_job(entry.runtime_data.cancel_fetch)
return unload_ok
+23 -19
View File
@@ -2,21 +2,20 @@
import asyncio
import collections
from collections.abc import Container, Mapping
from contextlib import suppress
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
import os
from random import SystemRandom
from typing import Final, final, override
from typing import Final, final
from aiohttp import web
from aiohttp import hdrs, web
import httpx
from propcache.api import cached_property
import voluptuous as vol
from homeassistant.components.http import KEY_HASS, HomeAssistantView
from homeassistant.components.http import KEY_AUTHENTICATED, KEY_HASS, HomeAssistantView
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONTENT_TYPE_MULTIPART, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import (
@@ -315,28 +314,33 @@ class ImageView(HomeAssistantView):
"""View to serve an image."""
name = "api:image:image"
use_query_token_for_auth = True
requires_auth = False
url = "/api/image_proxy/{entity_id}"
def __init__(self, component: EntityComponent[ImageEntity]) -> None:
"""Initialize an image view."""
self.component = component
@callback
@override
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
"""Return valid auth tokens, which can be used for query token authentication."""
if (image_entity := self.component.get_entity(match_info["entity_id"])) is None:
return ()
return image_entity.access_tokens
@callback
def _get_image_entity(self, entity_id: str) -> ImageEntity:
"""Get image entity from request."""
async def _authenticate_request(
self, request: web.Request, entity_id: str
) -> ImageEntity:
"""Authenticate request and return image entity."""
if (image_entity := self.component.get_entity(entity_id)) is None:
raise web.HTTPNotFound
authenticated = (
request[KEY_AUTHENTICATED]
or request.query.get("token") in image_entity.access_tokens
)
if not authenticated:
# Attempt with invalid bearer token, raise unauthorized
# so ban middleware can handle it.
if hdrs.AUTHORIZATION in request.headers:
raise web.HTTPUnauthorized
# Invalid sigAuth or image entity access token
raise web.HTTPForbidden
return image_entity
async def head(self, request: web.Request, entity_id: str) -> web.Response:
@@ -345,7 +349,7 @@ class ImageView(HomeAssistantView):
This is sent by some DLNA renderers, like Samsung ones, prior to sending
the GET request.
"""
image_entity = self._get_image_entity(entity_id)
image_entity = await self._authenticate_request(request, entity_id)
# Don't use `handle` as we don't care about the stream case, we only want
# to verify that the image exists.
@@ -361,7 +365,7 @@ class ImageView(HomeAssistantView):
async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse:
"""Start a GET request."""
image_entity = self._get_image_entity(entity_id)
image_entity = await self._authenticate_request(request, entity_id)
return await self.handle(request, image_entity)
async def handle(
@@ -248,7 +248,10 @@ def _generate_thumbnail_if_file_does_not_exist(
if not target_file.is_file():
image = ImageOps.exif_transpose(Image.open(original_path))
image.thumbnail(target_size)
image.save(target_path, format=content_type.partition("/")[-1])
save_format = content_type.partition("/")[-1]
if save_format == "jpeg" and image.mode not in ("RGB", "L", "CMYK"):
image = image.convert("RGB")
image.save(target_path, format=save_format)
def _validate_size_from_filename(filename: str) -> tuple[int, int]:
@@ -81,6 +81,7 @@ class ImmichDataUpdateCoordinator(DataUpdateCoordinator[ImmichData]):
async def _async_setup(self) -> None:
"""Handle setup of the coordinator."""
try:
await self.api.async_setup()
user_info = await self.api.users.async_get_my_user()
except ImmichUnauthorizedError as err:
raise ConfigEntryAuthFailed(
@@ -119,7 +120,7 @@ class ImmichDataUpdateCoordinator(DataUpdateCoordinator[ImmichData]):
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error",
translation_placeholders={"error": repr(err)},
translation_placeholders={"error": str(err)},
) from err
return ImmichData(
@@ -9,5 +9,5 @@
"iot_class": "local_polling",
"loggers": ["aioimmich"],
"quality_scale": "platinum",
"requirements": ["aioimmich==0.14.1"]
"requirements": ["aioimmich==0.15.0"]
}
@@ -225,10 +225,9 @@ class ImmichMediaSource(MediaSource):
entry.title,
)
try:
album_info = await immich_api.albums.async_get_album_info(
identifier.collection_id
assets = await immich_api.search.async_get_all_by_album_ids(
[identifier.collection_id]
)
assets = album_info.assets
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
+1 -1
View File
@@ -53,7 +53,7 @@ async def _async_upload_file(service_call: ServiceCall) -> None:
if target_album := service_call.data.get(CONF_ALBUM_ID):
try:
await coordinator.api.albums.async_get_album_info(target_album, True)
await coordinator.api.albums.async_get_album_info(target_album)
except ImmichError as ex:
raise ServiceValidationError(
translation_domain=DOMAIN,
@@ -423,7 +423,7 @@ def get_influx_connection( # noqa: C901
if CONF_HOST in conf:
kwargs[CONF_HOST] = conf[CONF_HOST]
if (path := conf.get(CONF_PATH)) is not None:
if (path := conf.get(CONF_PATH)) is not None and path != "/":
kwargs[CONF_PATH] = path
if (port := conf.get(CONF_PORT)) is not None:
+2 -1
View File
@@ -31,7 +31,8 @@
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
"host": "[%key:common::config_flow::data::host%]",
"protocol": "Protocol"
},
"data_description": {
"host": "Hostname or IP address of your Iskra device."
@@ -5,6 +5,10 @@
},
"step": {
"user": {
"data": {
"location": "[%key:common::config_flow::data::location%]",
"name": "[%key:common::config_flow::data::name%]"
},
"description": "Do you want to set up Islamic Prayer Times?",
"title": "Set up Islamic Prayer Times"
}
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["pyituran==0.1.5"]
"requirements": ["pyituran==0.1.6"]
}
@@ -10,6 +10,11 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"location": {
"data": {
"location": "[%key:common::config_flow::data::location%]"
}
},
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
@@ -121,7 +121,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS,
native_min_value=0,
native_max_value=10,
native_max_value=30,
entity_category=EntityCategory.CONFIG,
set_value_fn=(
lambda machine, value: machine.set_pre_extraction_times(
@@ -163,7 +163,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS,
native_min_value=0,
native_max_value=10,
native_max_value=30,
entity_category=EntityCategory.CONFIG,
set_value_fn=(
lambda machine, value: machine.set_pre_extraction_times(
@@ -207,7 +207,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS,
native_min_value=0,
native_max_value=10,
native_max_value=30,
entity_category=EntityCategory.CONFIG,
set_value_fn=(
lambda machine, value: machine.set_pre_extraction_times(
@@ -1,5 +1,6 @@
"""Support for LG TV running on NetCast 3 or 4."""
from collections import Counter
from datetime import datetime
from typing import TYPE_CHECKING, Any
@@ -133,13 +134,22 @@ class LgTVDevice(MediaPlayerEntity):
channel_list = client.query_data("channel_list")
if channel_list:
channel_names = []
channel_pairs = []
for channel in channel_list:
channel_name = channel.find("chname")
if channel_name is not None:
channel_names.append(str(channel_name.text))
self._sources = dict(zip(channel_names, channel_list, strict=False))
# sort source names by the major channel number
channel_pairs.append((str(channel_name.text), channel))
name_count = Counter(name for name, _ in channel_pairs)
self._sources = {}
for name, channel in channel_pairs:
if name_count[name] > 1:
major = channel.find("major")
if major is not None:
name = f"{name} ({major.text})"
self._sources[name] = channel
source_tuples = [
(k, source.find("major").text)
for k, source in self._sources.items()
@@ -7,7 +7,10 @@
"invalid_ics_file": "There was a problem reading the calendar information. See the error log for additional details."
},
"step": {
"import": {
"import_ics_file": {
"data": {
"ics_file": "ICS file"
},
"description": "You can import events in iCal format (.ics file)."
},
"user": {
+1 -1
View File
@@ -23,7 +23,7 @@
},
"user": {
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]"
"host": "[%key:common::config_flow::data::host%]"
}
}
}
@@ -2,7 +2,7 @@
import asyncio
import collections
from collections.abc import Callable, Container, Mapping
from collections.abc import Callable
from contextlib import suppress
import datetime as dt
from enum import StrEnum
@@ -12,7 +12,7 @@ import hashlib
from http import HTTPStatus
import logging
import secrets
from typing import Any, Final, Required, TypedDict, final, override
from typing import Any, Final, Required, TypedDict, final
from urllib.parse import quote, urlparse
import aiohttp
@@ -24,7 +24,7 @@ import voluptuous as vol
from yarl import URL
from homeassistant.components import websocket_api
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.components.websocket_api import ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( # noqa: F401
@@ -50,7 +50,7 @@ from homeassistant.const import ( # noqa: F401
STATE_PLAYING,
STATE_STANDBY,
)
from homeassistant.core import HomeAssistant, SupportsResponse, callback
from homeassistant.core import HomeAssistant, SupportsResponse
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity, EntityDescription
@@ -1248,7 +1248,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
class MediaPlayerImageView(HomeAssistantView):
"""Media player view to serve an image."""
use_query_token_for_auth = True
requires_auth = False
url = "/api/media_player_proxy/{entity_id}"
name = "api:media_player:image"
extra_urls = [
@@ -1261,15 +1261,6 @@ class MediaPlayerImageView(HomeAssistantView):
"""Initialize a media player view."""
self.component = component
@callback
@override
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
"""Return valid auth tokens, which can be used for query token authentication."""
if (player := self.component.get_entity(match_info["entity_id"])) is None:
return ()
return (player.access_token,)
async def get(
self,
request: web.Request,
@@ -1279,9 +1270,21 @@ class MediaPlayerImageView(HomeAssistantView):
) -> web.Response:
"""Start a get request."""
if (player := self.component.get_entity(entity_id)) is None:
return web.Response(status=HTTPStatus.NOT_FOUND)
status = (
HTTPStatus.NOT_FOUND
if request[KEY_AUTHENTICATED]
else HTTPStatus.UNAUTHORIZED
)
return web.Response(status=status)
assert isinstance(player, MediaPlayerEntity)
authenticated = (
request[KEY_AUTHENTICATED]
or request.query.get("token") == player.access_token
)
if not authenticated:
return web.Response(status=HTTPStatus.UNAUTHORIZED)
if media_content_type and media_content_id:
media_image_id = request.query.get("media_image_id")
@@ -8,6 +8,11 @@
"bluetooth_confirm": {
"description": "Do you want to add the Melnor Bluetooth valve `{name}` to Home Assistant?",
"title": "Discovered Melnor Bluetooth valve"
},
"pick_device": {
"data": {
"address": "[%key:common::config_flow::data::device%]"
}
}
}
},
@@ -10,10 +10,10 @@
"step": {
"user": {
"data": {
"code": "Station code"
"station_code": "Station code"
},
"data_description": {
"code": "Looks like ESCAT4300000043206B"
"station_code": "Looks like ESCAT4300000043206B"
}
}
}
+4 -3
View File
@@ -508,19 +508,20 @@ class DishWasherProgramId(MieleEnum, missing_to_none=True):
solar_save = 9, 34
gentle = 10, 35, 210
extra_quiet = 11, 36, 207
hygiene = 12, 37
quick_power_wash = 13, 38
hygiene = 12, 37, 206
quick_power_wash = 13, 38, 216
pasta_paela = 14
tall_items = 17, 42
glasses_warm = 19
quick_intense = 21
normal = 23, 30
normal = 23, 30, 217
pre_wash = 24
pot_rests_and_filters = 25
power_wash = 44, 204
comfort_wash = 203
comfort_wash_plus = 209
rinse_salt = 215
rinse_and_hold = 219
class TumbleDryerProgramId(MieleEnum, missing_to_none=True):
+2
View File
@@ -135,6 +135,8 @@ class MieleFan(MieleEntity, FanEntity):
_LOGGER.debug("Calc ventilation_step: %s", ventilation_step)
if ventilation_step == 0:
await self.async_turn_off()
elif ventilation_step == self.device.state_ventilation_step:
return
else:
try:
await self.api.send_action(
@@ -791,6 +791,7 @@
"rice_pudding_rapid_steam_cooking": "Rice pudding (rapid steam cooking)",
"rice_pudding_steam_cooking": "Rice pudding (steam cooking)",
"rinse": "Rinse",
"rinse_and_hold": "Rinse and hold",
"rinse_out_lint": "Rinse out lint",
"rinse_salt": "Rinse salt",
"risotto": "Risotto",
@@ -42,12 +42,23 @@ class MitsubishiComfortCoordinator(DataUpdateCoordinator[IndoorUnit | KumoStatio
try:
success = await self.device.update_status()
except Exception as err:
# The user-facing UpdateFailed message is translated and omits the IP;
# log it here so the failing address is visible in debug logs.
_LOGGER.debug(
"Error polling %s at %s: %s",
self.device.name,
self.device.address,
err,
)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="communication_error",
translation_placeholders={"device_name": self.device.name},
) from err
if not success:
_LOGGER.debug(
"%s at %s returned no data", self.device.name, self.device.address
)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
@@ -14,8 +14,7 @@
},
"user": {
"data": {
"name": "[%key:common::config_flow::data::name%]",
"port": "[%key:common::config_flow::data::port%]"
"device": "[%key:common::config_flow::data::device%]"
},
"description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call."
}
@@ -10,6 +10,9 @@
},
"step": {
"confirm": {
"data": {
"blind_type": "Blind type"
},
"description": "What kind of blind is {display_name}?"
},
"user": {
@@ -4179,7 +4179,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
CONF_PROTOCOL: DEFAULT_PROTOCOL,
CONF_USERNAME: addon_discovery_config.get(CONF_USERNAME),
CONF_PASSWORD: addon_discovery_config.get(CONF_PASSWORD),
CONF_DISCOVERY: DEFAULT_DISCOVERY,
}
except AddonError:
# We do not have discovery information yet
@@ -4420,7 +4419,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
CONF_PROTOCOL: DEFAULT_PROTOCOL,
CONF_USERNAME: data.get(CONF_USERNAME),
CONF_PASSWORD: data.get(CONF_PASSWORD),
CONF_DISCOVERY: DEFAULT_DISCOVERY,
},
)
+3 -2
View File
@@ -1432,9 +1432,10 @@ class MqttEntity(
if (
self._config[CONF_ENABLED_BY_DEFAULT]
and deleted_entry
and deleted_entry.disabled_by is not None
and deleted_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
):
# Enable previous deleted entity and enable it
# Enable previous deleted entity and enable it,
# if it was not disabled by the user
recreated_entry = entity_registry.async_get_or_create(
entity_platform, DOMAIN, self.unique_id
)
@@ -52,5 +52,5 @@ class OpenGarageEntity(CoordinatorEntity[OpenGarageDataUpdateCoordinator]):
manufacturer="Open Garage",
name=self.coordinator.data["name"],
suggested_area="Garage",
sw_version=self.coordinator.data["fwv"],
sw_version=str(self.coordinator.data["fwv"]),
)
@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "platinum",
"requirements": ["opower==0.18.4"]
"requirements": ["opower==0.18.5"]
}
+2 -1
View File
@@ -11,7 +11,8 @@
"step": {
"confirm": {
"data": {
"code": "Verification code (OTP)"
"code": "Verification code (OTP)",
"qr_code": "QR code"
},
"data_description": {
"code": "The six-digit code currently displayed in your authentication app."
+1 -1
View File
@@ -41,7 +41,7 @@
"step": {
"user": {
"data": {
"update_interval": "Update interval (minutes)"
"scan_interval": "Update interval (minutes)"
},
"description": "Set the update interval (minutes)",
"title": "Options for Plaato"
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pyrainbird"],
"requirements": ["pyrainbird==6.3.0"]
"requirements": ["pyrainbird==6.3.1"]
}
@@ -54,7 +54,7 @@ class RainMachineEntity(CoordinatorEntity[RainMachineDataUpdateCoordinator]):
connections={(dr.CONNECTION_NETWORK_MAC, self._data.controller.mac)},
name=self._data.controller.name.capitalize(),
manufacturer="RainMachine",
hw_version=self._version_coordinator.data["hwVer"],
hw_version=str(self._version_coordinator.data["hwVer"]),
sw_version=f"{self._version_coordinator.data['swVer']} "
f"(API: {self._version_coordinator.data['apiVer']})",
)
@@ -20,5 +20,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.20.1"]
"requirements": ["reolink-aio==0.21.0"]
}
@@ -1,5 +1,7 @@
"""Base class for Rituals Perfume Genie diffuser entity."""
from typing import Any
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -12,6 +14,12 @@ MODEL = "The Perfume Genie"
MODEL2 = "The Perfume Genie 2.0"
def _version_string(version: Any) -> str:
if isinstance(version, dict):
return str(version.get("title", version))
return str(version)
class DiffuserEntity(CoordinatorEntity[RitualsDataUpdateCoordinator]):
"""Representation of a diffuser entity."""
@@ -31,7 +39,7 @@ class DiffuserEntity(CoordinatorEntity[RitualsDataUpdateCoordinator]):
manufacturer=MANUFACTURER,
model=MODEL if coordinator.diffuser.has_battery else MODEL2,
name=coordinator.diffuser.name,
sw_version=coordinator.diffuser.version,
sw_version=_version_string(coordinator.diffuser.version),
)
@property
@@ -40,6 +40,7 @@ ENTITY_DESCRIPTIONS = (
key="fill",
translation_key="fill",
value_fn=lambda diffuser: diffuser.fill,
has_fn=lambda diffuser: "fillc" in diffuser.hub_data.get("sensors", {}),
),
RitualsSensorEntityDescription(
key="perfume",
@@ -6,5 +6,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/scrape",
"iot_class": "cloud_polling",
"requirements": ["beautifulsoup4==4.13.3", "lxml==6.0.1"]
"requirements": ["beautifulsoup4==4.13.3", "lxml==6.1.1"]
}
@@ -58,6 +58,38 @@
"user": "Add sensor"
},
"step": {
"reconfigure": {
"data": {
"index": "[%key:component::scrape::config_subentries::entity::step::user::data::index%]",
"select": "[%key:component::scrape::config_subentries::entity::step::user::data::select%]"
},
"data_description": {
"index": "[%key:component::scrape::config_subentries::entity::step::user::data_description::index%]",
"select": "[%key:component::scrape::config_subentries::entity::step::user::data_description::select%]"
},
"sections": {
"advanced": {
"data": {
"attribute": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::attribute%]",
"availability": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::availability%]",
"device_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::device_class%]",
"state_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::state_class%]",
"unit_of_measurement": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::unit_of_measurement%]",
"value_template": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::value_template%]"
},
"data_description": {
"attribute": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::attribute%]",
"availability": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::availability%]",
"device_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::device_class%]",
"state_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::state_class%]",
"unit_of_measurement": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::unit_of_measurement%]",
"value_template": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::value_template%]"
},
"description": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::description%]",
"name": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::name%]"
}
}
},
"user": {
"data": {
"index": "Index",
+1 -1
View File
@@ -13,7 +13,7 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"additional_account": {
"add_account": {
"data": {
"account": "[%key:component::sia::config::step::user::data::account%]",
"additional_account": "[%key:component::sia::config::step::user::data::additional_account%]",
@@ -247,7 +247,7 @@ def _async_register_base_station(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, str(system.system_id))},
manufacturer="SimpliSafe",
model=system.version,
model=str(system.version),
name=system.address,
)
+2 -1
View File
@@ -18,7 +18,8 @@
},
"user": {
"data": {
"address": "[%key:common::config_flow::data::device%]"
"address": "[%key:common::config_flow::data::device%]",
"name": "[%key:common::config_flow::data::name%]"
},
"description": "[%key:component::bluetooth::config::step::user::description%]"
}
+26 -7
View File
@@ -64,6 +64,7 @@ from .const import (
MODELS_TV_ONLY,
PLAYABLE_MEDIA_TYPES,
SONOS_CREATE_MEDIA_PLAYER,
SONOS_FAVORITES_UPDATED,
SONOS_MEDIA_UPDATED,
SONOS_STATE_PLAYING,
SONOS_STATE_TRANSITIONING,
@@ -131,7 +132,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
| MediaPlayerEntityFeature.REPEAT_SET
| MediaPlayerEntityFeature.SEARCH_MEDIA
| MediaPlayerEntityFeature.SEEK
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.SHUFFLE_SET
| MediaPlayerEntityFeature.STOP
| MediaPlayerEntityFeature.VOLUME_MUTE
@@ -145,6 +145,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
super().__init__(speaker, config_entry)
self._attr_unique_id = self.soco.uid
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Flag media player features that are supported."""
features = self._attr_supported_features
if self.source_list:
features |= MediaPlayerEntityFeature.SELECT_SOURCE
return features
async def async_added_to_hass(self) -> None:
"""Handle common setup when added to hass."""
await super().async_added_to_hass()
@@ -155,6 +163,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
self.async_write_media_state,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{SONOS_FAVORITES_UPDATED}-{self.speaker.household_id}",
self.async_write_ha_state,
)
)
@callback
def async_write_media_state(self, uid: str) -> None:
@@ -394,14 +409,18 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
@property
def source_list(self) -> list[str]:
"""List of available input sources."""
sources: list[str] = []
model = self.coordinator.model_name.split()[-1].upper()
if model in MODELS_LINEIN_ONLY:
return [SOURCE_LINEIN]
if model in MODELS_TV_ONLY:
return [SOURCE_TV]
if model in MODELS_LINEIN_AND_TV:
return [SOURCE_LINEIN, SOURCE_TV]
return []
sources = [SOURCE_LINEIN]
elif model in MODELS_TV_ONLY:
sources = [SOURCE_TV]
elif model in MODELS_LINEIN_AND_TV:
sources = [SOURCE_LINEIN, SOURCE_TV]
sources.extend(
fav.title for fav in self.speaker.favorites if fav.title not in sources
)
return sources
@soco_error(UPNP_ERRORS_TO_IGNORE)
def media_play(self) -> None:
@@ -160,7 +160,7 @@ async def async_setup_entry(
model=model,
manufacturer=manufacturer,
model_id=model_id,
hw_version=player.firmware,
hw_version=str(player.firmware) if player.firmware is not None else None,
sw_version=sw_version,
via_device=(DOMAIN, coordinator.server_uuid),
)
+1
View File
@@ -29,6 +29,7 @@ VEHICLE_STATUS = "vehicle_status"
API_GEN_1 = "g1"
API_GEN_2 = "g2"
API_GEN_3 = "g3"
API_GEN_4 = "g4"
MANUFACTURER = "Subaru"
PLATFORMS = [
+3 -2
View File
@@ -24,6 +24,7 @@ from . import get_device_info
from .const import (
API_GEN_2,
API_GEN_3,
API_GEN_4,
VEHICLE_API_GEN,
VEHICLE_HAS_EV,
VEHICLE_STATUS,
@@ -153,10 +154,10 @@ def create_vehicle_sensors(
sensor_descriptions_to_add = []
sensor_descriptions_to_add.extend(SAFETY_SENSORS)
if vehicle_info[VEHICLE_API_GEN] in [API_GEN_2, API_GEN_3]:
if vehicle_info[VEHICLE_API_GEN] in [API_GEN_2, API_GEN_3, API_GEN_4]:
sensor_descriptions_to_add.extend(API_GEN_2_SENSORS)
if vehicle_info[VEHICLE_API_GEN] == API_GEN_3:
if vehicle_info[VEHICLE_API_GEN] in [API_GEN_3, API_GEN_4]:
sensor_descriptions_to_add.extend(API_GEN_3_SENSORS)
if vehicle_info[VEHICLE_HAS_EV]:
@@ -36,8 +36,11 @@ class SwitchBotCloudEntity(CoordinatorEntity[SwitchBotCoordinator]):
self._api = api
self._attr_unique_id = device.device_id
_sw_version = None
if self.coordinator.data is not None:
_sw_version = self.coordinator.data.get("version")
if (
self.coordinator.data is not None
and (_version := self.coordinator.data.get("version")) is not None
):
_sw_version = str(_version)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.device_id)},
name=device.device_name,
@@ -61,6 +61,7 @@ def setup_platform(
if http_error.response.status_code == requests.codes.unauthorized:
_LOGGER.error("Invalid credentials")
return
raise
all_sensors = []
for device in devices:
@@ -250,13 +250,9 @@ class DatasetStore:
entry: DatasetEntry | None
for entry in self.datasets.values():
if entry.dataset == dataset:
if (
preferred_extended_address
and entry.preferred_extended_address is None
):
self.async_set_preferred_border_agent(
entry.id, preferred_border_agent_id, preferred_extended_address
)
self._async_maybe_update_preferred_border_agent(
entry, preferred_border_agent_id, preferred_extended_address
)
return
# Update if dataset with same extended pan id exists and the timestamp
@@ -307,10 +303,9 @@ class DatasetStore:
self.datasets[entry.id], tlv=tlv
)
self.async_schedule_save()
if preferred_extended_address and entry.preferred_extended_address is None:
self.async_set_preferred_border_agent(
entry.id, preferred_border_agent_id, preferred_extended_address
)
self._async_maybe_update_preferred_border_agent(
entry, preferred_border_agent_id, preferred_extended_address
)
return
entry = DatasetEntry(
@@ -348,6 +343,37 @@ class DatasetStore:
"""Get dataset by id."""
return self.datasets.get(dataset_id)
@callback
def _async_maybe_update_preferred_border_agent(
self,
entry: DatasetEntry,
preferred_border_agent_id: str | None,
preferred_extended_address: str | None,
) -> None:
"""Update the preferred border agent of an existing dataset if appropriate.
Sets the preferred border agent if it was not set yet, or refreshes the
stored extended address when the border agent ID still matches but the
extended address changed. The latter happens e.g. after an OTBR upgrade
regenerates the extended address while keeping the same border agent ID.
"""
if not preferred_extended_address:
return
if entry.preferred_extended_address is None or (
preferred_border_agent_id is not None
and preferred_border_agent_id == entry.preferred_border_agent_id
and preferred_extended_address != entry.preferred_extended_address
):
_LOGGER.info(
"Updating extended address of preferred border agent %s from %s to %s",
preferred_border_agent_id,
entry.preferred_extended_address,
preferred_extended_address,
)
self.async_set_preferred_border_agent(
entry.id, preferred_border_agent_id, preferred_extended_address
)
@callback
def async_set_preferred_border_agent(
self, dataset_id: str, border_agent_id: str | None, extended_address: str
@@ -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."]
}
@@ -12,6 +12,12 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"user": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
@@ -15,6 +15,9 @@
"description": "The Tuya integration now uses an improved login method. To reauthenticate with your Smart Life or Tuya Smart account, you need to enter your user code.\n\nYou can find this code in the Smart Life app or Tuya Smart app in **Settings** > **Account and Security** screen, and enter the code shown on the **User Code** field. The user code is case-sensitive, please be sure to enter it exactly as shown in the app."
},
"scan": {
"data": {
"QR": "QR code"
},
"description": "Use the Smart Life app or Tuya Smart app to scan the following QR code to complete the login.\n\nContinue to the next step once you have completed this step in the app."
},
"user": {
@@ -440,7 +440,7 @@ class ProtectSettableKeysMixin(ProtectEntityDescription[T]):
async def ufp_set(self, obj: T, value: Any) -> None:
"""Set value for UniFi Protect device."""
_LOGGER.debug("Setting %s to %s for %s", self.name, value, obj.display_name)
_LOGGER.debug("Setting %s to %s for %s", self.key, value, obj.display_name)
if self.ufp_set_method is not None:
await getattr(obj, self.ufp_set_method)(value)
elif self.ufp_set_method_fn is not None:
@@ -8,8 +8,10 @@ from homeassistant.const import Platform
LOGGER: Logger = getLogger(__package__)
# The free plan is limited to 10 requests/minute
COORDINATOR_UPDATE_INTERVAL: timedelta = timedelta(seconds=10)
# The free plan is formally limited to 10 requests/minute
# But real world says 5 requests/minute is the real limit
# Opened a ticket with support with no response for 2 months
COORDINATOR_UPDATE_INTERVAL: timedelta = timedelta(seconds=15)
DOMAIN: Final = "uptimerobot"
PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/v2c",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["pytrydan==1.0.1"]
"requirements": ["pytrydan==1.0.2"]
}
+10 -1
View File
@@ -3,7 +3,11 @@
import logging
from aiowebdav2.client import Client
from aiowebdav2.exceptions import UnauthorizedError
from aiowebdav2.exceptions import (
ConnectionExceptionError,
NoConnectionError,
UnauthorizedError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL
@@ -35,6 +39,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: WebDavConfigEntry) -> bo
translation_domain=DOMAIN,
translation_key="invalid_username_password",
) from err
except (ConnectionExceptionError, NoConnectionError, TimeoutError) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
# Check if we can connect to the WebDAV server
# and access the root directory
+12 -6
View File
@@ -5,6 +5,7 @@ import logging
from typing import Any
from weheat.abstractions.user import async_get_user_id_from_token
from weheat.exceptions import ApiException
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
@@ -33,12 +34,17 @@ class OAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
"""Override create entry to find heat pumps."""
# get the user id and use that as unique id for this entry
user_id = await async_get_user_id_from_token(
API_URL,
data[CONF_TOKEN][CONF_ACCESS_TOKEN],
async_get_clientsession(self.hass),
)
try:
user_id = await async_get_user_id_from_token(
API_URL,
data[CONF_TOKEN][CONF_ACCESS_TOKEN],
async_get_clientsession(self.hass),
)
except ApiException as err:
self.logger.error("Failed to get user ID from Weheat API: %s", err)
return self.async_abort(reason="oauth_failed")
if user_id is None:
return self.async_abort(reason="oauth_failed")
await self.async_set_unique_id(user_id)
if self.source != SOURCE_REAUTH:
self._abort_if_unique_id_configured()
@@ -177,6 +177,6 @@ class XiaomiGatewayDevice(CoordinatorEntity[GatewayDeviceCoordinator], Entity):
manufacturer="Xiaomi",
name=self._sub_device.name,
model=self._sub_device.model,
sw_version=self._sub_device.firmware_version,
sw_version=str(self._sub_device.firmware_version),
hw_version=self._sub_device.zigbee_model,
)
@@ -31,7 +31,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> boo
async def async_migrate_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool:
"""Migrate old entry."""
LOGGER.debug("Migrating from version %s", entry.version)
LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
if entry.version == 1:
new_options = entry.options.copy()
@@ -55,6 +55,19 @@ async def async_migrate_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bo
del new_data[CONF_NAME]
hass.config_entries.async_update_entry(entry, data=new_data, minor_version=2)
LOGGER.debug("Migration to version %s successful", entry.version)
if entry.version == 2 and entry.minor_version == 2:
entity_reg = er.async_get(hass)
entries = er.async_entries_for_config_entry(entity_reg, entry.entry_id)
for entity in entries:
if entity.unique_id == "yale_smart_alarm-panic":
entity_reg.async_update_entity(
entity.entity_id,
new_unique_id=f"{entry.entry_id}-panic",
)
hass.config_entries.async_update_entry(entry, minor_version=3)
LOGGER.debug(
"Migration to version %s.%s successful", entry.version, entry.minor_version
)
return True
@@ -47,7 +47,7 @@ class YalePanicButton(YaleAlarmEntity, ButtonEntity):
"""Initialize the plug switch."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"yale_smart_alarm-{description.key}"
self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{description.key}"
async def async_press(self) -> None:
"""Press the button."""
@@ -64,7 +64,7 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Yale integration."""
VERSION = 2
MINOR_VERSION = 2
MINOR_VERSION = 3
@staticmethod
@callback
@@ -432,15 +432,6 @@ async def async_set_credential(
translation_key="no_available_credential_slots",
translation_placeholders={"credential_type": cred_type_str},
)
elif not 1 <= credential_slot <= type_cap.number_of_credential_slots:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="credential_slot_out_of_range",
translation_placeholders={
"credential_type": cred_type_str,
"max_slot": str(type_cap.number_of_credential_slots),
},
)
status = await node.access_control.set_credential(
user_id, credential_type, credential_slot, credential_data
@@ -322,9 +322,6 @@
"credential_rejected_wrong_uuid": {
"message": "The device rejected the credential because the user unique identifier does not match."
},
"credential_slot_out_of_range": {
"message": "Credential slot for {credential_type} must be between 1 and {max_slot}."
},
"credential_type_not_supported": {
"message": "Credential type {credential_type} is not supported on this device"
},
+1 -1
View File
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2026
MINOR_VERSION: Final = 6
PATCH_VERSION: Final = "2"
PATCH_VERSION: Final = "4"
__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)
+3 -15
View File
@@ -1,6 +1,6 @@
"""Helper to track the current http request."""
from collections.abc import Awaitable, Callable, Container, Mapping
from collections.abc import Awaitable, Callable
from contextvars import ContextVar
from http import HTTPStatus
import inspect
@@ -20,7 +20,7 @@ import voluptuous as vol
from homeassistant import exceptions
from homeassistant.const import CONTENT_TYPE_JSON
from homeassistant.core import Context, HomeAssistant, callback, is_callback
from homeassistant.core import Context, HomeAssistant, is_callback
from homeassistant.util.json import JSON_ENCODE_EXCEPTIONS, format_unserializable_data
from .json import find_paths_unserializable_data, json_bytes, json_dumps
@@ -55,13 +55,7 @@ def request_handler_factory(
authenticated = request.get(KEY_AUTHENTICATED, False)
if view.use_query_token_for_auth and not authenticated:
token = request.query.get("token")
if token and token in view.get_valid_auth_tokens(request.match_info):
_LOGGER.debug("Authenticated request with query token")
authenticated = True
if (view.requires_auth or view.use_query_token_for_auth) and not authenticated:
if view.requires_auth and not authenticated:
# Import here to avoid circular dependency with network.py
from .network import NoURLAvailableError, get_url # noqa: PLC0415
@@ -135,7 +129,6 @@ class HomeAssistantView:
extra_urls: list[str] = []
# Views inheriting from this class can override this
requires_auth = True
use_query_token_for_auth = False
cors_allowed = False
@staticmethod
@@ -211,8 +204,3 @@ class HomeAssistantView:
if allow_cors:
for route in routes:
allow_cors(route)
@callback
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
"""Return valid auth tokens, which can be used for query token authentication."""
return ()
+4 -4
View File
@@ -699,7 +699,7 @@ def _get_exposed_entities(
):
# Entity is in area
area_names.append(area_entry.name)
area_names.extend(area_entry.aliases)
area_names.extend(sorted(area_entry.aliases))
elif device_entry is not None:
# Check device area
if (
@@ -710,7 +710,7 @@ def _get_exposed_entities(
is not None
):
area_names.append(area_entry.name)
area_names.extend(area_entry.aliases)
area_names.extend(sorted(area_entry.aliases))
info: dict[str, Any] = {
"names": ", ".join(names),
@@ -957,9 +957,9 @@ def _get_cached_action_parameters(
aliases = er.async_get_entity_aliases(hass, entity_entry)
if aliases:
if description:
description = description + ". Aliases: " + str(list(aliases))
description = description + ". Aliases: " + str(sorted(aliases))
else:
description = "Aliases: " + str(list(aliases))
description = "Aliases: " + str(sorted(aliases))
parameters_cache.setdefault(domain, {})[action] = (description, parameters)
@@ -1,6 +1,7 @@
"""Config entry functions for Home Assistant templates."""
from collections.abc import Iterable
from enum import Enum
from typing import TYPE_CHECKING, Any
from homeassistant.exceptions import TemplateError
@@ -104,4 +105,6 @@ class ConfigEntryExtension(BaseTemplateExtension):
if config_entry is None:
return None
return getattr(config_entry, attr_name)
if isinstance(result := getattr(config_entry, attr_name), Enum):
return result.value
return result
+4 -4
View File
@@ -1,7 +1,7 @@
# Automatically generated by gen_requirements_all.py, do not edit
aiodhcpwatcher==1.2.7
aiodiscover==3.2.4
aiodiscover==3.3.2
aiodns==4.0.4
aiogithubapi==26.0.0
aiohttp-asyncmdnsresolver==0.2.0
@@ -25,7 +25,7 @@ bleak==3.0.2
bluetooth-adapters==2.3.0
bluetooth-auto-recovery==1.6.4
bluetooth-data-tools==1.29.18
cached-ipaddress==1.1.1
cached-ipaddress==1.1.2
certifi>=2021.5.30
ciso8601==2.3.3
cronsim==2.7
@@ -37,9 +37,9 @@ go2rtc-client==0.4.0
ha-ffmpeg==3.2.2
habluetooth==6.8.1
hass-nabucasa==2.2.0
hassil==3.5.0
hassil==3.7.0
home-assistant-bluetooth==2.0.0
home-assistant-frontend==20260527.5
home-assistant-frontend==20260527.7
home-assistant-intents==2026.6.1
httpx==0.28.1
ifaddr==0.2.0
+1 -1
View File
@@ -5,7 +5,7 @@ To update, run python3 -m script.hassfest
from typing import Final
FRONTEND_VERSION: Final[str] = "20260527.5"
FRONTEND_VERSION: Final[str] = "20260527.7"
MDI_ICONS: Final[set[str]] = {
"ab-testing",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2026.6.2"
version = "2026.6.4"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."

Some files were not shown because too many files have changed in this diff Show More