Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,9 @@ jobs:
sed -i "/uv/d" requirements.txt
sed -i "/uv/d" requirements_diff.txt

# home-assistant/wheels doesn't support sha pinning
- name: Build wheels
uses: home-assistant/wheels@bf4ddde339dde61ba98ccb4330517936bed6d2f8 # 2025.07.0
uses: home-assistant/[email protected]
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
Expand Down Expand Up @@ -218,8 +219,9 @@ jobs:
sed -i "/uv/d" requirements.txt
sed -i "/uv/d" requirements_diff.txt

# home-assistant/wheels doesn't support sha pinning
- name: Build wheels
uses: home-assistant/wheels@bf4ddde339dde61ba98ccb4330517936bed6d2f8 # 2025.07.0
uses: home-assistant/[email protected]
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/apcupsd/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@
"upsmode": SensorEntityDescription(
key="upsmode",
translation_key="ups_mode",
entity_category=EntityCategory.DIAGNOSTIC,
),
"upsname": SensorEntityDescription(
key="upsname",
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/bluetooth/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
"bleak==1.0.1",
"bleak-retry-connector==4.4.3",
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.2",
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.28.2",
"dbus-fast==2.44.3",
"habluetooth==5.6.2"
"habluetooth==5.6.4"
]
}
55 changes: 34 additions & 21 deletions homeassistant/components/homekit_controller/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@

RETRY_INTERVAL = 60 # seconds
MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3

# HomeKit accessories have varying limits on how many characteristics
# they can handle per request. Since we don't know each device's specific limit,
# we batch requests to a conservative size to avoid overwhelming any device.
MAX_CHARACTERISTICS_PER_REQUEST = 49

BLE_AVAILABILITY_CHECK_INTERVAL = 1800 # seconds

Expand Down Expand Up @@ -326,16 +329,20 @@ async def async_setup(self) -> None:
)
entry.async_on_unload(self._async_cancel_subscription_timer)

if transport != Transport.BLE:
# Although async_populate_accessories_state fetched the accessory database,
# the /accessories endpoint may return cached values from the accessory's
# perspective. For example, Ecobee thermostats may report stale temperature
# values (like 100°C) in their /accessories response after restarting.
# We need to explicitly poll characteristics to get fresh sensor readings
# before processing the entity map and creating devices.
# Use poll_all=True since entities haven't registered their characteristics yet.
await self.async_update(poll_all=True)

await self.async_process_entity_map()

if transport != Transport.BLE:
# When Home Assistant starts, we restore the accessory map from storage
# which contains characteristic values from when HA was last running.
# These values are stale and may be incorrect (e.g., Ecobee thermostats
# report 100°C when restarting). We need to poll for fresh values before
# creating entities. Use poll_all=True since entities haven't registered
# their characteristics yet.
await self.async_update(poll_all=True)
# Start regular polling after entity map is processed
self._async_start_polling()

