Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3d2ec71
Raise exceptions for Telegram bot when actions fail (#148140)
hanwg Oct 15, 2025
751540e
Component asuswrt: Update SENSORS_DEFAULT in tests (#154547)
kennedyshead Oct 15, 2025
d0cad43
Recalculate derivative unit correctly when source or options change (…
karwosts Oct 15, 2025
ca91290
Automatically removing stale devices in Homee (#152680)
Taraman17 Oct 15, 2025
1012c7b
Ensure psn wait more than coordinator tick (#154549)
elupus Oct 15, 2025
cd7015c
Add integration type `device` to IronOS manifest (#154533)
tr4nt0r Oct 15, 2025
fbd8443
Simplify onewire entity descriptions (#154513)
epenet Oct 15, 2025
a3b0132
Move template-rendering test helpers to separate module (#154366)
akx Oct 15, 2025
57dccd1
Remove zha from _IGNORE_ROOT_IMPORT in pylint plugin (#154534)
epenet Oct 15, 2025
148a133
Firefly refactor entities (#153292)
erwindouna Oct 15, 2025
7829c2d
Align Shelly entity names with device classes (#154492)
bieniu Oct 15, 2025
7abe289
Add support for Python 3.14 (#153939)
cdce8p Oct 15, 2025
645089e
Bump aio-ownet to 0.0.4 (#154520)
epenet Oct 15, 2025
1e59102
Update pylint to 4.0.1 (#154526)
cdce8p Oct 15, 2025
051e472
Import device_tracker classes from component root (#154524)
epenet Oct 15, 2025
e2c2815
Expose the entity_id of an entity to LLMs (#149428)
jbkkd Oct 15, 2025
ec5c484
Fix typing issue in fritz (#154497)
cdce8p Oct 15, 2025
3c001bd
Revert "Expose the entity_id of an entity to LLMs" (#154561)
balloob Oct 15, 2025
88d3839
Fix lingering todoist test by fixing its test time (#154511)
jbouwh Oct 15, 2025
01effb7
Remove hardware from _IGNORE_ROOT_IMPORT in pylint plugin (#154532)
epenet Oct 15, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ env:
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.11"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
ALL_PYTHON_VERSIONS: "['3.13', '3.14']"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support
Expand Down
73 changes: 47 additions & 26 deletions homeassistant/components/apple_tv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,9 @@
import asyncio
import logging
from random import randrange
import sys
from typing import Any, cast

from pyatv import connect, exceptions, scan
from pyatv.conf import AppleTV
from pyatv.const import DeviceModel, Protocol
from pyatv.convert import model_str
from pyatv.interface import AppleTV as AppleTVInterface, DeviceListener

from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
Expand All @@ -29,7 +24,11 @@
Platform,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
Expand All @@ -43,6 +42,18 @@
SIGNAL_DISCONNECTED,
)

if sys.version_info < (3, 14):
from pyatv import connect, exceptions, scan
from pyatv.conf import AppleTV
from pyatv.const import DeviceModel, Protocol
from pyatv.convert import model_str
from pyatv.interface import AppleTV as AppleTVInterface, DeviceListener
else:

class DeviceListener:
"""Dummy class."""


_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME_TV = "Apple TV"
Expand All @@ -53,31 +64,41 @@

PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE]

AUTH_EXCEPTIONS = (
exceptions.AuthenticationError,
exceptions.InvalidCredentialsError,
exceptions.NoCredentialsError,
)
CONNECTION_TIMEOUT_EXCEPTIONS = (
OSError,
asyncio.CancelledError,
TimeoutError,
exceptions.ConnectionLostError,
exceptions.ConnectionFailedError,
)
DEVICE_EXCEPTIONS = (
exceptions.ProtocolError,
exceptions.NoServiceError,
exceptions.PairingError,
exceptions.BackOffError,
exceptions.DeviceIdMissingError,
)
if sys.version_info < (3, 14):
AUTH_EXCEPTIONS = (
exceptions.AuthenticationError,
exceptions.InvalidCredentialsError,
exceptions.NoCredentialsError,
)
CONNECTION_TIMEOUT_EXCEPTIONS = (
OSError,
asyncio.CancelledError,
TimeoutError,
exceptions.ConnectionLostError,
exceptions.ConnectionFailedError,
)
DEVICE_EXCEPTIONS = (
exceptions.ProtocolError,
exceptions.NoServiceError,
exceptions.PairingError,
exceptions.BackOffError,
exceptions.DeviceIdMissingError,
)
else:
AUTH_EXCEPTIONS = ()
CONNECTION_TIMEOUT_EXCEPTIONS = ()
DEVICE_EXCEPTIONS = ()


type AppleTvConfigEntry = ConfigEntry[AppleTVManager]


async def async_setup_entry(hass: HomeAssistant, entry: AppleTvConfigEntry) -> bool:
"""Set up a config entry for Apple TV."""
if sys.version_info >= (3, 14):
raise HomeAssistantError(
"Apple TV is not supported on Python 3.14. Please use Python 3.13."
)
manager = AppleTVManager(hass, entry)

if manager.is_on:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/apple_tv/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.16.1"],
"requirements": ["pyatv==0.16.1;python_version<'3.14'"],
"zeroconf": [
"_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.",
Expand Down
36 changes: 29 additions & 7 deletions homeassistant/components/derivative/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def __init__(

self._attr_name = name if name is not None else f"{source_entity} derivative"
self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity}

self._unit_template: str | None = None
if unit_of_measurement is None:
final_unit_prefix = "" if unit_prefix is None else unit_prefix
self._unit_template = f"{final_unit_prefix}{{}}/{unit_time}"
Expand All @@ -217,6 +217,23 @@ def __init__(
lambda *args: None
)

def _derive_and_set_attributes_from_state(self, source_state: State | None) -> None:
if self._unit_template and source_state:
original_unit = self._attr_native_unit_of_measurement
source_unit = source_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
self._attr_native_unit_of_measurement = self._unit_template.format(
"" if source_unit is None else source_unit
)
if original_unit != self._attr_native_unit_of_measurement:
_LOGGER.debug(
"%s: Derivative sensor switched UoM from %s to %s, resetting state to 0",
self.entity_id,
original_unit,
self._attr_native_unit_of_measurement,
)
self._state_list = []
self._attr_native_value = round(Decimal(0), self._round_digits)

def _calc_derivative_from_state_list(self, current_time: datetime) -> Decimal:
def calculate_weight(start: datetime, end: datetime, now: datetime) -> float:
window_start = now - timedelta(seconds=self._time_window)
Expand Down Expand Up @@ -285,6 +302,9 @@ async def async_added_to_hass(self) -> None:
except (InvalidOperation, TypeError):
self._attr_native_value = None

source_state = self.hass.states.get(self._sensor_source_id)
self._derive_and_set_attributes_from_state(source_state)

def schedule_max_sub_interval_exceeded(source_state: State | None) -> None:
"""Schedule calculation using the source state and max_sub_interval.

Expand Down Expand Up @@ -358,10 +378,18 @@ def on_state_changed(event: Event[EventStateChangedData]) -> None:
_LOGGER.debug("%s: New state changed event: %s", self.entity_id, event.data)
self._cancel_max_sub_interval_exceeded_callback()
new_state = event.data["new_state"]

if not self._handle_invalid_source_state(new_state):
return

assert new_state

original_unit = self._attr_native_unit_of_measurement
self._derive_and_set_attributes_from_state(new_state)
if original_unit != self._attr_native_unit_of_measurement:
self.async_write_ha_state()
return

schedule_max_sub_interval_exceeded(new_state)
old_state = event.data["old_state"]
if old_state is not None:
Expand Down Expand Up @@ -391,12 +419,6 @@ def calc_derivative(
self.async_write_ha_state()
return

if self.native_unit_of_measurement is None:
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
self._attr_native_unit_of_measurement = self._unit_template.format(
"" if unit is None else unit
)

self._prune_state_list(new_timestamp)

try:
Expand Down
63 changes: 54 additions & 9 deletions homeassistant/components/firefly_iii/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

from __future__ import annotations

from pyfirefly.models import Account, Category
from yarl import URL

from homeassistant.const import CONF_URL
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN, MANUFACTURER
from .const import DOMAIN, MANUFACTURER, NAME
from .coordinator import FireflyDataUpdateCoordinator


Expand All @@ -21,20 +21,65 @@ class FireflyBaseEntity(CoordinatorEntity[FireflyDataUpdateCoordinator]):
def __init__(
self,
coordinator: FireflyDataUpdateCoordinator,
entity_description: EntityDescription,
) -> None:
"""Initialize a Firefly entity."""
super().__init__(coordinator)

self.entity_description = entity_description
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
name=NAME,
configuration_url=URL(coordinator.config_entry.data[CONF_URL]),
identifiers={(DOMAIN, f"{coordinator.config_entry.entry_id}_service")},
)


class FireflyAccountBaseEntity(FireflyBaseEntity):
"""Base class for Firefly III account entity."""

def __init__(
self,
coordinator: FireflyDataUpdateCoordinator,
account: Account,
key: str,
) -> None:
"""Initialize a Firefly account entity."""
super().__init__(coordinator)
self._account = account
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
name=account.attributes.name,
configuration_url=f"{URL(coordinator.config_entry.data[CONF_URL])}/accounts/show/{account.id}",
identifiers={
(
DOMAIN,
f"{coordinator.config_entry.entry_id}_{self.entity_description.key}",
)
(DOMAIN, f"{coordinator.config_entry.entry_id}_account_{account.id}")
},
)
self._attr_unique_id = (
f"{coordinator.config_entry.unique_id}_account_{account.id}_{key}"
)


class FireflyCategoryBaseEntity(FireflyBaseEntity):
"""Base class for Firefly III category entity."""

def __init__(
self,
coordinator: FireflyDataUpdateCoordinator,
category: Category,
key: str,
) -> None:
"""Initialize a Firefly category entity."""
super().__init__(coordinator)
self._category = category
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
name=category.attributes.name,
configuration_url=f"{URL(coordinator.config_entry.data[CONF_URL])}/categories/show/{category.id}",
identifiers={
(DOMAIN, f"{coordinator.config_entry.entry_id}_category_{category.id}")
},
)
self._attr_unique_id = (
f"{coordinator.config_entry.unique_id}_category_{category.id}_{key}"
)
14 changes: 7 additions & 7 deletions homeassistant/components/firefly_iii/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
"entity": {
"sensor": {
"account_type": {
"default": "mdi:bank",
"state": {
"expense": "mdi:cash-minus",
"revenue": "mdi:cash-plus",
"asset": "mdi:account-cash",
"liability": "mdi:hand-coin"
}
"default": "mdi:bank"
},
"account_balance": {
"default": "mdi:currency-usd"
},
"account_role": {
"default": "mdi:account-circle"
},
"category": {
"default": "mdi:label"
Expand Down
Loading
Loading