Skip to content
Merged

2026.5.3 #171185

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0830988
Bump qbittorrent-api to 2026.5.1 (#170181)
lumatijev May 10, 2026
b26c2f3
Improve iaqualink 429 handling (#170231)
flz May 19, 2026
356e6a6
Fix Apple TV keyboard focus binary_sensor missing on cold start (#170…
kroehre May 19, 2026
47d8adc
Add tilt controls for UpDownSheerScreen in Overkiz (#170563)
dankarization May 16, 2026
3293ebc
Fix ValueError when turning on blebox light with brightness set to 0 …
bkobus-bbx May 18, 2026
249b543
Bump aiodns to 4.0.3 (#170865)
bdraco May 16, 2026
37d6449
Populate uid and recurrence_id in CalDAV calendar events (#170910)
frenck May 16, 2026
95cc9ae
Fix is_closed state for SlidingDiscreteGateWithPedestrianPosition cov…
iMicknl May 16, 2026
2f35ad2
Disable USB discovery for teleinfo (#170933)
puddly May 16, 2026
d366027
Fix utility meter next_reset shifting forward on entity rename (#170957)
frenck May 17, 2026
228ac01
Use correct state_class for utility meters with device classes that d…
frenck May 17, 2026
4a96880
Reduce GoodWe connect retries to avoid blocking startup (#170964)
frenck May 17, 2026
5e45f37
Fix is_closed state for DiscretePositionableGarageDoor in Overkiz (#1…
iMicknl May 18, 2026
070de13
Fix controls for OpenCloseGate4T (rts:GateOpenerRTS4TComponent) in Ov…
iMicknl May 18, 2026
2456753
Prevent Google Assistant entity sync from blocking startup (#170991)
frenck May 17, 2026
ea08479
Load template extensions by class to prevent import deadlock (#170995)
frenck May 17, 2026
0bc0745
Use asyncio.get_running_loop() in emulated_hue UPnP responder (#171000)
frenck May 17, 2026
771b016
Fix Netatmo valve KeyError when hvac_action state is unavailable in O…
frenck May 17, 2026
db8589b
Fix time trigger crash when using entity_id dict format without offse…
frenck May 17, 2026
17e1050
Fix threshold preview crash when hysteresis is not provided (#171009)
frenck May 17, 2026
5a76f3b
Fix Growatt mix device IndexError when chart data is empty (#171012)
frenck May 17, 2026
37478d3
Fix SleepIQ timer units: seconds should be minutes for core climate a…
frenck May 17, 2026
a314f7b
Fix Control4 climate crash when humidity is 'Undefined' (#171015)
frenck May 17, 2026
d39775a
Fix manual alarm panel crash on restore with invalid state (#171016)
frenck May 17, 2026
266767e
Handle Daikin connection errors gracefully in coordinator (#171017)
frenck May 17, 2026
8d66752
Fix shorthand template conditions in choose blocks crashing all autom…
frenck May 17, 2026
6b15f9a
Add additional overrides to cover entity in Overkiz (#171019)
iMicknl May 18, 2026
8e1a04d
Fix Verisure alarm crash when cloud rejects arm/disarm command (#171024)
frenck May 17, 2026
51589ec
Add stop command to Overkiz pergola horizontal awning covers (#171034)
frenck May 18, 2026
cd6c3c8
Fix WeatherFlow websocket crash when data payload is None (#171037)
frenck May 18, 2026
311e5a9
Bump pyIntesishome to 1.8.8 (#171041)
jnimmo May 18, 2026
ebc582c
Return media_content_id as string in forked_daapd (#171059)
frenck May 18, 2026
ee734de
Bump aioimmich to 0.14.1 (#171138)
mib1185 May 18, 2026
7ebaaf1
Fix controls for UpDownGarageDoor4T and additional 4T covers in Overk…
iMicknl May 19, 2026
e8295e1
Fix ZHA config entries using a URI without a port (#171164)
puddly May 19, 2026
1e90882
Fix is_closed state and position for DynamicPergola covers in Overkiz…
iMicknl May 18, 2026
dc9116a
Fix tilt and position support for VenetianBlind covers in Overkiz (#1…
iMicknl May 18, 2026
54aba11
Bump version to 2026.5.3
frenck May 19, 2026
0e09019
Fix blebox light temperature scaling (#170573)
bkobus-bbx May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions homeassistant/components/apple_tv/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import CONF_NAME
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

Expand All @@ -23,23 +23,33 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Load Apple TV binary sensor based on a config entry."""
# apple_tv config entries always have a unique id
manager = config_entry.runtime_data
cb: CALLBACK_TYPE
added = False

@callback
def setup_entities(atv: AppleTV) -> None:
nonlocal added
if added:
return
if atv.features.in_state(FeatureState.Available, FeatureName.TextFocusState):
assert config_entry.unique_id is not None
name: str = config_entry.data[CONF_NAME]
async_add_entities(
[AppleTVKeyboardFocused(name, config_entry.unique_id, manager)]
)
cb()
added = True

cb = async_dispatcher_connect(
hass, f"{SIGNAL_CONNECTED}_{config_entry.unique_id}", setup_entities
config_entry.async_on_unload(
async_dispatcher_connect(
hass, f"{SIGNAL_CONNECTED}_{config_entry.unique_id}", setup_entities
)
)
config_entry.async_on_unload(cb)

# The manager may have already connected (and dispatched SIGNAL_CONNECTED)
# before this platform was forwarded, in which case the signal above was
# missed; handle that case directly.
if manager.atv is not None:
setup_entities(manager.atv)


class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener):
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/blebox/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@

DEFAULT_HOST = "192.168.0.2"
DEFAULT_PORT = 80


LIGHT_MAX_KELVINS = 6500 # 154 Mireds
LIGHT_MIN_KELVINS = 2700 # 370 Mireds
55 changes: 46 additions & 9 deletions homeassistant/components/blebox/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from datetime import timedelta
import logging
import math
from typing import Any

import blebox_uniapi.light
Expand All @@ -22,9 +23,9 @@
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import color as color_util

from . import BleBoxConfigEntry
from .const import LIGHT_MAX_KELVINS, LIGHT_MIN_KELVINS
from .entity import BleBoxEntity

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -59,8 +60,8 @@ async def async_setup_entry(
class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
"""Representation of BleBox lights."""

_attr_min_color_temp_kelvin = 2700 # 370 Mireds
_attr_max_color_temp_kelvin = 6500 # 154 Mireds
_attr_min_color_temp_kelvin = LIGHT_MIN_KELVINS
_attr_max_color_temp_kelvin = LIGHT_MAX_KELVINS

def __init__(self, feature: blebox_uniapi.light.Light) -> None:
"""Initialize a BleBox light."""
Expand All @@ -78,10 +79,43 @@ def brightness(self) -> int | None:
"""Return the name."""
return self._feature.brightness

def _color_temp_to_native_scale(self, x: int) -> int:
"""Convert color temperature from Kelvin to native BleBox scale (0-255).

BleBox native scale is inverted relative to Kelvin: 0=warm (2700K), 255=cold (6500K).
"""
scaled = (
(self._attr_max_color_temp_kelvin - x)
/ (self._attr_max_color_temp_kelvin - self._attr_min_color_temp_kelvin)
) * 255
# note: within the operating temperature range here the Kelvin
# scale has less "integer steps" than the native scale used
# by blebox devices. Thus we need to use rounding method that is opposite
# to the one used in _color_temp_from_native_scale in order to avoid
# temperature value jumping by one step when the temperature value is read
# back from the device
bounded = max(min(math.floor(scaled), 255), 0)
return int(bounded)

def _color_temp_from_native_scale(self, x: int) -> int:
"""Convert color temperature from native BleBox scale (0-255) to Kelvin.

BleBox native scale is inverted relative to Kelvin: 0=warm (2700K), 255=cold (6500K).
"""
scaled = self._attr_max_color_temp_kelvin - (x / 255) * (
self._attr_max_color_temp_kelvin - self._attr_min_color_temp_kelvin
)
# note: see _color_temp_to_native_scale for explanation of rounding method
bounded = max(
min(math.ceil(scaled), self._attr_max_color_temp_kelvin),
self._attr_min_color_temp_kelvin,
)
return int(bounded)

@property
def color_temp_kelvin(self) -> int:
"""Return the color temperature value in Kelvin."""
return color_util.color_temperature_mired_to_kelvin(self._feature.color_temp)
return self._color_temp_from_native_scale(self._feature.color_temp)

@property
def color_mode(self) -> ColorMode:
Expand Down Expand Up @@ -139,15 +173,16 @@ async def async_turn_on(self, **kwargs: Any) -> None:
effect = kwargs.get(ATTR_EFFECT)
color_temp_kelvin = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
rgbww = kwargs.get(ATTR_RGBWW_COLOR)
rgb = kwargs.get(ATTR_RGB_COLOR)

feature = self._feature
value = feature.sensible_on_value
rgb = kwargs.get(ATTR_RGB_COLOR)

if rgbw is not None:
value = list(rgbw)
if color_temp_kelvin is not None:
value = feature.return_color_temp_with_brightness(
int(color_util.color_temperature_kelvin_to_mired(color_temp_kelvin)),
self._color_temp_to_native_scale(color_temp_kelvin),
self.brightness,
)

Expand All @@ -162,14 +197,16 @@ async def async_turn_on(self, **kwargs: Any) -> None:
if brightness is not None:
if self.color_mode == ColorMode.COLOR_TEMP:
value = feature.return_color_temp_with_brightness(
color_util.color_temperature_kelvin_to_mired(
self.color_temp_kelvin
),
self._color_temp_to_native_scale(self.color_temp_kelvin),
brightness,
)
else:
value = feature.apply_brightness(value, brightness)

if isinstance(value, (list, tuple)) and not any(value):
await self._feature.async_off()
return

try:
await self._feature.async_on(value)
except ValueError as exc:
Expand Down
12 changes: 12 additions & 0 deletions homeassistant/components/caldav/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ async def async_get_events(
end=self.to_local(self.get_end_date(vevent)),
location=get_attr_value(vevent, "location"),
description=get_attr_value(vevent, "description"),
uid=get_attr_value(vevent, "uid"),
recurrence_id=(
str(v)
if (v := get_attr_value(vevent, "recurrence_id")) is not None
else None
),
)
)

Expand Down Expand Up @@ -176,6 +182,12 @@ async def _async_update_data(self) -> CalendarEvent | None:
end=self.to_local(self.get_end_date(vevent)),
location=get_attr_value(vevent, "location"),
description=get_attr_value(vevent, "description"),
uid=get_attr_value(vevent, "uid"),
recurrence_id=(
str(v)
if (v := get_attr_value(vevent, "recurrence_id")) is not None
else None
),
)

@staticmethod
Expand Down
5 changes: 4 additions & 1 deletion homeassistant/components/control4/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,10 @@ def current_humidity(self) -> int | None:
if data is None:
return None
humidity = data.get(CONTROL4_HUMIDITY)
return int(humidity) if humidity is not None else None
try:
return int(humidity) if humidity is not None else None
except ValueError, TypeError:
return None

@property
def hvac_mode(self) -> HVACMode:
Expand Down
12 changes: 10 additions & 2 deletions homeassistant/components/daikin/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import logging

from pydaikin.daikin_base import Appliance
from pydaikin.exceptions import DaikinException

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN, TIMEOUT_SEC

Expand All @@ -33,4 +34,11 @@ def __init__(
self.device = device

async def _async_update_data(self) -> None:
await self.device.update_status()
try:
await self.device.update_status()
except DaikinException as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="error_communicating",
translation_placeholders={"error": str(err)},
) from err
3 changes: 3 additions & 0 deletions homeassistant/components/daikin/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
}
},
"exceptions": {
"error_communicating": {
"message": "Error communicating with Daikin device: {error}"
},
"zone_hvac_mode_unsupported": {
"message": "Zone temperature can only be changed when the main climate mode is heat or cool."
},
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/dnsip/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["aiodns==4.0.0"]
"requirements": ["aiodns==4.0.3"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/emulated_hue/upnp.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ async def async_create_upnp_datagram_endpoint(

ssdp_socket.bind(("" if upnp_bind_multicast else host_ip_addr, BROADCAST_PORT))

loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()

transport_protocol = await loop.create_datagram_endpoint(
lambda: UPNPResponderProtocol(loop, ssdp_socket, advertise_ip, advertise_port),
Expand Down
6 changes: 4 additions & 2 deletions homeassistant/components/forked_daapd/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,9 +470,11 @@ def is_volume_muted(self):
return self._player["volume"] == 0

@property
def media_content_id(self):
def media_content_id(self) -> str | None:
"""Content ID of current playing media."""
return self._player["item_id"]
if (item_id := self._player["item_id"]) == 0:
return None
return str(item_id)

@property
def media_content_type(self):
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/goodwe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoodweConfigEntry) -> bo
host=host,
port=port,
family=model_family,
retries=10,
retries=3,
)
except InverterError as err:
try:
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/goodwe/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ async def async_detect_inverter_port(
"""Detects the port of the Inverter."""
port = GOODWE_UDP_PORT
try:
inverter = await connect(host=host, port=port, retries=10)
inverter = await connect(host=host, port=port, retries=3)
except InverterError:
port = GOODWE_TCP_PORT
inverter = await connect(host=host, port=port, retries=10)
inverter = await connect(host=host, port=port, retries=3)
return inverter, port
2 changes: 1 addition & 1 deletion homeassistant/components/google_assistant/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async def sync_google(_):
"""Sync entities to Google."""
await self.async_sync_entities_all()

self._on_deinitialize.append(start.async_at_start(self.hass, sync_google))
self._on_deinitialize.append(start.async_at_started(self.hass, sync_google))

@callback
def async_deinitialize(self) -> None:
Expand Down
17 changes: 9 additions & 8 deletions homeassistant/components/growatt_server/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,15 @@ def _sync_update_data(self) -> dict[str, Any]:
mix_chart_entries = mix_detail["chartData"]
sorted_keys = sorted(mix_chart_entries)

# Create datetime from the latest entry
date_now = dt_util.now().date()
last_updated_time = dt_util.parse_time(str(sorted_keys[-1]))
mix_detail["lastdataupdate"] = datetime.datetime.combine(
date_now,
last_updated_time, # type: ignore[arg-type]
dt_util.get_default_time_zone(),
)
if sorted_keys:
# Create datetime from the latest entry
date_now = dt_util.now().date()
last_updated_time = dt_util.parse_time(str(sorted_keys[-1]))
mix_detail["lastdataupdate"] = datetime.datetime.combine(
date_now,
last_updated_time, # type: ignore[arg-type]
dt_util.get_default_time_zone(),
)

# Dashboard data for mix system
dashboard_data = self.api.dashboard_data(self.plant_id)
Expand Down
6 changes: 3 additions & 3 deletions homeassistant/components/homeassistant/triggers/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,9 @@ def update_entity_trigger(
# entity
update_entity_trigger(at_time, new_state=hass.states.get(at_time))
to_track.append(TrackEntity(at_time, update_entity_trigger_event))
elif isinstance(at_time, dict) and CONF_OFFSET in at_time:
# entity with offset
entity_id: str = at_time.get(CONF_ENTITY_ID, "")
elif isinstance(at_time, dict):
# entity with optional offset
entity_id: str = at_time[CONF_ENTITY_ID]
offset: timedelta = at_time.get(CONF_OFFSET, timedelta(0))
update_entity_trigger(
entity_id, new_state=hass.states.get(entity_id), offset=offset
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/iaqualink/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import httpx
from iaqualink.exception import (
AqualinkServiceException,
AqualinkServiceThrottledException,
AqualinkServiceUnauthorizedException,
)

Expand Down Expand Up @@ -46,6 +47,12 @@ async def _async_update_data(self) -> None:
await self.system.update()
except AqualinkServiceUnauthorizedException as err:
raise ConfigEntryAuthFailed("Invalid credentials for iAquaLink") from err
except AqualinkServiceThrottledException:
_LOGGER.warning(
"Rate limited by iAquaLink system %s, will retry later",
self.system.serial,
)
return
except (AqualinkServiceException, httpx.HTTPError) as err:
raise UpdateFailed(
f"Unable to update iAquaLink system {self.system.serial}: {err}"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/immich/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"iot_class": "local_polling",
"loggers": ["aioimmich"],
"quality_scale": "platinum",
"requirements": ["aioimmich==0.14.0"]
"requirements": ["aioimmich==0.14.1"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/intesishome/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"iot_class": "cloud_push",
"loggers": ["pyintesishome"],
"quality_scale": "legacy",
"requirements": ["pyintesishome==1.8.7"]
"requirements": ["pyintesishome==1.8.8"]
}
15 changes: 9 additions & 6 deletions homeassistant/components/manual/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,12 +455,15 @@ async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
if state := await self.async_get_last_state():
self._state_ts = state.last_updated
if next_state := state.attributes.get(ATTR_NEXT_STATE):
# If in arming or pending state we record the transition,
# not the current state
self._state = AlarmControlPanelState(next_state)
else:
self._state = AlarmControlPanelState(state.state)
try:
if next_state := state.attributes.get(ATTR_NEXT_STATE):
# If in arming or pending state we record the transition,
# not the current state
self._state = AlarmControlPanelState(next_state)
else:
self._state = AlarmControlPanelState(state.state)
except ValueError:
return

if prev_state := state.attributes.get(ATTR_PREVIOUS_STATE):
self._previous_state = prev_state
Expand Down
Loading
Loading