# If everything is up to date, we can create the entities
Expand Down Expand Up @@ -938,20 +945,26 @@ async def async_update(
async with self._polling_lock:
_LOGGER.debug("Starting HomeKit device update: %s", self.unique_id)

try:
new_values_dict = await self.get_characteristics(to_poll)
except AccessoryNotFoundError:
# Not only did the connection fail, but also the accessory is not
# visible on the network.
self.async_set_available_state(False)
return
except (AccessoryDisconnectedError, EncryptionError):
# Temporary connection failure. Device may still available but our
# connection was dropped or we are reconnecting
self._poll_failures += 1
if self._poll_failures >= MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE:
new_values_dict: dict[tuple[int, int], dict[str, Any]] = {}
to_poll_list = list(to_poll)

for i in range(0, len(to_poll_list), MAX_CHARACTERISTICS_PER_REQUEST):
batch = to_poll_list[i : i + MAX_CHARACTERISTICS_PER_REQUEST]
try:
batch_values = await self.get_characteristics(batch)
new_values_dict.update(batch_values)
except AccessoryNotFoundError:
# Not only did the connection fail, but also the accessory is not
# visible on the network.
self.async_set_available_state(False)
return
return
except (AccessoryDisconnectedError, EncryptionError):
# Temporary connection failure. Device may still available but our
# connection was dropped or we are reconnecting
self._poll_failures += 1
if self._poll_failures >= MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE:
self.async_set_available_state(False)
return

self._poll_failures = 0
self.process_new_events(new_values_dict)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/homekit_controller/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==3.2.15"],
"requirements": ["aiohomekit==3.2.16"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
}
9 changes: 4 additions & 5 deletions homeassistant/components/media_source/local_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,13 @@ async def async_upload_media(

def _do_move() -> None:
"""Move file to target."""
if not target_dir.is_dir():
raise PathNotSupportedError("Target is not an existing directory")

target_path = target_dir / uploaded_file.filename

try:
target_path = target_dir / uploaded_file.filename

target_path.relative_to(target_dir)
raise_if_invalid_path(str(target_path))

target_dir.mkdir(parents=True, exist_ok=True)
except ValueError as err:
raise PathNotSupportedError("Invalid path") from err

Expand Down
11 changes: 3 additions & 8 deletions homeassistant/components/tts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from time import monotonic
from typing import Any, Final, Generic, Protocol, TypeVar

import aiofiles
from aiohttp import web
import mutagen
from mutagen.id3 import ID3, TextFrame as ID3Text
Expand Down Expand Up @@ -591,13 +590,9 @@ async def _async_stream_override_result(self) -> AsyncGenerator[bytes]:

if not needs_conversion:
# Read file directly (no conversion)
async with aiofiles.open(self._override_media_path, "rb") as media_file:
while True:
chunk = await media_file.read(FFMPEG_CHUNK_SIZE)
if not chunk:
break
yield chunk

yield await self.hass.async_add_executor_job(
self._override_media_path.read_bytes
)
return

# Use ffmpeg to convert audio to preferred format
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/twitch/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ async def _async_setup(self) -> None:
if not (user := await first(self.twitch.get_users())):
raise UpdateFailed("Logged in user not found")
self.current_user = user
self.users.append(self.current_user) # Add current_user to users list.

async def _async_update_data(self) -> dict[str, TwitchUpdate]:
await self.session.async_ensure_token_valid()
Expand All @@ -95,6 +96,8 @@ async def _async_update_data(self) -> dict[str, TwitchUpdate]:
user_id=self.current_user.id, first=100
)
}
async for s in self.twitch.get_streams(user_id=[self.current_user.id]):
streams.update({s.user_id: s})
follows: dict[str, FollowedChannel] = {
f.broadcaster_id: f
async for f in await self.twitch.get_followed_channels(
Expand Down
25 changes: 18 additions & 7 deletions homeassistant/components/verisure/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,13 @@ async def _async_set_arm_state(
)
LOGGER.debug("Verisure set arm state %s", state)
result = None
attempts = 0
while result is None:
await asyncio.sleep(0.5)
if attempts == 30:
break
if attempts > 1:
await asyncio.sleep(0.5)
attempts += 1
transaction = await self.hass.async_add_executor_job(
self.coordinator.verisure.request,
self.coordinator.verisure.poll_arm_state(
Expand All @@ -81,8 +86,10 @@ async def _async_set_arm_state(
.get("armStateChangePollResult", {})
.get("result")
)

await self.coordinator.async_refresh()
LOGGER.debug("Result is %s", result)
if result == "OK":
self._attr_alarm_state = ALARM_STATE_TO_HA.get(state)
self.async_write_ha_state()

async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
Expand All @@ -108,16 +115,20 @@ async def async_alarm_arm_away(self, code: str | None = None) -> None:
"ARMED_AWAY", self.coordinator.verisure.arm_away(code)
)

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
def _update_alarm_attributes(self) -> None:
"""Update alarm state and changed by from coordinator data."""
self._attr_alarm_state = ALARM_STATE_TO_HA.get(
self.coordinator.data["alarm"]["statusType"]
)
self._attr_changed_by = self.coordinator.data["alarm"].get("name")

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_alarm_attributes()
super()._handle_coordinator_update()

async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self._handle_coordinator_update()
self._update_alarm_attributes()
55 changes: 28 additions & 27 deletions homeassistant/components/verisure/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from homeassistant.components.lock import LockEntity, LockState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_CODE
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
Expand Down Expand Up @@ -70,7 +70,9 @@ def __init__(
self._attr_unique_id = serial_number

self.serial_number = serial_number
self._state: str | None = None
self._attr_is_locked = None
self._attr_changed_by = None
self._changed_method: str | None = None

@property
def device_info(self) -> DeviceInfo:
Expand All @@ -92,20 +94,6 @@ def available(self) -> bool:
super().available and self.serial_number in self.coordinator.data["locks"]
)

@property
def changed_by(self) -> str | None:
"""Last change triggered by."""
return (
self.coordinator.data["locks"][self.serial_number]
.get("user", {})
.get("name")
)

@property
def changed_method(self) -> str:
"""Last change method."""
return self.coordinator.data["locks"][self.serial_number]["lockMethod"]

@property
def code_format(self) -> str:
"""Return the configured code format."""
Expand All @@ -115,16 +103,9 @@ def code_format(self) -> str:
return f"^\\d{{{digits}}}$"

@property
def is_locked(self) -> bool:
"""Return true if lock is locked."""
return (
self.coordinator.data["locks"][self.serial_number]["lockStatus"] == "LOCKED"
)

@property
def extra_state_attributes(self) -> dict[str, str]:
def extra_state_attributes(self) -> dict[str, str | None]:
"""Return the state attributes."""
return {"method": self.changed_method}
return {"method": self._changed_method}

async def async_unlock(self, **kwargs: Any) -> None:
"""Send unlock command."""
Expand Down Expand Up @@ -154,7 +135,7 @@ async def async_set_lock_state(self, code: str, state: LockState) -> None:
target_state = "LOCKED" if state == LockState.LOCKED else "UNLOCKED"
lock_status = None
attempts = 0
while lock_status != "OK":
while lock_status is None:
if attempts == 30:
break
if attempts > 1:
Expand All @@ -172,8 +153,10 @@ async def async_set_lock_state(self, code: str, state: LockState) -> None:
.get("doorLockStateChangePollResult", {})
.get("result")
)
LOGGER.debug("Lock status is %s", lock_status)
if lock_status == "OK":
self._state = state
self._attr_is_locked = state == LockState.LOCKED
self.async_write_ha_state()

def disable_autolock(self) -> None:
"""Disable autolock on a doorlock."""
Expand All @@ -196,3 +179,21 @@ def enable_autolock(self) -> None:
LOGGER.debug("Enabling autolock on %s", self.serial_number)
except VerisureError as ex:
LOGGER.error("Could not enable autolock, %s", ex)

def _update_lock_attributes(self) -> None:
"""Update lock state, changed by, and method from coordinator data."""
lock_data = self.coordinator.data["locks"][self.serial_number]
self._attr_is_locked = lock_data["lockStatus"] == "LOCKED"
self._attr_changed_by = lock_data.get("user", {}).get("name")
self._changed_method = lock_data["lockMethod"]

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_lock_attributes()
super()._handle_coordinator_update()

async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self._update_lock_attributes()
2 changes: 1 addition & 1 deletion homeassistant/components/verisure/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,4 @@ async def async_set_plug_state(self, state: bool) -> None:
)
self._state = state
self._change_timestamp = monotonic()
await self.coordinator.async_request_refresh()
self.async_write_ha_state()
2 changes: 1 addition & 1 deletion homeassistant/components/waterfurnace/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["waterfurnace"],
"quality_scale": "legacy",
"requirements": ["waterfurnace==1.1.0"]
"requirements": ["waterfurnace==1.2.0"]
}
Loading
Loading