Compare commits

...

83 Commits

Author SHA1 Message Date
Franck Nijhof 7b06228a5a Bump version to 2026.6.0b1 2026-06-01 16:54:56 +00:00
Paul Bottein 06b2ec22f0 Bump yoto-api to 3.1.5 (#172753) 2026-06-01 16:54:33 +00:00
jameson_uk 7950998083 Bump aioamazondevices to 13.8.2 (#172748) 2026-06-01 16:54:30 +00:00
Maciej Bieniek 86999063d7 Translate the name of the Tractive tracker (#172747) 2026-06-01 16:54:28 +00:00
Maciej Bieniek 9843fdad2c Add missing _attr_name = None for Tractive device tracker (#172746) 2026-06-01 16:54:26 +00:00
Jan Bouwhuis e53914a0ef Fix MQTT device_tracker logging attributes order (#172732) 2026-06-01 16:54:24 +00:00
Franck Nijhof f7afe22318 Skip Overkiz events for unknown device URLs (#172712) 2026-06-01 16:54:22 +00:00
Franck Nijhof acfecd7f5c Convert set_id to int in LG TV RS-232 config flow (#172701) 2026-06-01 16:54:20 +00:00
Franck Nijhof 56057a11e6 Return 404 instead of 500 when media player artwork is unavailable (#172700) 2026-06-01 16:54:18 +00:00
Yardian Support 0d079c57e4 Fix Yardian water hammer diagnostic sensor name (#172698) 2026-06-01 16:54:16 +00:00
Denis Shulyaka 3ad3e1fafb Fix ai_task camera snapshot mime type (#172682) 2026-06-01 16:54:13 +00:00
Josef Zweck 0677ed824f Fix tedee entity availability (#172667)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-01 16:54:11 +00:00
Jordan Harvey 4b9945e012 Bump pynintendoparental to 2.4.0 (#172666) 2026-06-01 16:54:08 +00:00
Michael 9fa0132b1c Add missing exception translation keys in Ecovacs (#172658) 2026-06-01 16:54:06 +00:00
jameson_uk 10a25368a0 Improve http2 task handling for Alexa Devices (#172649) 2026-06-01 16:54:04 +00:00
epenet fbb68c26b6 Bump tuya-device-handlers to 0.0.22 (#172648) 2026-06-01 16:54:02 +00:00
Michael 25875de414 Add extra device info to FRITZ!Box Tools diagnostics (#172647) 2026-06-01 16:54:00 +00:00
TheJulianJES 22ace88b2c Bump ZHA to 1.4.1 (#172640) 2026-06-01 16:53:57 +00:00
David Knowles a47105d314 Schlage: use lock connected status as availability signal (#172638)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 16:53:55 +00:00
Jan Bouwhuis b50bfda00c Fix MQTT device_tracker not saving state on location accuracy changes (#172629) 2026-06-01 16:53:53 +00:00
Sören 0d37319ba9 Improve Avea Bluetooth discovery flow (#172623) 2026-06-01 16:53:51 +00:00
Michael 24a5c75cf2 Show error about missing api permissions while browsing Immich media (#172609) 2026-06-01 16:53:49 +00:00
renovate[bot] dd43b1135d Update rf-protocols to 4.0.1 (#172597) 2026-06-01 16:53:47 +00:00
J. Nick Koston de0a202c4e Explain why a Switchbot device could not be found (#172581) 2026-06-01 16:53:44 +00:00
J. Nick Koston d550d1da90 Expose bluetooth address reachability diagnostics API (#172578) 2026-06-01 16:53:42 +00:00
J. Nick Koston ce8875ae8c Bump habluetooth to 6.8.0 (#172577) 2026-06-01 16:53:40 +00:00
J. Nick Koston 3364096b2b Fix ESPHome update entity stuck on for project versions with build suffix (#172571) 2026-06-01 16:53:38 +00:00
A. Gideonse c2b75b9634 Bugfix: Gen-1 Inverter sensor for Indevolt to display "N/A" when turned off (#172559) 2026-06-01 16:53:36 +00:00
Franck Nijhof ae278d3c80 Sanitize surrogate characters in MeteoAlarm alert attributes (#172545) 2026-06-01 16:53:34 +00:00
Paul Bottein 25f9cd9ab8 Fix Yoto OAuth flow with cloud credentials (#172544)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-01 16:53:31 +00:00
Franck Nijhof 796d82d6ed Add missing ssdp dependency to BraviaTV manifest (#172536) 2026-06-01 16:53:30 +00:00
Franck Nijhof 4b517fb164 Use state-based icon for Hue grouped light (#172535) 2026-06-01 16:53:27 +00:00
Kamil Breguła 2d74091a36 Refresh WLED firmware releases on manual entity update (#172517)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-01 16:53:25 +00:00
Franck Nijhof 504e22ee3e Raise errors instead of swallowing exceptions in Toon action handlers (#172511) 2026-06-01 16:53:23 +00:00
Franck Nijhof c95a39c26e Guard Shelly repairs checks for uninitialized RPC devices (#172509) 2026-06-01 16:53:21 +00:00
Franck Nijhof 8ec3eac705 Fix Overkiz UnoIO cover reporting wrong movement direction (#172506) 2026-06-01 16:53:19 +00:00
Franck Nijhof 589d2637c9 Fix ephember crash when zone mode is None (#172504) 2026-06-01 16:53:17 +00:00
Franck Nijhof 26cf728165 Handle missing notAfter field in cert_expiry certificate data (#172503)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-01 16:51:22 +00:00
Franck Nijhof b61559bdbb Handle malformed response errors in Denon AVR error wrapper (#172502) 2026-06-01 16:06:02 +00:00
Jan Bouwhuis 57259132d9 Silent migrate MQTT protocol version to version 5 if the broker supports it or raise an issue (#172500)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 16:06:00 +00:00
Franck Nijhof 2776e966ff Reduce Wyoming satellite disconnect log to debug level (#172499) 2026-06-01 16:05:58 +00:00
Franck Nijhof 5f9872886d Convert Roomba hw_version to string for device registry (#172497) 2026-06-01 16:05:56 +00:00
Franck Nijhof f728a1bf09 Add missing Flexit BACnet transient operation modes to preset map (#172493) 2026-06-01 16:05:53 +00:00
Franck Nijhof df65132268 Add prog operating mode to Overkiz Atlantic heater HVAC mapping (#172491) 2026-06-01 16:05:51 +00:00
Michael c13822b776 Handle FileNotFoundError in Immich upload_file action (#172490) 2026-06-01 16:05:49 +00:00
Simone Chemelli c6d696db0c Remove redundant definitions in Alexa Devices (#172488) 2026-06-01 16:05:46 +00:00
Franck Nijhof 114c9bbafa Increase ConfigEntryNotReady retry backoff cap from 80s to 10 minutes (#172487) 2026-06-01 16:05:44 +00:00
Franck Nijhof 323ce99fda Fix Tado config flow crash on device activation polling (#172486) 2026-06-01 16:05:42 +00:00
Jan Bouwhuis 7a7ef85db2 Move MQTT protocol setting to main options (#172482) 2026-06-01 16:05:40 +00:00
Franck Nijhof 7ab402618d Handle DAVError in CalDAV get_supported_components (#172479) 2026-06-01 16:05:37 +00:00
Franck Nijhof aa87295a1e Fix Growatt setup failure on API rate limit (#172472) 2026-06-01 16:05:35 +00:00
Simone Chemelli 3bd979e976 Bump samsungtvws to 3.0.5 (#172471) 2026-06-01 16:05:33 +00:00
Paul Bottein 9dddf76548 Name the Broadlink RF transmitter entity (#172468) 2026-06-01 16:05:31 +00:00
Franck Nijhof 1828579f03 Fix Volvo lock crash when API field is missing from coordinator data (#172465) 2026-06-01 16:05:29 +00:00
Bram Kragten 47bca8d8c2 Bump frontend to 20260527.1 (#172462)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 16:05:27 +00:00
Paulus Schoutsen 6f3fb5c7bd Add lg_tv_rs232 to LG brand (#172458)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 16:05:24 +00:00
TheJulianJES d9b4b5b3d0 Fix Matter BLE proxy blocking startup (#172456) 2026-06-01 16:05:22 +00:00
Ronald van der Meer 342b364af6 Fix Duco regression where entities become unavailable when LAN info fetch fails (#172448) 2026-06-01 16:05:20 +00:00
Simone Chemelli 951cd71741 Discard old events for Alexa Devices (#172446) 2026-06-01 16:05:18 +00:00
Franck Nijhof e86a54f81c Fix Hue light ZeroDivisionError when mirek value is zero (#172442) 2026-06-01 16:05:15 +00:00
Simone Chemelli ba8b33e1a9 Fix Shelly sensor restore when not initialized (#172441) 2026-06-01 16:05:13 +00:00
Franck Nijhof b6c40ba3fc Fix Jellyfin media source crash when entry is not loaded (#172437) 2026-06-01 16:05:11 +00:00
Franck Nijhof f2f29c07c7 Fix SmartThings light checking wrong component for capabilities (#172430)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 16:05:08 +00:00
Franck Nijhof 50a3ab115d Fix iZone integration broken by python-izone 1.2.10 API change (#172427) 2026-06-01 16:05:06 +00:00
Franck Nijhof c204054847 Convert yamaha_musiccast sw_version to string (#172411) 2026-06-01 16:05:04 +00:00
Jan Bouwhuis 28d6eab2dd Improve MQTT protocol deprecation repair message (#172404)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 16:05:02 +00:00
Manu 6b1ee57bd5 Fix index error in DuckDNS integration (#172392) 2026-06-01 16:05:00 +00:00
J. Nick Koston 7247f95b05 Bump onvif-zeep-async to 4.1.1 (#172391) 2026-06-01 16:04:57 +00:00
J. Nick Koston cdeafdfd42 Bump yalexs to 9.2.1 (#172389) 2026-06-01 16:04:55 +00:00
Abílio Costa 9d60fce72e Fix OMIE sensors not updating on setup (#172383) 2026-06-01 16:04:53 +00:00
Simone Chemelli 2e4c6c4370 Bump aioamazondevices to 13.8.1 (#172382) 2026-06-01 16:04:50 +00:00
J. Nick Koston b7e36e297b Bump dbus-fast to 5.0.16 (#172378) 2026-06-01 16:04:48 +00:00
Stefan Agner 7e178efe63 Reject backup uploads with unsafe inner name (#172368)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 16:04:46 +00:00
puddly 38f25c4b41 Bump ZHA to 1.4.0 (#172357) 2026-06-01 16:04:44 +00:00
torben-iometer 2c2e70a11c bump iometer version to 1.0.1 (#172338) 2026-06-01 16:04:41 +00:00
Linkplay2020 190350aec3 Bump wiim to 1.0.4 (#172334)
Co-authored-by: Tao Jiang <tao.jiang@linkplay.com>
2026-06-01 16:04:39 +00:00
tlpeter a87083b6c1 Bump renault-api to 0.5.11 (#172333)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-06-01 16:04:36 +00:00
Mike Degatano d5be54fd40 Migrate analytics integration to config entry setup (#171801)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 16:04:34 +00:00
Mike Degatano 46f2ad9eb2 During onboarding, ensure Supervisor is up to date during hassio setup (#171129)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 16:04:31 +00:00
mhuiskes add75622d6 Fix zeversolar coordinator to raise UpdateFailed on errors (#170507) 2026-06-01 16:04:29 +00:00
Daniel Feinberg 2f334d657d Fix apple_tv HomePod streaming failures when device is idle (#170033)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 16:04:26 +00:00
Nikhil Deepak fd69d384be Reset MQTT valve opening/closing state at intermediate positions (#165176)
Co-authored-by: jbouwh <jan@jbsoft.nl>
2026-06-01 16:04:24 +00:00
Franck Nijhof fce17c8e6f Bump version to 2026.6.0b0 2026-05-27 16:07:37 +00:00
201 changed files with 3084 additions and 1244 deletions
+1
View File
@@ -6,6 +6,7 @@
"lg_netcast",
"lg_soundbar",
"lg_thinq",
"lg_tv_rs232",
"webostv"
]
}
+1 -2
View File
@@ -72,8 +72,7 @@ async def _resolve_attachments(
resolved_attachments.append(
conversation.Attachment(
media_content_id=media_content_id,
mime_type=attachment.get("media_content_type")
or image_data.content_type,
mime_type=image_data.content_type,
path=temp_filename,
)
)
@@ -1,8 +1,5 @@
"""Alexa Devices integration."""
import asyncio
import contextlib
from homeassistant.const import CONF_COUNTRY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client, config_validation as cv, httpx_client
@@ -46,21 +43,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
async def _on_http2_reauth_required() -> None:
entry.async_start_reauth(hass)
async def _cancel_http2() -> None:
http2_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await http2_task
alexa_httpx_client = httpx_client.get_async_client(
hass,
alpn_protocols=SSL_ALPN_HTTP11_HTTP2,
)
http2_task = await coordinator.api.start_http2_processing(
alexa_httpx_client, on_reauth_required=_on_http2_reauth_required
await coordinator.api.start_http2_processing(
alexa_httpx_client,
on_reauth_required=_on_http2_reauth_required,
)
entry.async_on_unload(_cancel_http2)
entry.async_on_unload(coordinator.api.stop_http2_processing)
entry.runtime_data = coordinator
@@ -39,11 +39,8 @@ async def async_setup_entry(
class AmazonRoutineButton(AmazonServiceEntity, ButtonEntity):
"""Button entity for Alexa routine."""
_attr_has_entity_name = True
def __init__(self, coordinator: AmazonDevicesCoordinator, routine: str) -> None:
"""Initialize the routine button entity."""
self._coordinator = coordinator
self._routine = routine
super().__init__(
coordinator,
@@ -52,4 +49,4 @@ class AmazonRoutineButton(AmazonServiceEntity, ButtonEntity):
async def async_press(self) -> None:
"""Handle button press action."""
await self._coordinator.api.call_routine(self._routine)
await self.coordinator.api.call_routine(self._routine)
@@ -53,7 +53,7 @@ class AlexaVoiceEvent(AmazonEntity, EventEntity):
_attr_event_types = [EVENT_TYPE]
coordinator: AmazonDevicesCoordinator
_last_seen_timestamp: int | None = None
_last_seen_timestamp: int = 0 # January 1, 1970 at 12:00:00 AM
@callback
def _handle_coordinator_update(self) -> None:
@@ -71,7 +71,8 @@ class AlexaVoiceEvent(AmazonEntity, EventEntity):
)
return
if vocal_record.timestamp == self._last_seen_timestamp:
if vocal_record.timestamp <= self._last_seen_timestamp:
# Discard old events that have already been processed
return
self._last_seen_timestamp = vocal_record.timestamp
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==13.8.0"]
"requirements": ["aioamazondevices==13.8.2"]
}
+41 -16
View File
@@ -5,8 +5,12 @@ from typing import Any
import voluptuous as vol
from homeassistant.components import labs, websocket_api
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.components.hassio import HassioNotReadyError
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import discovery_flow
from homeassistant.helpers.start import async_at_started
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
@@ -49,6 +53,7 @@ CONFIG_SCHEMA = vol.Schema(
)
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
_DATA_SNAPSHOTS_URL: HassKey[str | None] = HassKey(f"{DOMAIN}_snapshots_url")
LABS_SNAPSHOT_FEATURE = "snapshots"
@@ -57,18 +62,39 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the analytics integration."""
analytics_config = config.get(DOMAIN, {})
snapshots_url: str | None = None
if CONF_SNAPSHOTS_URL in analytics_config:
await labs.async_update_preview_feature(
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, enabled=True
)
snapshots_url = analytics_config[CONF_SNAPSHOTS_URL]
else:
snapshots_url = None
hass.data[_DATA_SNAPSHOTS_URL] = snapshots_url
discovery_flow.async_create_flow(
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
)
websocket_api.async_register_command(hass, websocket_analytics)
websocket_api.async_register_command(hass, websocket_analytics_preferences)
hass.http.register_view(AnalyticsDevicesView)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Analytics from a config entry."""
snapshots_url = hass.data[_DATA_SNAPSHOTS_URL]
analytics = Analytics(hass, snapshots_url)
# Load stored data
await analytics.load()
try:
await analytics.load()
except HassioNotReadyError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="supervisor_not_ready",
) from err
started = False
@@ -80,8 +106,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if started:
await analytics.async_schedule()
async def start_schedule(_event: Event) -> None:
"""Start the send schedule after the started event."""
async def start_schedule(hass: HomeAssistant) -> None:
"""Start the send schedule once Home Assistant has started."""
nonlocal started
started = True
await analytics.async_schedule()
@@ -89,12 +115,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
labs.async_subscribe_preview_feature(
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, _async_handle_labs_update
)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)
websocket_api.async_register_command(hass, websocket_analytics)
websocket_api.async_register_command(hass, websocket_analytics_preferences)
hass.http.register_view(AnalyticsDevicesView)
async_at_started(hass, start_schedule)
hass.data[DATA_COMPONENT] = analytics
return True
@@ -109,7 +130,9 @@ def websocket_analytics(
msg: dict[str, Any],
) -> None:
"""Return analytics preferences."""
analytics = hass.data[DATA_COMPONENT]
if (analytics := hass.data.get(DATA_COMPONENT)) is None:
connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, "Not loaded")
return
connection.send_result(
msg["id"],
{ATTR_PREFERENCES: analytics.preferences, ATTR_ONBOARDED: analytics.onboarded},
@@ -130,8 +153,10 @@ async def websocket_analytics_preferences(
msg: dict[str, Any],
) -> None:
"""Update analytics preferences."""
if (analytics := hass.data.get(DATA_COMPONENT)) is None:
connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, "Not loaded")
return
preferences = msg[ATTR_PREFERENCES]
analytics = hass.data[DATA_COMPONENT]
await analytics.save_preferences(preferences)
await analytics.async_schedule()
@@ -299,12 +299,8 @@ class Analytics:
self._data = AnalyticsData.from_dict(stored)
if self.supervisor and not self.onboarded:
# This may raise HassioNotReadyError if Supervisor was unreachable
# during setup of the Supervisor integration. That will fail setup
# of this integration. However there is no better option at this time
# since we need to get the diagnostic setting from Supervisor to correctly
# setup this integration and we can't raise ConfigEntryNotReady to
# trigger a retry from async_setup.
# This may raise HassioNotReadyError if Supervisor was unreachable.
# The caller is responsible for handling this and triggering a retry.
supervisor_info = hassio.get_supervisor_info(self._hass)
# User have not configured analytics, get this setting from the supervisor
@@ -349,10 +345,10 @@ class Analytics:
await self._save()
if self.supervisor:
# get_supervisor_info was called during setup so we can't get here
# if it raised. The others may raise HassioNotReadyError if only some
# data was successfully fetched from Supervisor
supervisor_info = hassio.get_supervisor_info(hass)
# Try to pull Supervisor information, but don't fail if some or all
# of it is unavailable due to setup failures in the hassio integration.
with contextlib.suppress(hassio.HassioNotReadyError):
supervisor_info = hassio.get_supervisor_info(hass)
with contextlib.suppress(hassio.HassioNotReadyError):
operating_system_info = hassio.get_os_info(hass)
with contextlib.suppress(hassio.HassioNotReadyError):
@@ -0,0 +1,19 @@
"""Config flow for Analytics integration."""
from typing import Any
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from .const import DOMAIN
class AnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Analytics."""
VERSION = 1
async def async_step_system(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
return self.async_create_entry(title="Analytics", data={})
@@ -3,6 +3,7 @@
"name": "Analytics",
"after_dependencies": ["energy", "hassio", "recorder"],
"codeowners": ["@home-assistant/core"],
"config_flow": true,
"dependencies": ["api", "websocket_api", "http"],
"documentation": "https://www.home-assistant.io/integrations/analytics",
"integration_type": "system",
@@ -14,5 +15,6 @@
"report_issue_url": "https://github.com/OHF-Device-Database/device-database/issues/new"
}
},
"quality_scale": "internal"
"quality_scale": "internal",
"single_config_entry": true
}
@@ -1,4 +1,9 @@
{
"exceptions": {
"supervisor_not_ready": {
"message": "Supervisor was not ready during setup, will retry"
}
},
"preview_features": {
"snapshots": {
"description": "We're creating the [Open Home Foundation Device Database](https://www.home-assistant.io/blog/2026/02/02/about-device-database/): a free, open source community-powered resource to help users find practical information about how smart home devices perform in real installations.\n\nYou can help us build it by opting in to share anonymized data about your devices. This data will only ever include device-specific details (like model or manufacturer) never personally identifying information (like the names you assign).\n\nFind out how we process your data (should you choose to contribute) in our [Data Use Statement](https://www.openhomefoundation.org/device-database-data-use-statement).",
@@ -38,11 +38,13 @@ from homeassistant.components.media_player import (
)
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from . import AppleTvConfigEntry, AppleTVManager
from .browse_media import build_app_list
from .const import DOMAIN
from .entity import AppleTVEntity
_LOGGER = logging.getLogger(__name__)
@@ -126,7 +128,6 @@ class AppleTvMediaPlayer(
@callback
def async_device_connected(self, atv: AppleTV) -> None:
"""Handle when connection is made to device."""
# NB: Do not use _is_feature_available here as it only works when playing
if atv.features.in_state(FeatureState.Available, FeatureName.PushUpdates):
atv.push_updater.listener = self
atv.push_updater.start()
@@ -352,21 +353,41 @@ class AppleTvMediaPlayer(
media_id = async_process_play_media_url(self.hass, play_item.url)
media_type = MediaType.MUSIC
if self._is_feature_available(FeatureName.StreamFile) and (
use_stream_file = self._is_feature_available(FeatureName.StreamFile) and (
media_type == MediaType.MUSIC or await is_streamable(media_id)
):
_LOGGER.debug("Streaming %s via RAOP", media_id)
await self.atv.stream.stream_file(media_id)
elif self._is_feature_available(FeatureName.PlayUrl) and (
(parsed_url := URL(media_id)).is_absolute() and parsed_url.host
):
_LOGGER.debug("Playing %s via AirPlay", media_id)
await self.atv.stream.play_url(media_id)
else:
_LOGGER.error(
"Media streaming is not possible with current configuration for %s",
media_id,
)
)
try:
if use_stream_file:
_LOGGER.debug("Streaming %s via RAOP", media_id)
await self.atv.stream.stream_file(media_id)
elif self._is_feature_available(FeatureName.PlayUrl) and (
(parsed_url := URL(media_id)).is_absolute() and parsed_url.host
):
_LOGGER.debug("Playing %s via AirPlay", media_id)
await self.atv.stream.play_url(media_id)
else:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="streaming_not_supported",
)
except exceptions.NotSupportedError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="streaming_not_supported",
) from ex
except (
exceptions.BlockedStateError,
exceptions.ConnectionLostError,
exceptions.InvalidStateError,
exceptions.OperationTimeoutError,
exceptions.PlaybackError,
exceptions.ProtocolError,
) as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="stream_failed",
) from ex
@property
def media_image_hash(self) -> str | None:
@@ -460,7 +481,7 @@ class AppleTvMediaPlayer(
def _is_feature_available(self, feature: FeatureName) -> bool:
"""Return if a feature is available."""
if self.atv and self._playing:
if self.atv:
return self.atv.features.in_state(FeatureState.Available, feature)
return False
@@ -81,6 +81,12 @@
},
"not_connected": {
"message": "Apple TV is not connected"
},
"stream_failed": {
"message": "Failed to stream media to the Apple TV"
},
"streaming_not_supported": {
"message": "Streaming the requested media is not supported"
}
},
"options": {
@@ -30,5 +30,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.3.0"]
"requirements": ["yalexs==9.2.1", "yalexs-ble==3.3.0"]
}
+13 -6
View File
@@ -8,6 +8,7 @@ import avea
from bleak.exc import BleakError
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -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."""
@@ -150,6 +160,7 @@ class AveaConfigFlow(ConfigFlow, domain=DOMAIN):
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 (
@@ -165,11 +176,10 @@ class AveaConfigFlow(ConfigFlow, domain=DOMAIN):
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)}
)
}
)
@@ -178,10 +188,7 @@ class AveaConfigFlow(ConfigFlow, domain=DOMAIN):
{
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()
}
),
+16 -3
View File
@@ -11,7 +11,7 @@ from homeassistant.helpers.hassio import is_hassio
from .agent import BackupAgent, LocalBackupAgent, OnProgressCallback
from .const import DOMAIN, LOGGER
from .models import AgentBackup, BackupNotFound
from .models import AgentBackup, BackupNotFound, InvalidBackupFilename
from .util import read_backup, suggested_filename
@@ -54,7 +54,13 @@ class CoreLocalBackupAgent(LocalBackupAgent):
try:
backup = read_backup(backup_path)
backups[backup.backup_id] = (backup, backup_path)
except (OSError, TarError, json.JSONDecodeError, KeyError) as err:
except (
OSError,
TarError,
json.JSONDecodeError,
KeyError,
InvalidBackupFilename,
) as err:
LOGGER.warning("Unable to read backup %s: %s", backup_path, err)
return backups
@@ -122,7 +128,14 @@ class CoreLocalBackupAgent(LocalBackupAgent):
def get_new_backup_path(self, backup: AgentBackup) -> Path:
"""Return the local path to a new backup."""
return self._backup_dir / suggested_filename(backup)
candidate = self._backup_dir / suggested_filename(backup)
# suggested_filename does not strip separators; refuse paths that would
# land outside the backup directory.
if candidate.parent != self._backup_dir:
raise InvalidBackupFilename(
f"Refusing to write outside {self._backup_dir}: {candidate}"
)
return candidate
async def async_delete_backup(self, backup_id: str, **kwargs: Any) -> None:
"""Delete a backup file."""
+7 -1
View File
@@ -1978,7 +1978,13 @@ class CoreBackupReaderWriter(BackupReaderWriter):
try:
backup = await async_add_executor_job(read_backup, temp_file)
except (OSError, tarfile.TarError, json.JSONDecodeError, KeyError) as err:
except (
OSError,
tarfile.TarError,
json.JSONDecodeError,
KeyError,
InvalidBackupFilename,
) as err:
LOGGER.warning("Unable to parse backup %s: %s", temp_file, err)
raise
+10 -3
View File
@@ -6,7 +6,7 @@ import copy
from dataclasses import dataclass, replace
from io import BytesIO
import json
from pathlib import Path, PurePath
from pathlib import Path, PurePath, PureWindowsPath
from queue import SimpleQueue
import tarfile
import threading
@@ -34,7 +34,7 @@ from homeassistant.util.async_iterator import (
from homeassistant.util.json import JsonObjectType, json_loads_object
from .const import BUF_SIZE, LOGGER, SECURETAR_CREATE_VERSION
from .models import AddonInfo, AgentBackup, Folder
from .models import AddonInfo, AgentBackup, Folder, InvalidBackupFilename
class DecryptError(HomeAssistantError):
@@ -109,6 +109,13 @@ def read_backup(backup_path: Path) -> AgentBackup:
extra_metadata = cast(dict[str, bool | str], data.get("extra", {}))
date = extra_metadata.get("supervisor.backup_request_date", data["date"])
name = cast(str, data["name"])
# The name is used to derive the on-disk filename via suggested_filename;
# reject anything that could escape the backup directory.
safe_name = PureWindowsPath(name).name
if safe_name != name or name in ("", ".", ".."):
raise InvalidBackupFilename(f"Invalid backup name: {name!r}")
return AgentBackup(
addons=addons,
backup_id=cast(str, data["slug"]),
@@ -118,7 +125,7 @@ def read_backup(backup_path: Path) -> AgentBackup:
folders=folders,
homeassistant_included=homeassistant_included,
homeassistant_version=homeassistant_version,
name=cast(str, data["name"]),
name=name,
protected=cast(bool, data.get("protected", False)),
size=backup_path.stat().st_size,
)
@@ -27,6 +27,7 @@ from bluetooth_data_tools import monotonic_time_coarse as MONOTONIC_TIME
from habluetooth import (
BaseHaRemoteScanner,
BaseHaScanner,
BluetoothReachabilityIntent,
BluetoothScannerDevice,
BluetoothScanningMode,
HaBluetoothConnector,
@@ -55,6 +56,7 @@ from . import passive_update_processor, websocket_api
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,
@@ -108,12 +110,14 @@ __all__ = [
"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",
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, cast
from bleak import BleakScanner
from habluetooth import (
BaseHaScanner,
BluetoothReachabilityIntent,
BluetoothScannerDevice,
BluetoothScanningMode,
HaBleakScannerWrapper,
@@ -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
@@ -20,7 +20,7 @@
"bluetooth-adapters==2.3.0",
"bluetooth-auto-recovery==1.6.4",
"bluetooth-data-tools==1.29.18",
"dbus-fast==5.0.15",
"habluetooth==6.7.9"
"dbus-fast==5.0.16",
"habluetooth==6.8.0"
]
}
@@ -3,6 +3,7 @@
"name": "Sony Bravia TV",
"codeowners": ["@bieniu", "@Drafteed"],
"config_flow": true,
"dependencies": ["ssdp"],
"documentation": "https://www.home-assistant.io/integrations/braviatv",
"integration_type": "device",
"iot_class": "local_polling",
@@ -92,7 +92,7 @@ class BroadlinkRadioFrequency(BroadlinkEntity, RadioFrequencyTransmitterEntity):
"""Representation of a Broadlink RF transmitter."""
_attr_has_entity_name = True
_attr_name = None
_attr_translation_key = "rf_transmitter"
def __init__(self, device: BroadlinkDevice) -> None:
"""Initialize the entity."""
@@ -54,6 +54,11 @@
"name": "IR emitter"
}
},
"radio_frequency": {
"rf_transmitter": {
"name": "RF transmitter"
}
},
"select": {
"day_of_week": {
"name": "Day of week",
+2 -1
View File
@@ -3,6 +3,7 @@
import logging
import caldav
from caldav.lib.error import DAVError
from homeassistant.core import HomeAssistant
@@ -26,7 +27,7 @@ async def async_get_calendars(
for calendar in client.principal().calendars():
try:
supported_components = calendar.get_supported_components()
except KeyError:
except KeyError, DAVError:
needs_warning.append((str(calendar.url), calendar.name, component))
if component in ASSUMED_COMPONENTS:
@@ -66,5 +66,10 @@ async def get_cert_expiry_timestamp(
except ssl.SSLError as err:
raise ValidationFailure(err.args[0]) from err
if not cert or "notAfter" not in cert:
raise ValidationFailure(
f"No certificate expiration found for: {hostname}:{port}"
)
ts_seconds = ssl.cert_time_to_seconds(cert["notAfter"])
return dt_util.utc_from_timestamp(ts_seconds)
@@ -20,6 +20,8 @@ from denonavr.const import (
from denonavr.exceptions import (
AvrCommandError,
AvrForbiddenError,
AvrIncompleteResponseError,
AvrInvalidResponseError,
AvrNetworkError,
AvrProcessingError,
AvrTimoutError,
@@ -191,6 +193,17 @@ def async_log_errors[_DenonDeviceT: DenonDevice, **_P, _R](
self._receiver.host,
)
self._attr_available = False
except AvrInvalidResponseError, AvrIncompleteResponseError:
available = False
if self.available:
_LOGGER.warning(
(
"Denon AVR receiver at host %s returned malformed response. "
"Device is unavailable"
),
self._receiver.host,
)
self._attr_available = False
except AvrCommandError as err:
available = False
_LOGGER.error(
@@ -50,7 +50,7 @@ class DuckDnsUpdateCoordinator(DataUpdateCoordinator[None]):
"""Update Duck DNS."""
retry_after = BACKOFF_INTERVALS[
min(self.failed, len(BACKOFF_INTERVALS))
min(self.failed, len(BACKOFF_INTERVALS) - 1)
].total_seconds()
try:
+12 -2
View File
@@ -86,7 +86,6 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
"""Fetch node data from the Duco box."""
try:
nodes = await self.client.async_get_nodes()
lan_info = await self.client.async_get_lan_info()
except DucoConnectionError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
@@ -100,7 +99,18 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
translation_placeholders={"error": repr(err)},
) from err
# LAN info only backs the diagnostic RSSI sensor, so failures on this
# supplemental endpoint, including connection failures, should not make
# the primary node entities unavailable.
rssi_wifi = self.data.rssi_wifi if self.data else None
try:
lan_info = await self.client.async_get_lan_info()
except DucoError as err:
_LOGGER.debug("Could not fetch Duco LAN info", exc_info=err)
else:
rssi_wifi = lan_info.rssi_wifi
return DucoData(
nodes={node.node_id: node for node in nodes},
rssi_wifi=lan_info.rssi_wifi,
rssi_wifi=rssi_wifi,
)
@@ -294,6 +294,9 @@
"vacuum_raw_get_positions_not_supported": {
"message": "Retrieving the positions of the chargers and the device itself is not supported"
},
"vacuum_send_command_not_supported": {
"message": "The {command} command is not supported by {name}"
},
"vacuum_send_command_params_dict": {
"message": "Params must be a dictionary and not a list"
},
+2 -3
View File
@@ -353,11 +353,10 @@ class EcovacsVacuum(
if self._capability.clean.action.area is None:
info = self._device.device_info
name = info.get("nick", info["name"])
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="vacuum_send_command_area_not_supported",
translation_placeholders={"name": name},
translation_key="vacuum_send_command_not_supported",
translation_placeholders={"command": command, "name": name},
)
if command == "spot_area":
@@ -196,4 +196,6 @@ class EphEmberThermostat(ClimateEntity):
@staticmethod
def map_mode_eph_hass(operation_mode):
"""Map from eph mode to Home Assistant mode."""
if operation_mode is None:
return HVACMode.HEAT_COOL
return EPH_TO_HA_STATE.get(operation_mode.name, HVACMode.HEAT_COOL)
@@ -284,6 +284,19 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
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:
@@ -2,10 +2,12 @@
from flexit_bacnet import (
OPERATION_MODE_AWAY,
OPERATION_MODE_COOKER_HOOD,
OPERATION_MODE_FIREPLACE,
OPERATION_MODE_HIGH,
OPERATION_MODE_HOME,
OPERATION_MODE_OFF,
OPERATION_MODE_TEMPORARY_HIGH,
VENTILATION_MODE_AWAY,
VENTILATION_MODE_HIGH,
VENTILATION_MODE_HOME,
@@ -28,7 +30,9 @@ OPERATION_TO_PRESET_MODE_MAP = {
OPERATION_MODE_AWAY: PRESET_AWAY,
OPERATION_MODE_HOME: PRESET_HOME,
OPERATION_MODE_HIGH: PRESET_HIGH,
OPERATION_MODE_COOKER_HOOD: PRESET_HIGH,
OPERATION_MODE_FIREPLACE: PRESET_FIREPLACE,
OPERATION_MODE_TEMPORARY_HIGH: PRESET_HIGH,
}
# Map preset to ventilation mode (for setting standard modes)
@@ -938,3 +938,15 @@ class AvmWrapper(FritzBoxTools):
"X_AVM-DE_WakeOnLANByMACAddress",
NewMACAddress=mac_address,
)
async def async_get_firmware_extra_infos(self) -> dict[str, Any]:
"""Return extra infos for firmware."""
return await self._async_service_call("UserInterface", "1", "X_AVM-DE_GetInfo")
async def async_get_device_uptime_hours(self) -> int:
"""Get device uptime in hours."""
def _get_uptime_hours() -> int:
return int(self.fritz_status.device_uptime // 3600)
return await self.hass.async_add_executor_job(_get_uptime_hours)
@@ -24,9 +24,11 @@ async def async_get_config_entry_diagnostics(
"unique_id": avm_wrapper.unique_id.replace(
avm_wrapper.unique_id[6:11], "XX:XX"
),
"device_uptime_hours": await avm_wrapper.async_get_device_uptime_hours(),
"current_firmware": avm_wrapper.current_firmware,
"latest_firmware": avm_wrapper.latest_firmware,
"update_available": avm_wrapper.update_available,
"firmware_extra_infos": await avm_wrapper.async_get_firmware_extra_infos(),
"connection_type": avm_wrapper.device_conn_type,
"is_router": avm_wrapper.device_is_router,
"mesh_role": avm_wrapper.mesh_role,
@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260527.0"]
"requirements": ["home-assistant-frontend==20260527.1"]
}
@@ -34,7 +34,11 @@ from requests import RequestException
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryError,
ConfigEntryNotReady,
)
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
@@ -55,6 +59,7 @@ from .const import (
PLATFORMS,
SUPPORTED_DEVICE_TYPES,
V1_API_ERROR_NO_PRIVILEGE,
V1_API_ERROR_RATE_LIMITED,
V1_DEVICE_TYPES,
)
from .coordinator import GrowattConfigEntry, GrowattCoordinator
@@ -264,6 +269,10 @@ def get_device_list_v1(
raise ConfigEntryAuthFailed(
f"Authentication failed for Growatt API: {e.error_msg or str(e)}"
) from e
if e.error_code == V1_API_ERROR_RATE_LIMITED:
raise ConfigEntryNotReady(
f"Growatt API rate limited, will retry: {e.error_msg or str(e)}"
) from e
raise ConfigEntryError(
f"API error during device list: {e.error_msg or str(e)}"
f" (Code: {e.error_code})"
+24 -1
View File
@@ -8,7 +8,7 @@ import os
import struct
from typing import Any
from aiohasupervisor import SupervisorError
from aiohasupervisor import SupervisorBadRequestError, SupervisorError
from aiohasupervisor.models import (
GreenOptions,
HomeAssistantOptions,
@@ -25,6 +25,7 @@ from homeassistant.components.http import (
CONF_SERVER_PORT,
CONF_SSL_CERTIFICATE,
)
from homeassistant.components.onboarding import async_is_onboarded
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
from homeassistant.const import (
EVENT_CORE_CONFIG_UPDATE,
@@ -301,6 +302,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
translation_key="supervisor_not_connected",
) from err
# During onboarding, Supervisor may be out of date. Attempt an update now
# so that core loads against an up-to-date Supervisor. A
# SupervisorBadRequestError means there is no update available, proceed
# normally. No exception means an update was triggered and we must wait for
# it to complete. Any other SupervisorError means something unexpected went
# wrong and we cannot proceed right now.
if not async_is_onboarded(hass):
try:
await supervisor_client.supervisor.update()
except SupervisorBadRequestError:
pass # No update available, proceed normally.
except SupervisorError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="supervisor_not_connected",
) from err
else:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="supervisor_update_pending",
)
# Get or create a refresh token for the Supervisor user
user = hass.data[DATA_HASSIO_SUPERVISOR_USER]
if user.refresh_tokens:
@@ -55,6 +55,9 @@
},
"supervisor_not_connected": {
"message": "Not connected with the supervisor / system too busy"
},
"supervisor_update_pending": {
"message": "Supervisor was out-of-date during onboarding. Update triggered, will retry when complete"
}
},
"issues": {
+6
View File
@@ -1,6 +1,12 @@
{
"entity": {
"light": {
"hue_grouped_light": {
"default": "mdi:lightbulb-group",
"state": {
"off": "mdi:lightbulb-group-off"
}
},
"hue_light": {
"state_attributes": {
"effect": {
-1
View File
@@ -85,7 +85,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
entity_description = LightEntityDescription(
key="hue_grouped_light",
icon="mdi:lightbulb-group",
has_entity_name=True,
name=None,
)
+10 -6
View File
@@ -178,17 +178,21 @@ class HueLight(HueBaseEntity, LightEntity):
@property
def max_color_temp_mireds(self) -> int:
"""Return the warmest color_temp in mireds that this light supports."""
if color_temp := self.resource.color_temperature:
return color_temp.mirek_schema.mirek_maximum
# return a fallback value if the light doesn't provide limits
if (color_temp := self.resource.color_temperature) and (
mirek_max := color_temp.mirek_schema.mirek_maximum
):
return mirek_max
# return a fallback value if the light doesn't provide valid limits
return FALLBACK_MAX_MIREDS
@property
def min_color_temp_mireds(self) -> int:
"""Return the coldest color_temp in mireds that this light supports."""
if color_temp := self.resource.color_temperature:
return color_temp.mirek_schema.mirek_minimum
# return a fallback value if the light doesn't provide limits
if (color_temp := self.resource.color_temperature) and (
mirek_min := color_temp.mirek_schema.mirek_minimum
):
return mirek_min
# return a fallback value if the light doesn't provide valid limits
return FALLBACK_MIN_MIREDS
@property
@@ -4,7 +4,7 @@ from logging import getLogger
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
@@ -79,7 +79,7 @@ class ImmichMediaSource(MediaSource):
],
)
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."""
@@ -137,6 +137,12 @@ class ImmichMediaSource(MediaSource):
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 []
@@ -158,6 +164,12 @@ class ImmichMediaSource(MediaSource):
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 []
@@ -178,6 +190,12 @@ class ImmichMediaSource(MediaSource):
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 []
@@ -211,6 +229,12 @@ class ImmichMediaSource(MediaSource):
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 []
@@ -223,6 +247,12 @@ class ImmichMediaSource(MediaSource):
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 []
@@ -235,12 +265,24 @@ class ImmichMediaSource(MediaSource):
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 []
+1 -1
View File
@@ -67,7 +67,7 @@ async def _async_upload_file(service_call: ServiceCall) -> None:
await coordinator.api.albums.async_add_assets_to_album(
target_album, [upload_result.asset_id]
)
except ImmichError as ex:
except (ImmichError, FileNotFoundError) as ex:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="upload_failed",
@@ -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."
},
@@ -46,6 +46,8 @@ BINARY_SENSORS: Final = (
key=IndevoltSystem.HEATING_STATE,
generation=(1,),
translation_key="electric_heating_state",
on_value=1000,
off_value=1001,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
+13 -1
View File
@@ -939,12 +939,24 @@ class IndevoltSensorEntity(IndevoltEntity, SensorEntity):
@property
def available(self) -> bool:
"""Return False when the device is not in the required energy mode."""
"""Return False for sensors in a non-applicable state."""
# Check whether device is not in the required energy mode
if self.entity_description.energy_mode is not None:
energy_mode = self.coordinator.data.get(IndevoltConfig.READ_ENERGY_MODE)
if energy_mode != self.entity_description.energy_mode:
return False
# Check whether inverter is reporting 0 degrees with heater not active (thus reporting to indicate "idle")
# Pending fix by Indevolt: https://discord.com/channels/1417471269942591571/1510277757689659522
if self.entity_description.key == IndevoltBattery.GEN_1_INVERTER_TEMPERATURE:
inverter_temp = self.coordinator.data.get(
IndevoltBattery.GEN_1_INVERTER_TEMPERATURE
)
heating_state = self.coordinator.data.get(IndevoltSystem.HEATING_STATE)
if inverter_temp == 0 and heating_state != 1000:
return False
return super().available
@property
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["iometer==0.4.0"],
"requirements": ["iometer==1.0.1"],
"zeroconf": ["_iometer._tcp.local."]
}
+1 -1
View File
@@ -94,7 +94,7 @@ async def async_setup_entry(
async_add_entities(device.zones.values())
# create any components not yet created
for controller in disco.pi_disco.controllers.values():
for controller in (await disco.pi_disco.fetch_controllers()).values():
init_controller(controller)
# connect to register any further components
@@ -29,12 +29,13 @@ async def _async_has_devices(hass: HomeAssistant) -> bool:
async with asyncio.timeout(TIMEOUT_DISCOVERY):
await controller_ready.wait()
if not disco.pi_disco.controllers:
controllers = await disco.pi_disco.fetch_controllers()
if not controllers:
await async_stop_discovery_service(hass)
_LOGGER.debug("No controllers found")
return False
_LOGGER.debug("Controllers %s", disco.pi_disco.controllers)
_LOGGER.debug("Controllers %s", controllers)
return True
@@ -52,11 +52,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
"""Set up Jellyfin media source."""
# Currently only a single Jellyfin server is supported
entry: JellyfinConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
coordinator = entry.runtime_data
return JellyfinSource(hass, coordinator.api_client, entry)
return JellyfinSource(hass)
class JellyfinSource(MediaSource):
@@ -64,21 +60,28 @@ class JellyfinSource(MediaSource):
name: str = "Jellyfin"
def __init__(
self, hass: HomeAssistant, client: JellyfinClient, entry: JellyfinConfigEntry
) -> None:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the Jellyfin media source."""
super().__init__(DOMAIN)
self.hass = hass
self.entry = entry
self.entry: JellyfinConfigEntry
self.client: JellyfinClient
self.api: Any
self.url: str
self.client = client
self.api = client.jellyfin
self.url = jellyfin_url(client, "")
def _ensure_loaded(self) -> None:
"""Ensure the Jellyfin integration is loaded and set up instance state."""
if not (entries := self.hass.config_entries.async_loaded_entries(DOMAIN)):
raise BrowseError("Jellyfin integration not loaded")
entry: JellyfinConfigEntry = entries[0]
self.entry = entry
self.client = entry.runtime_data.api_client
self.api = self.client.jellyfin
self.url = jellyfin_url(self.client, "")
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Return a streamable URL and associated mime type."""
self._ensure_loaded()
media_item = await self.hass.async_add_executor_job(
self.api.get_item, item.identifier
)
@@ -94,6 +97,7 @@ class JellyfinSource(MediaSource):
async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource:
"""Return a browsable Jellyfin media source."""
self._ensure_loaded()
if not item.identifier:
return await self._build_libraries()
@@ -70,7 +70,7 @@ class LGTVRS232ConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
port = user_input[CONF_DEVICE]
set_id = user_input[CONF_SET_ID]
set_id = int(user_input[CONF_SET_ID])
self._async_abort_entries_match({CONF_DEVICE: port, CONF_SET_ID: set_id})
error = await _async_attempt_connect(port, set_id)
+3 -1
View File
@@ -108,5 +108,7 @@ def create_matter_ble_proxy(hass: HomeAssistant, ws_url: str) -> MatterBleProxy:
ws_url=ws_url,
scan_source=HaBluetoothScanSource(hass),
device_resolver=HaBluetoothDeviceResolver(hass),
task_factory=hass.async_create_task,
task_factory=lambda coro: hass.async_create_background_task(
coro, name="matter_ble_proxy"
),
)
@@ -1295,7 +1295,7 @@ class MediaPlayerImageView(HomeAssistantView):
data, content_type = await player.async_get_media_image()
if data is None:
return web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)
return web.Response(status=HTTPStatus.NOT_FOUND)
headers: LooseHeaders = {CACHE_CONTROL: "max-age=3600"}
return web.Response(body=data, content_type=content_type, headers=headers)
@@ -80,5 +80,12 @@ class MeteoAlertBinarySensor(BinarySensorEntity):
expiration_date = dt_util.parse_datetime(alert["expires"])
if expiration_date is not None and expiration_date > dt_util.utcnow():
self._attr_extra_state_attributes = alert
self._attr_extra_state_attributes = {
key: (
value.encode("utf-8", errors="replace").decode("utf-8")
if isinstance(value, str)
else value
)
for key, value in alert.items()
}
self._attr_is_on = True
+43 -20
View File
@@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_DISCOVERY,
CONF_PLATFORM,
CONF_PORT,
CONF_PROTOCOL,
SERVICE_RELOAD,
)
@@ -50,6 +51,7 @@ from .client import (
async_subscribe_internal,
publish,
subscribe,
try_connection,
)
from .config import MQTT_BASE_SCHEMA, MQTT_RO_SCHEMA, MQTT_RW_SCHEMA
from .config_integration import CONFIG_SCHEMA_BASE
@@ -79,14 +81,15 @@ from .const import (
CONFIG_ENTRY_VERSION,
DEFAULT_DISCOVERY,
DEFAULT_ENCODING,
DEFAULT_PORT,
DEFAULT_PREFIX,
DEFAULT_PROTOCOL,
DEFAULT_QOS,
DEFAULT_RETAIN,
DOMAIN,
ENTITY_PLATFORMS,
ENTRY_OPTION_FIELDS,
MQTT_CONNECTION_STATE,
PROTOCOL_5,
PROTOCOL_311,
TEMPLATE_ERRORS,
Platform,
@@ -496,25 +499,45 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Load a config entry."""
mqtt_data: MqttData
if (protocol := entry.data.get(CONF_PROTOCOL, PROTOCOL_311)) != DEFAULT_PROTOCOL:
broker: str = entry.data[CONF_BROKER]
async_create_issue(
hass,
DOMAIN,
"protocol_5_migration",
issue_domain=DOMAIN,
is_fixable=True,
breaks_in_ha_version="2027.1.0",
severity=IssueSeverity.WARNING,
learn_more_url="https://www.home-assistant.io/integrations/mqtt/#mqtt-protocol",
data={
"entry_id": entry.entry_id,
"broker": broker,
"protocol": protocol,
},
translation_placeholders={"broker": broker, "protocol": protocol},
translation_key="protocol_5_migration",
)
if (protocol := entry.data.get(CONF_PROTOCOL, PROTOCOL_311)) != PROTOCOL_5:
# Automatically migrate the broker protocol to v5 if possible
# Can be removed with HA Core 2027.1
new_entry_data = entry.data.copy()
new_entry_data[CONF_PROTOCOL] = PROTOCOL_5
# Try the connection with protocol version 5
# And update the protocol if successful
if await hass.async_add_executor_job(
try_connection,
{CONF_PORT: DEFAULT_PORT} | new_entry_data,
):
hass.config_entries.async_update_entry(
entry,
data=new_entry_data,
)
ir.async_delete_issue(hass, DOMAIN, "protocol_5_migration")
_LOGGER.info(
"The MQTT protocol version was successfully updated to version 5"
)
else:
broker: str = entry.data[CONF_BROKER]
async_create_issue(
hass,
DOMAIN,
"protocol_5_migration",
issue_domain=DOMAIN,
is_fixable=False,
breaks_in_ha_version="2027.1.0",
severity=IssueSeverity.WARNING,
learn_more_url="https://www.home-assistant.io/integrations/mqtt/"
"#mqtt-protocol",
data={
"entry_id": entry.entry_id,
"broker": broker,
"protocol": protocol,
},
translation_placeholders={"broker": broker, "protocol": protocol},
translation_key="protocol_5_migration",
)
async def _setup_client() -> tuple[MqttData, dict[str, Any]]:
"""Set up the MQTT client."""
+37
View File
@@ -9,6 +9,7 @@ from functools import lru_cache, partial
from itertools import chain, groupby
import logging
from operator import attrgetter
import queue
import socket
import ssl
import time
@@ -92,6 +93,8 @@ from .util import EnsureJobAfterCooldown, get_file_path, mqtt_config_entry_enabl
_LOGGER = logging.getLogger(__name__)
MQTT_TIMEOUT = 5
MIN_BUFFER_SIZE = 131072 # Minimum buffer size to use if preferred size fails
PREFERRED_BUFFER_SIZE = 8 * 1024 * 1024 # Set receive buffer size to 8MiB
@@ -433,6 +436,40 @@ class MqttClientSetup:
return self._client
def try_connection(
user_input: dict[str, Any],
) -> bool:
"""Test if we can connect to an MQTT broker."""
mqtt_client_setup = MqttClientSetup(user_input)
mqtt_client_setup.setup()
client = mqtt_client_setup.client
result: queue.Queue[bool] = queue.Queue(maxsize=1)
def on_connect(
_mqttc: mqtt.Client,
_userdata: None,
_connect_flags: mqtt.ConnectFlags,
reason_code: mqtt.ReasonCode,
_properties: mqtt.Properties | None = None,
) -> None:
"""Handle connection result."""
result.put(not reason_code.is_failure)
client.on_connect = on_connect
client.connect_async(user_input[CONF_BROKER], user_input[CONF_PORT])
client.loop_start()
try:
return result.get(timeout=MQTT_TIMEOUT)
except queue.Empty:
return False
finally:
client.disconnect()
client.loop_stop()
class MQTT:
"""Home Assistant MQTT client."""
+7 -46
View File
@@ -8,7 +8,6 @@ from dataclasses import dataclass
from enum import IntEnum
import json
import logging
import queue
from ssl import PROTOCOL_TLS_CLIENT, SSLContext, SSLError
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, cast
@@ -22,7 +21,6 @@ from cryptography.hazmat.primitives.serialization import (
load_pem_private_key,
)
from cryptography.x509 import load_der_x509_certificate, load_pem_x509_certificate
import paho.mqtt.client as mqtt
import voluptuous as vol
import yaml
@@ -143,7 +141,7 @@ from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
from homeassistant.util.unit_conversion import TemperatureConverter
from .addon import get_addon_manager
from .client import MqttClientSetup
from .client import try_connection
from .const import (
ALARM_CONTROL_PANEL_SUPPORTED_FEATURES,
ATTR_PAYLOAD,
@@ -444,8 +442,6 @@ ADDON_SETUP_TIMEOUT_ROUNDS = 5
CONF_CLIENT_KEY_PASSWORD = "client_key_password"
MQTT_TIMEOUT = 5
ADVANCED_OPTIONS = "advanced_options"
SET_CA_CERT = "set_ca_cert"
SET_CLIENT_CERT = "set_client_cert"
@@ -5457,7 +5453,6 @@ async def async_get_broker_settings(
or current_client_certificate
or current_client_key
or current_tls_insecure
or current_protocol != DEFAULT_PROTOCOL
or current_config.get(SET_CA_CERT, "off") != "off"
or current_config.get(SET_CLIENT_CERT)
or current_transport == TRANSPORT_WEBSOCKETS
@@ -5466,6 +5461,12 @@ async def async_get_broker_settings(
# Build form
fields[vol.Required(CONF_BROKER, default=current_broker)] = TEXT_SELECTOR
fields[vol.Required(CONF_PORT, default=current_port)] = PORT_SELECTOR
fields[
vol.Optional(
CONF_PROTOCOL,
description={"suggested_value": current_protocol},
)
] = PROTOCOL_SELECTOR
fields[
vol.Optional(
CONF_USERNAME,
@@ -5556,12 +5557,6 @@ async def async_get_broker_settings(
description={"suggested_value": current_tls_insecure},
)
] = BOOLEAN_SELECTOR
fields[
vol.Optional(
CONF_PROTOCOL,
description={"suggested_value": current_protocol},
)
] = PROTOCOL_SELECTOR
fields[
vol.Optional(
CONF_TRANSPORT,
@@ -5582,40 +5577,6 @@ async def async_get_broker_settings(
return False
def try_connection(
user_input: dict[str, Any],
) -> bool:
"""Test if we can connect to an MQTT broker."""
mqtt_client_setup = MqttClientSetup(user_input)
mqtt_client_setup.setup()
client = mqtt_client_setup.client
result: queue.Queue[bool] = queue.Queue(maxsize=1)
def on_connect(
_mqttc: mqtt.Client,
_userdata: None,
_connect_flags: mqtt.ConnectFlags,
reason_code: mqtt.ReasonCode,
_properties: mqtt.Properties | None = None,
) -> None:
"""Handle connection result."""
result.put(not reason_code.is_failure)
client.on_connect = on_connect
client.connect_async(user_input[CONF_BROKER], user_input[CONF_PORT])
client.loop_start()
try:
return result.get(timeout=MQTT_TIMEOUT)
except queue.Empty:
return False
finally:
client.disconnect()
client.loop_stop()
def check_certicate_chain() -> str | None:
"""Check the MQTT certificates."""
if client_certificate := get_file_path(CONF_CLIENT_CERT):
@@ -175,10 +175,10 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
self._attr_latitude = None
self._attr_longitude = None
_LOGGER.warning(
"Extra state attributes received at % and template %s "
"Extra state attributes received at %s and template %s "
"contain invalid or incomplete location info. Got %s",
self._config.get(CONF_JSON_ATTRS_TEMPLATE),
self._config.get(CONF_JSON_ATTRS_TOPIC),
self._config.get(CONF_JSON_ATTRS_TEMPLATE),
extra_state_attributes,
)
@@ -190,11 +190,11 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
self._attr_location_accuracy = gps_accuracy
else:
_LOGGER.warning(
"Extra state attributes received at % and template %s "
"Extra state attributes received at %s and template %s "
"contain invalid GPS accuracy setting, "
"gps_accuracy was set to 0 as the default. Got %s",
self._config.get(CONF_JSON_ATTRS_TEMPLATE),
self._config.get(CONF_JSON_ATTRS_TOPIC),
self._config.get(CONF_JSON_ATTRS_TEMPLATE),
extra_state_attributes,
)
self._attr_location_accuracy = 0
+1 -1
View File
@@ -530,7 +530,7 @@ class MqttAttributesMixin(Entity):
self._attributes_message_received,
{
"_attr_extra_state_attributes",
"_attr_gps_accuracy",
"_attr_location_accuracy",
"_attr_latitude",
"_attr_location_name",
"_attr_longitude",
+1 -56
View File
@@ -5,12 +5,10 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.repairs import RepairsFlow, RepairsFlowResult
from homeassistant.const import CONF_PORT, CONF_PROTOCOL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from .config_flow import try_connection
from .const import DEFAULT_PORT, DOMAIN, PROTOCOL_5
from .const import DOMAIN
URL_MQTT_BROKER_CONFIGURATION = (
"https://www.home-assistant.io/integrations/mqtt/#broker-configuration"
@@ -55,55 +53,6 @@ class MQTTDeviceEntryMigration(RepairsFlow):
)
class MQTTProtocolV5Migration(RepairsFlow):
"""Handler to migrate to MQTT protocol version 5."""
def __init__(self, entry_id: str, broker: str, protocol: str) -> None:
"""Initialize the flow."""
self.entry_id = entry_id
self.broker = broker
self.protocol = protocol
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> RepairsFlowResult:
"""Handle the first step of a fix flow."""
return await self.async_step_confirm()
async def async_step_confirm(
self, user_input: dict[str, str] | None = None
) -> RepairsFlowResult:
"""Handle the confirm step of a fix flow."""
if user_input is not None:
entry = self.hass.config_entries.async_get_entry(self.entry_id)
if TYPE_CHECKING:
assert entry is not None
new_entry_data = entry.data.copy()
new_entry_data[CONF_PROTOCOL] = PROTOCOL_5
# Try the connection with protocol version 5
if await self.hass.async_add_executor_job(
try_connection,
{CONF_PORT: DEFAULT_PORT} | new_entry_data,
):
self.hass.config_entries.async_update_entry(entry, data=new_entry_data)
return self.async_create_entry(data={})
return self.async_abort(
reason="mqtt_broker_migration_to_v5_failed",
description_placeholders={
"broker": self.broker,
"protocol": self.protocol,
"url_mqtt_broker_configuration": URL_MQTT_BROKER_CONFIGURATION,
},
)
return self.async_show_form(
step_id="confirm",
data_schema=vol.Schema({}),
description_placeholders={"broker": self.broker, "protocol": self.protocol},
)
async def async_create_fix_flow(
hass: HomeAssistant,
issue_id: str,
@@ -113,10 +62,6 @@ async def async_create_fix_flow(
if TYPE_CHECKING:
assert data is not None
entry_id: str = data["entry_id"] # type: ignore[assignment]
if issue_id == "protocol_5_migration":
broker: str = data["broker"] # type: ignore[assignment]
protocol: str = data["protocol"] # type: ignore[assignment]
return MQTTProtocolV5Migration(entry_id, broker, protocol)
subentry_id: str = data["subentry_id"] # type: ignore[assignment]
name: str = data["name"] # type: ignore[assignment]
return MQTTDeviceEntryMigration(
+3 -13
View File
@@ -56,7 +56,7 @@
"keepalive": "A value less than 90 seconds is advised.",
"password": "The password to log in to your MQTT broker.",
"port": "The port your MQTT broker listens to. For example 1883.",
"protocol": "The MQTT protocol your broker operates at. For example 3.1.1.",
"protocol": "The MQTT protocol version that is used. Note that Home Assistant will silently change to version 5 if your broker supports it.",
"set_ca_cert": "Select **Auto** for automatic CA validation, or **Custom** and select **Next** to set a custom CA certificate, to allow validating your MQTT broker's certificate.",
"set_client_cert": "Enable and select **Next** to set a client certificate and private key to authenticate against your MQTT broker.",
"tls_insecure": "Option to ignore validation of your MQTT broker's certificate.",
@@ -1134,18 +1134,8 @@
"title": "Invalid config found for MQTT {domain} item"
},
"protocol_5_migration": {
"fix_flow": {
"abort": {
"mqtt_broker_migration_to_v5_failed": "Migrating the broker ({broker}) protocol version from {protocol} to 5 failed, and the migration has been aborted.\n\nYour broker may not support MQTT protocol version 5.\n\nPlease [reconfigure your MQTT broker settings]({url_mqtt_broker_configuration}) or upgrade your broker to support MQTT protocol version 5 to fix this issue."
},
"step": {
"confirm": {
"description": "Home Assistant is migrating to MQTT protocol version 5. The currently configured protocol version for broker {broker} is {protocol}. This protocol version is deprecated, and support for it will be removed.\n\nSubmitting this form will try to migrate your MQTT broker configuration to use protocol version 5 to fix this issue.",
"title": "MQTT protocol change required"
}
}
},
"title": "Deprecated MQTT protocol {protocol} in use"
"description": "The automatic migration to MQTT protocol version 5 failed. The currently configured protocol version for MQTT broker {broker} is {protocol}, but this protocol version is deprecated, and support for it will be removed.\n\nMake sure your broker supports protocol version 5. Update your MQTT broker's connection settings, and restart Home Assistant to fix this issue.",
"title": "MQTT protocol migration failed"
},
"subentry_migration_discovery": {
"fix_flow": {
+4 -6
View File
@@ -271,7 +271,7 @@ class MqttValve(MqttEntity, ValveEntity):
self._range, float(position_payload)
)
except ValueError:
_LOGGER.warning(
_LOGGER.debug(
"Ignoring non numeric payload '%s' received on topic '%s'",
position_payload,
msg.topic,
@@ -279,9 +279,9 @@ class MqttValve(MqttEntity, ValveEntity):
else:
percentage_payload = min(max(percentage_payload, 0), 100)
self._attr_current_valve_position = percentage_payload
# Reset closing and opening if the valve is fully opened or fully closed
if state is None and percentage_payload in (0, 100):
state = RESET_CLOSING_OPENING
# Reset opening/closing when a position update is received
# without an explicit opening/closing transitional state.
state = state or RESET_CLOSING_OPENING
position_set = True
if state_payload and state is None and not position_set:
_LOGGER.warning(
@@ -291,8 +291,6 @@ class MqttValve(MqttEntity, ValveEntity):
state_payload,
)
return
if state is None:
return
self._update_state(state)
@callback
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["pynintendoauth", "pynintendoparental"],
"quality_scale": "bronze",
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.4"]
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.4.0"]
}
+5
View File
@@ -52,6 +52,11 @@ class OMIEPriceSensor(CoordinatorEntity[OMIECoordinator], SensorEntity):
self._attr_unique_id = pyomie_series_name
self._pyomie_series_name = pyomie_series_name
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
await super().async_added_to_hass()
self._handle_coordinator_update()
@callback
def _handle_coordinator_update(self) -> None:
"""Update this sensor's state from the coordinator results."""
+1 -1
View File
@@ -14,7 +14,7 @@
"iot_class": "local_push",
"loggers": ["onvif", "wsdiscovery", "zeep"],
"requirements": [
"onvif-zeep-async==4.1.0",
"onvif-zeep-async==4.1.1",
"onvif_parsers==2.3.0",
"WSDiscovery==2.1.2"
]
@@ -57,6 +57,7 @@ OVERKIZ_TO_HVAC_MODE: dict[str, HVACMode] = {
OverkizCommandParam.STANDBY: HVACMode.OFF, # main command
OverkizCommandParam.AUTO: HVACMode.AUTO,
OverkizCommandParam.EXTERNAL: HVACMode.AUTO,
OverkizCommandParam.PROG: HVACMode.AUTO,
OverkizCommandParam.INTERNAL: HVACMode.AUTO, # main command
}
@@ -144,7 +144,7 @@ async def on_device_available(
coordinator: OverkizDataUpdateCoordinator, event: Event
) -> None:
"""Handle device available event."""
if event.device_url:
if event.device_url and event.device_url in coordinator.devices:
coordinator.devices[event.device_url].available = True
@@ -154,7 +154,7 @@ async def on_device_unavailable_disabled(
coordinator: OverkizDataUpdateCoordinator, event: Event
) -> None:
"""Handle device unavailable / disabled event."""
if event.device_url:
if event.device_url and event.device_url in coordinator.devices:
coordinator.devices[event.device_url].available = False
@@ -174,7 +174,7 @@ async def on_device_state_changed(
coordinator: OverkizDataUpdateCoordinator, event: Event
) -> None:
"""Handle device state changed event."""
if not event.device_url:
if not event.device_url or event.device_url not in coordinator.devices:
return
for state in event.device_states:
@@ -198,7 +198,7 @@ async def on_device_removed(
):
registry.async_remove_device(registered_device.id)
if event.device_url:
if event.device_url and event.device_url in coordinator.devices:
del coordinator.devices[event.device_url]
@@ -854,6 +854,9 @@ class OverkizCover(OverkizDescriptiveEntity, CoverEntity):
if current_value is None or target_value is None:
return None
if current_value in (_POSITION_MY, _POSITION_UNKNOWN):
return None
return current_value - target_value
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/radio_frequency",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["rf-protocols==4.0.0"]
"requirements": ["rf-protocols==4.0.1"]
}
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
"quality_scale": "silver",
"requirements": ["renault-api==0.5.10"]
"requirements": ["renault-api==0.5.11"]
}
+5 -1
View File
@@ -29,7 +29,11 @@ class IRobotEntity(Entity):
model=self.vacuum_state.get("sku"),
name=str(self.vacuum_state.get("name")),
sw_version=self.vacuum_state.get("softwareVer"),
hw_version=self.vacuum_state.get("hardwareRev"),
hw_version=(
str(hw_rev)
if (hw_rev := self.vacuum_state.get("hardwareRev")) is not None
else None
),
)
if mac_address := self.vacuum_state.get("hwPartsRev", {}).get(
@@ -38,7 +38,7 @@
"requirements": [
"getmac==0.9.5",
"samsungctl[websocket]==0.7.1",
"samsungtvws[async,encrypted]==2.7.2",
"samsungtvws[async,encrypted]==3.0.5",
"wakeonlan==3.3.0",
"async-upnp-client==0.46.2"
],
+5 -1
View File
@@ -42,4 +42,8 @@ class SchlageEntity(CoordinatorEntity[SchlageDataUpdateCoordinator]):
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.device_id in self.coordinator.data.locks
return (
super().available
and self.device_id in self.coordinator.data.locks
and self._lock.connected
)
+8 -5
View File
@@ -244,7 +244,8 @@ def async_restore_rpc_attribute_entities(
sensor_class: Callable,
) -> None:
"""Restore RPC attributes entities."""
entities = []
entities: list[Entity] = []
sleep_period = config_entry.data[CONF_SLEEP_PERIOD]
ent_reg = er.async_get(hass)
entries = er.async_entries_for_config_entry(ent_reg, config_entry.entry_id)
@@ -259,11 +260,13 @@ def async_restore_rpc_attribute_entities(
attribute = entry.unique_id.split("-")[-1]
if description := sensors.get(attribute):
entities.append(
get_entity_class(sensor_class, description)(
coordinator, key, attribute, description, entry
entity_class = get_entity_class(sensor_class, description)
if sleep_period:
entities.append(
entity_class(coordinator, key, attribute, description, entry)
)
)
else:
entities.append(entity_class(coordinator, key, attribute, description))
if not entities:
return
@@ -132,6 +132,9 @@ def async_manage_outbound_websocket_incorrectly_enabled_issue(
device = entry.runtime_data.rpc.device
if not device.initialized:
return
if (
(ws_config := device.config.get("ws"))
and ws_config["enable"]
@@ -169,6 +172,9 @@ def async_manage_open_wifi_ap_issue(
device = entry.runtime_data.rpc.device
if not device.initialized:
return
# Check if WiFi AP is enabled and is open (no password)
if (
(wifi_config := device.config.get("wifi"))
@@ -72,8 +72,10 @@ async def async_setup_entry(
for device in entry_data.devices.values()
for component in device.status
if (
Capability.SWITCH in device.status[MAIN]
and any(capability in device.status[MAIN] for capability in CAPABILITIES)
Capability.SWITCH in device.status[component]
and any(
capability in device.status[component] for capability in CAPABILITIES
)
and Capability.SAMSUNG_CE_LAMP not in device.status[component]
)
]
+17 -1
View File
@@ -6,6 +6,7 @@ from typing import Any
import switchbot
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import BluetoothReachabilityIntent
from homeassistant.components.sensor import ConfigType
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -310,6 +311,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: SwitchbotConfigEntry) ->
translation_placeholders={
"sensor_type": entry.data[CONF_SENSOR_TYPE],
"address": entry.data[CONF_ADDRESS],
"reason": bluetooth.async_address_reachability_diagnostics(
hass,
entry.data[CONF_ADDRESS].upper(),
BluetoothReachabilityIntent.CONNECTION,
),
},
)
@@ -331,7 +337,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: SwitchbotConfigEntry) ->
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_not_found_error",
translation_placeholders={"sensor_type": sensor_type, "address": address},
translation_placeholders={
"sensor_type": sensor_type,
"address": address,
"reason": bluetooth.async_address_reachability_diagnostics(
hass,
address.upper(),
BluetoothReachabilityIntent.CONNECTION
if connectable
else BluetoothReachabilityIntent.PASSIVE_ADVERTISEMENT,
),
},
)
cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice)
@@ -384,7 +384,7 @@
"message": "The device ID {device_id} does not belong to SwitchBot integration."
},
"device_not_found_error": {
"message": "Could not find Switchbot {sensor_type} with address {address}"
"message": "Could not find Switchbot {sensor_type} with address {address}: {reason}"
},
"device_without_config_entry": {
"message": "The device ID {device_id} is not associated with a config entry."
+6 -4
View File
@@ -40,6 +40,8 @@ class TadoConfigFlow(ConfigFlow, domain=DOMAIN):
login_task: asyncio.Task | None = None
refresh_token: str | None = None
tado: Tado | None = None
tado_device_url: str = ""
user_code: str = ""
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
@@ -69,8 +71,8 @@ class TadoConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Error while initiating Tado")
return self.async_abort(reason="cannot_connect")
assert self.tado is not None
tado_device_url = self.tado.device_verification_url()
user_code = URL(tado_device_url).query["user_code"]
self.tado_device_url = self.tado.device_verification_url()
self.user_code = URL(self.tado_device_url).query["user_code"]
async def _wait_for_login() -> None:
"""Wait for the user to login."""
@@ -119,8 +121,8 @@ class TadoConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="user",
progress_action="wait_for_device",
description_placeholders={
"url": tado_device_url,
"code": user_code,
"url": self.tado_device_url,
"code": self.user_code,
},
progress_task=self.login_task,
)
+5
View File
@@ -36,6 +36,11 @@ class TedeeEntity(CoordinatorEntity[TedeeApiCoordinator]):
via_device=(DOMAIN, coordinator.bridge.serial),
)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self._lock.is_connected
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
+1 -5
View File
@@ -89,11 +89,7 @@ class TedeeLockEntity(TedeeEntity, LockEntity):
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available
and self._lock.is_connected
and self._lock.state != TedeeLockState.UNCALIBRATED
)
return super().available and self._lock.state != TedeeLockState.UNCALIBRATED
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the door."""
-2
View File
@@ -102,14 +102,12 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity):
"""Return the current state of the burner."""
return {"heating_type": self.coordinator.data.agreement.heating_type}
# pylint: disable-next=home-assistant-action-swallowed-exception
@toon_exception_handler
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Change the setpoint of the thermostat."""
temperature = kwargs.get(ATTR_TEMPERATURE)
await self.coordinator.toon.set_current_setpoint(temperature)
# pylint: disable-next=home-assistant-action-swallowed-exception
@toon_exception_handler
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
+12 -8
View File
@@ -1,14 +1,14 @@
"""Helpers for Toon."""
from collections.abc import Callable, Coroutine
import logging
from typing import Any, Concatenate
from toonapi import ToonConnectionError, ToonError
from .entity import ToonEntity
from homeassistant.exceptions import HomeAssistantError
_LOGGER = logging.getLogger(__name__)
from .const import DOMAIN
from .entity import ToonEntity
def toon_exception_handler[_ToonEntityT: ToonEntity, **_P](
@@ -17,20 +17,24 @@ def toon_exception_handler[_ToonEntityT: ToonEntity, **_P](
"""Decorate Toon calls to handle Toon exceptions.
A decorator that wraps the passed in function, catches Toon errors,
and handles the availability of the device in the data coordinator.
and raises a translated ``HomeAssistantError``.
"""
async def handler(self: _ToonEntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:
try:
await func(self, *args, **kwargs)
self.coordinator.async_update_listeners()
except ToonConnectionError as error:
_LOGGER.error("Error communicating with API: %s", error)
self.coordinator.last_update_success = False
self.coordinator.async_update_listeners()
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="communication_error",
) from error
except ToonError as error:
_LOGGER.error("Invalid response from API: %s", error)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="invalid_response",
) from error
return handler
@@ -34,6 +34,12 @@
}
},
"exceptions": {
"communication_error": {
"message": "An error occurred while communicating with the Toon device."
},
"invalid_response": {
"message": "Received an invalid response from the Toon device."
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
-4
View File
@@ -60,7 +60,6 @@ class ToonSwitch(ToonEntity, SwitchEntity):
class ToonProgramSwitch(ToonSwitch, ToonDisplayDeviceEntity):
"""Defines a Toon program switch."""
# pylint: disable-next=home-assistant-action-swallowed-exception
@toon_exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the Toon program switch."""
@@ -68,7 +67,6 @@ class ToonProgramSwitch(ToonSwitch, ToonDisplayDeviceEntity):
ACTIVE_STATE_AWAY, PROGRAM_STATE_OFF
)
# pylint: disable-next=home-assistant-action-swallowed-exception
@toon_exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the Toon program switch."""
@@ -80,7 +78,6 @@ class ToonProgramSwitch(ToonSwitch, ToonDisplayDeviceEntity):
class ToonHolidayModeSwitch(ToonSwitch, ToonDisplayDeviceEntity):
"""Defines a Toon Holiday mode switch."""
# pylint: disable-next=home-assistant-action-swallowed-exception
@toon_exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the Toon holiday mode switch."""
@@ -88,7 +85,6 @@ class ToonHolidayModeSwitch(ToonSwitch, ToonDisplayDeviceEntity):
ACTIVE_STATE_AWAY, PROGRAM_STATE_ON
)
# pylint: disable-next=home-assistant-action-swallowed-exception
@toon_exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the Toon holiday mode switch."""
@@ -34,6 +34,7 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity):
"""Tractive device tracker."""
_attr_translation_key = "tracker"
_attr_name = None
def __init__(self, client: TractiveClient, item: Trackables) -> None:
"""Initialize tracker entity."""
+2 -1
View File
@@ -29,7 +29,8 @@ class TractiveEntity(Entity):
self._attr_device_info = DeviceInfo(
configuration_url="https://my.tractive.com/",
identifiers={(DOMAIN, tracker_details["_id"])},
name=f"Tracker {tracker_details['_id']}",
translation_key="tracker",
translation_placeholders={"id": tracker_details["_id"]},
manufacturer="Tractive GmbH",
sw_version=tracker_details["fw_version"],
model_id=tracker_details["model_number"],
@@ -20,6 +20,11 @@
}
}
},
"device": {
"tracker": {
"name": "Tracker {id}"
}
},
"entity": {
"binary_sensor": {
"tracker_power_saving": {
+1 -1
View File
@@ -44,7 +44,7 @@
"iot_class": "cloud_push",
"loggers": ["tuya_sharing"],
"requirements": [
"tuya-device-handlers==0.0.21",
"tuya-device-handlers==0.0.22",
"tuya-device-sharing-sdk==0.2.8"
]
}
+4
View File
@@ -75,6 +75,10 @@ class VolvoLock(VolvoEntity, LockEntity):
def _update_state(self, api_field: VolvoCarsApiBaseModel | None) -> None:
"""Update the state of the entity."""
if api_field is None:
self._attr_is_locked = None
return
assert isinstance(api_field, VolvoCarsValue)
self._attr_is_locked = api_field.value == "LOCKED"
+1 -1
View File
@@ -8,6 +8,6 @@
"iot_class": "local_push",
"loggers": ["wiim.sdk", "async_upnp_client"],
"quality_scale": "bronze",
"requirements": ["wiim==0.1.2"],
"requirements": ["wiim==0.1.4"],
"zeroconf": ["_linkplay._tcp.local."]
}
@@ -349,15 +349,12 @@ class WiimMediaPlayerEntity(WiimBaseEntity, MediaPlayerEntity):
sdk_status_str,
)
else:
self._device.playing_status = sdk_status
if sdk_status == SDKPlayingStatus.STOPPED:
LOGGER.debug(
"Device %s: TransportState is STOPPED."
" Resetting media position and metadata",
self.entity_id,
)
self._device.current_position = 0
self._device.current_track_duration = 0
self._attr_media_position_updated_at = None
self._attr_media_duration = None
self._attr_media_position = None
+5
View File
@@ -112,3 +112,8 @@ class WLEDUpdateEntity(WLEDEntity, UpdateEntity):
version = cast(str, self.latest_version)
await self.coordinator.wled.upgrade(version=version)
await self.coordinator.async_refresh()
async def async_update(self) -> None:
"""Update the entity."""
await super().async_update()
await self.releases_coordinator.async_request_refresh()
@@ -468,7 +468,7 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
async def on_restart(self) -> None:
"""Block until pipeline loop will be restarted."""
_LOGGER.warning(
_LOGGER.debug(
"Satellite has been disconnected. Reconnecting in %s second(s)",
_RECONNECT_SECONDS,
)
+1 -1
View File
@@ -14,5 +14,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["socketio", "engineio", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.3.0"]
"requirements": ["yalexs==9.2.1", "yalexs-ble==3.3.0"]
}
@@ -63,7 +63,7 @@ class MusicCastDeviceEntity(MusicCastEntity):
},
manufacturer=BRAND,
model=self.coordinator.data.model_name,
sw_version=self.coordinator.data.system_version,
sw_version=str(self.coordinator.data.system_version),
)
if self._zone_id == DEFAULT_ZONE:
@@ -44,7 +44,7 @@
"name": "Rain delay"
},
"water_hammer_duration": {
"name": "Water hammer reduction"
"name": "Water hammer duration"
},
"zone_delay": {
"name": "Zone delay"
@@ -6,8 +6,6 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
LocalOAuth2ImplementationWithPkce,
)
from .const import YOTO_AUDIENCE, YOTO_SCOPES
AUTHORIZE_URL = "https://login.yotoplay.com/authorize"
TOKEN_URL = "https://login.yotoplay.com/oauth/token"
@@ -16,9 +14,9 @@ async def async_get_auth_implementation(
hass: HomeAssistant,
auth_domain: str,
credential: ClientCredential,
) -> YotoOAuth2Implementation:
"""Return a Yoto OAuth2 implementation backed by the user's credential."""
return YotoOAuth2Implementation(
) -> LocalOAuth2ImplementationWithPkce:
"""Return a Yoto OAuth2 implementation with PKCE."""
return LocalOAuth2ImplementationWithPkce(
hass,
auth_domain,
credential.client_id,
@@ -26,15 +24,3 @@ async def async_get_auth_implementation(
TOKEN_URL,
credential.client_secret,
)
class YotoOAuth2Implementation(LocalOAuth2ImplementationWithPkce):
"""Yoto OAuth2 implementation with PKCE, audience and scopes."""
@property
def extra_authorize_data(self) -> dict:
"""Append Yoto's audience and scopes to every authorize URL."""
return super().extra_authorize_data | {
"audience": YOTO_AUDIENCE,
"scope": " ".join(YOTO_SCOPES),
}

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