Compare commits

..

31 Commits

Author SHA1 Message Date
Joostlek 10c82955cc Bump google-cloud-pubsub 2026-05-28 13:26:00 +02:00
Joostlek 96188e6d9b Merge branch 'dev' into protobuf_7341
# Conflicts:
#	requirements_test_all.txt
2026-05-28 13:21:11 +02:00
Manu 37f41d8e09 Fix index error in DuckDNS integration (#172392) 2026-05-28 12:58:51 +02:00
Crocmagnon b02f312bed ovhcloud_ai_endpoints: reauthentication flow (#172405) 2026-05-28 12:58:39 +02:00
Nikhil Deepak 3520c821c5 Reset MQTT valve opening/closing state at intermediate positions (#165176)
Co-authored-by: jbouwh <jan@jbsoft.nl>
2026-05-28 12:07:30 +02:00
Jan Bouwhuis cbf737a03e Improve MQTT protocol deprecation repair message (#172404)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-28 12:05:35 +02:00
Franck Nijhof 5bd6d52e6a Convert yamaha_musiccast sw_version to string (#172411) 2026-05-28 12:05:19 +02:00
Linkplay2020 d9a89beb3d Bump wiim to 1.0.4 (#172334)
Co-authored-by: Tao Jiang <tao.jiang@linkplay.com>
2026-05-28 11:38:22 +02:00
Ludovic BOUÉ 41f783f14d Add Matter soil moisture sensor (#172372) 2026-05-28 11:03:58 +02:00
Erik Montnemery 35397b818d Deprecate device tracker battery_level property (#171819)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-28 10:54:08 +02:00
Erik Montnemery d42d02f20a Revert "Add zone triggers entered/left zone" (#172409) 2026-05-28 10:32:28 +02:00
Franck Nijhof 99c445f261 Bump version to 2026.7.0dev0 (#172367) 2026-05-28 10:20:00 +02:00
Stefan Agner 567fe85828 Reject backup uploads with unsafe inner name (#172368)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:19:06 +02:00
Erik Montnemery fd1a5d0c5a Add zone triggers entered/left zone (#171751) 2026-05-28 10:05:41 +02:00
Erik Montnemery 632ec39d53 Deprecate device tracker TrackerEntity location_name property (#171820) 2026-05-28 10:02:28 +02:00
Abílio Costa 67b9d28953 Fix OMIE sensors not updating on setup (#172383) 2026-05-28 08:29:53 +02:00
J. Nick Koston e3880eedb0 Bump yalexs to 9.2.1 (#172389) 2026-05-27 22:01:07 -05:00
J. Nick Koston ce64f5f902 Bump onvif-zeep-async to 4.1.1 (#172391) 2026-05-27 22:00:56 -05:00
J. Nick Koston 0da99a50fc Bump dbus-fast to 5.0.16 (#172378) 2026-05-27 17:16:36 -05:00
Arcadiy Ivanov 43f636be65 Include device identity in Matter light transition blocklist warning (#172324) 2026-05-27 23:58:37 +02:00
Simone Chemelli 262cdbfab5 Bump aioamazondevices to 13.8.1 (#172382) 2026-05-27 23:16:23 +02:00
puddly 8cbd358435 Bump ZHA to 1.4.0 (#172357) 2026-05-27 22:55:07 +02:00
torben-iometer df04b19a0a bump iometer version to 1.0.1 (#172338) 2026-05-27 22:19:20 +02:00
markvp adeb352079 Add GeneralDiagnostics sensors and fault binary sensors to Matter integration (#169830) 2026-05-27 21:07:08 +02:00
Stefan Agner 1e457600f1 Harden backup tar extraction with Python tar_filter (#172252) 2026-05-27 18:10:04 +02:00
J. Nick Koston 30f8f0517f another 2026-04-22 12:58:51 +02:00
J. Nick Koston 3f31be37f5 another 2026-04-22 12:56:06 +02:00
J. Nick Koston cf0a14f92b another 2026-04-22 12:51:13 +02:00
J. Nick Koston 2fcbd50784 make the bot happy 2026-04-22 12:31:22 +02:00
J. Nick Koston c08743f907 bump 2026-04-22 12:29:10 +02:00
J. Nick Koston a67ea6d4f7 Bump protobuf to 7.34.1
changelog: http://github.com/protocolbuffers/protobuf/compare/v32.0...v34.1
2026-04-22 12:25:10 +02:00
505 changed files with 14784 additions and 9976 deletions
+4 -4
View File
@@ -39,7 +39,7 @@ on:
env:
CACHE_VERSION: 3
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.6"
HA_SHORT_VERSION: "2026.7"
ADDITIONAL_PYTHON_VERSIONS: "[]"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
@@ -1326,7 +1326,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
fail_ci_if_error: true
flags: full-suite
@@ -1485,7 +1485,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
@@ -1513,7 +1513,7 @@ jobs:
with:
pattern: test-results-*
- name: Upload test results to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
report_type: test_results
fail_ci_if_error: true
+2 -2
View File
@@ -137,7 +137,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: home-assistant/wheels@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -195,7 +195,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: home-assistant/wheels@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
Generated
+2 -2
View File
@@ -623,8 +623,8 @@ CLAUDE.md @home-assistant/core
/tests/components/generic_hygrostat/ @Shulyaka
/homeassistant/components/geniushub/ @manzanotti
/tests/components/geniushub/ @manzanotti
/homeassistant/components/gentex_homelink/ @Gentex-Corporation/Homelink @rjones-gentex
/tests/components/gentex_homelink/ @Gentex-Corporation/Homelink @rjones-gentex
/homeassistant/components/gentex_homelink/ @niaexa @ryanjones-gentex
/tests/components/gentex_homelink/ @niaexa @ryanjones-gentex
/homeassistant/components/geo_json_events/ @exxamalte
/tests/components/geo_json_events/ @exxamalte
/homeassistant/components/geo_location/ @home-assistant/core
+2 -4
View File
@@ -92,8 +92,7 @@ def _extract_backup(
):
ostf.tar.extractall(
path=Path(tempdir, "extracted"),
members=securetar.secure_path(ostf.tar),
filter="fully_trusted",
filter="tar",
)
backup_meta_file = Path(tempdir, "extracted", "backup.json")
backup_meta = json.loads(backup_meta_file.read_text(encoding="utf8"))
@@ -119,8 +118,7 @@ def _extract_backup(
) as istf:
istf.extractall(
path=Path(tempdir, "homeassistant"),
members=securetar.secure_path(istf),
filter="fully_trusted",
filter="tar",
)
if restore_content.restore_homeassistant:
keep = list(KEEP_BACKUPS)
-1
View File
@@ -6,7 +6,6 @@
"lg_netcast",
"lg_soundbar",
"lg_thinq",
"lg_tv_rs232",
"webostv"
]
}
@@ -13,5 +13,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["actron-neo-api==0.5.12"]
"requirements": ["actron-neo-api==0.5.6"]
}
+2 -1
View File
@@ -72,7 +72,8 @@ async def _resolve_attachments(
resolved_attachments.append(
conversation.Attachment(
media_content_id=media_content_id,
mime_type=image_data.content_type,
mime_type=attachment.get("media_content_type")
or image_data.content_type,
path=temp_filename,
)
)
@@ -8,7 +8,6 @@ from bleak.backends.device import BLEDevice
from bleak_retry_connector import close_stale_connections_by_address
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import BluetoothReachabilityIntent
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
@@ -64,16 +63,7 @@ class AirthingsBLEDataUpdateCoordinator(DataUpdateCoordinator[AirthingsDevice]):
if not ble_device:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={
"address": address,
"reason": bluetooth.async_address_reachability_diagnostics(
self.hass,
address.upper(),
BluetoothReachabilityIntent.CONNECTION,
),
},
f"Could not find Airthings device with address {address}"
)
self.ble_device = ble_device
@@ -54,10 +54,5 @@
"name": "Radon longterm level"
}
}
},
"exceptions": {
"device_not_found": {
"message": "Could not find Airthings device with address {address}: {reason}"
}
}
}
@@ -37,9 +37,6 @@
"title": "Re-authenticate AirVisual"
},
"user": {
"data": {
"type": "Integration type"
},
"description": "Pick what type of AirVisual data you want to monitor.",
"title": "Configure AirVisual"
}
@@ -1,5 +1,8 @@
"""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
@@ -43,17 +46,21 @@ 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,
)
await coordinator.api.start_http2_processing(
alexa_httpx_client,
on_reauth_required=_on_http2_reauth_required,
http2_task = await coordinator.api.start_http2_processing(
alexa_httpx_client, on_reauth_required=_on_http2_reauth_required
)
entry.async_on_unload(coordinator.api.stop_http2_processing)
entry.async_on_unload(_cancel_http2)
entry.runtime_data = coordinator
@@ -6,7 +6,7 @@ from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import slugify
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator, alexa_api_call
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .entity import AmazonServiceEntity
# Coordinator is used to centralize the data updates
@@ -39,8 +39,11 @@ 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,
@@ -49,5 +52,4 @@ class AmazonRoutineButton(AmazonServiceEntity, ButtonEntity):
async def async_press(self) -> None:
"""Handle button press action."""
async with alexa_api_call(self.coordinator):
await self.coordinator.api.call_routine(self._routine)
await self._coordinator.api.call_routine(self._routine)
@@ -1,7 +1,5 @@
"""Support for Alexa Devices."""
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from datetime import timedelta
from aioamazondevices.api import AmazonEchoApi
@@ -21,11 +19,7 @@ from aiohttp import ClientSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -35,65 +29,6 @@ from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
SCAN_INTERVAL = 300
@asynccontextmanager
async def alexa_api_call(
coordinator: DataUpdateCoordinator | None = None,
) -> AsyncGenerator[None]:
"""Handle common Alexa API exceptions as HomeAssistantError."""
try:
yield
except CannotAuthenticate as err:
if coordinator:
coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": repr(err)},
) from err
except CannotConnect as err:
if coordinator:
coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_connect_with_error",
translation_placeholders={"error": repr(err)},
) from err
except (CannotRetrieveData, ValueError) as err:
if coordinator:
coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(err)},
) from err
@asynccontextmanager
async def alexa_config_entry_errors() -> AsyncGenerator[None]:
"""Handle common Alexa API exceptions as ConfigEntry errors."""
try:
yield
except CannotAuthenticate as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": repr(err)},
) from err
except (CannotConnect, TimeoutError) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect_with_error",
translation_placeholders={"error": repr(err)},
) from err
except (CannotRetrieveData, ValueError, KeyError, StopIteration) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(err)},
) from err
type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
@@ -178,23 +113,14 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
translation_key="invalid_auth",
translation_placeholders={"error": repr(err)},
) from err
except ValueError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(err)},
) from err
else:
current_devices = set(data.keys())
if stale_devices := self.previous_devices - current_devices:
await self._async_remove_device_stale(stale_devices)
self.previous_devices = current_devices
current_routines = {
f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}"
for routine in self.api.routines
}
if stale_routines := (self.previous_routines - current_routines):
current_routines = {slugify(routine) for routine in self.api.routines}
if stale_routines := self.previous_routines - current_routines:
await self._async_remove_routine_stale(stale_routines)
self.previous_routines = current_routines
@@ -228,25 +154,41 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
"""Remove stale routine."""
entity_registry = er.async_get(self.hass)
for routine_unique_id in stale_routines:
for routine in stale_routines:
_LOGGER.debug(
"Detected change in routines: routine %s removed",
routine,
)
entity_id = entity_registry.async_get_entity_id(
Platform.BUTTON,
DOMAIN,
routine_unique_id,
f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}",
)
if entity_id:
_LOGGER.debug(
"Detected change in routines: routine %s removed",
routine_unique_id.replace(
f"{slugify(self.config_entry.unique_id)}-", ""
),
)
entity_registry.async_remove(entity_id)
async def sync_history_state(self) -> None:
"""Sync history state."""
async with alexa_config_entry_errors():
try:
self._vocal_records = await self.api.sync_history_state()
except CannotAuthenticate as e:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": repr(e)},
) from e
except CannotConnect as e:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect_with_error",
translation_placeholders={"error": repr(e)},
) from e
except BaseException as e:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(e)},
) from e
async def history_state_event_handler(
self, vocal_records: dict[str, AmazonVocalRecord]
@@ -262,8 +204,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
async def sync_media_state(self) -> None:
"""Sync media state."""
async with alexa_config_entry_errors():
await self.api.sync_media_state()
await self.api.sync_media_state()
async def media_state_event_handler(
self, media_state: dict[str, AmazonMediaState]
@@ -12,18 +12,7 @@ from homeassistant.helpers.device_registry import DeviceEntry
from .coordinator import AmazonConfigEntry
TO_REDACT = {
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
"access_token",
"adp_token",
"device_private_key",
"refresh_token",
"store_authentication_cookie",
"title",
"website_cookies",
}
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME, CONF_NAME, "title"}
async def async_get_config_entry_diagnostics(
@@ -2,20 +2,13 @@
from typing import Final
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
from homeassistant.components.event import (
DOMAIN as EVENT_DOMAIN,
EventEntity,
EventEntityDescription,
)
from homeassistant.components.event import EventEntity, EventEntityDescription
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import _LOGGER
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .entity import AmazonEntity
from .utils import async_remove_entity_from_virtual_group
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -38,11 +31,6 @@ async def async_setup_entry(
"""Set up Alexa Devices events based on a config entry."""
coordinator = entry.runtime_data
# Remove voice event from virtual groups
await async_remove_entity_from_virtual_group(
hass, coordinator, EVENT_DOMAIN, "voice_event"
)
known_devices: set[str] = set()
def _check_device() -> None:
@@ -54,7 +42,6 @@ async def async_setup_entry(
AlexaVoiceEvent(coordinator, serial_num, event_desc)
for event_desc in EVENTS
for serial_num in new_devices
if coordinator.data[serial_num].device_family != SPEAKER_GROUP_FAMILY
)
_check_device()
@@ -66,7 +53,7 @@ class AlexaVoiceEvent(AmazonEntity, EventEntity):
_attr_event_types = [EVENT_TYPE]
coordinator: AmazonDevicesCoordinator
_last_seen_timestamp: int = 0 # January 1, 1970 at 12:00:00 AM
_last_seen_timestamp: int | None = None
@callback
def _handle_coordinator_update(self) -> None:
@@ -84,8 +71,7 @@ class AlexaVoiceEvent(AmazonEntity, EventEntity):
)
return
if vocal_record.timestamp <= self._last_seen_timestamp:
# Discard old events that have already been processed
if vocal_record.timestamp == self._last_seen_timestamp:
return
self._last_seen_timestamp = vocal_record.timestamp
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==14.1.3"]
"requirements": ["aioamazondevices==13.8.1"]
}
@@ -1,7 +1,8 @@
"""Media player platform for Alexa Devices."""
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from typing import Any, Final
from aioamazondevices.structures import (
AmazonMediaControls,
@@ -22,8 +23,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import _LOGGER
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator, alexa_api_call
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .entity import AmazonEntity
from .utils import alexa_api_call
PARALLEL_UPDATES = 1
@@ -36,6 +38,18 @@ STANDARD_SUPPORTED_FEATURES = (
)
@dataclass(frozen=True, kw_only=True)
class AmazonDevicesMediaPlayerEntityDescription(MediaPlayerEntityDescription):
"""Describes an Alexa Devices media player entity."""
MEDIA_PLAYERS: Final = (
AmazonDevicesMediaPlayerEntityDescription(
key="media",
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AmazonConfigEntry,
@@ -55,10 +69,9 @@ async def async_setup_entry(
continue
known_devices.add(serial_num)
new_entities.append(
AlexaDevicesMediaPlayer(
coordinator, serial_num, MediaPlayerEntityDescription(key="media")
)
new_entities.extend(
AlexaDevicesMediaPlayer(coordinator, serial_num, description)
for description in MEDIA_PLAYERS
)
if new_entities:
@@ -72,6 +85,8 @@ async def async_setup_entry(
class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity):
"""Representation of an Alexa device media player."""
entity_description: AmazonDevicesMediaPlayerEntityDescription
_attr_name = None # Uses the device name
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
_attr_volume_step = 0.05
@@ -80,7 +95,7 @@ class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity):
self,
coordinator: AmazonDevicesCoordinator,
serial_num: str,
description: MediaPlayerEntityDescription,
description: AmazonDevicesMediaPlayerEntityDescription,
) -> None:
"""Initialize."""
self._prev_volume: int | None = None
@@ -141,11 +156,9 @@ class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity):
@property
def is_volume_muted(self) -> bool | None:
"""Return True if the volume is muted."""
if not self.volume_state or self.volume_state.volume is None:
if not self.volume_state:
return None
# is_muted is True when Alexa has muted the device
# volume == 0 is where we have muted by setting volume to 0
return self.volume_state.is_muted or self.volume_state.volume == 0
return self.volume_state.volume == 0
@property
def media_title(self) -> str | None:
@@ -199,7 +212,7 @@ class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity):
@property
def media_content_type(self) -> MediaType | None:
"""Content type — tells HA what kind of media is playing."""
if self.state in (MediaPlayerState.PLAYING, MediaPlayerState.PAUSED):
if self.state in [MediaPlayerState.PLAYING, MediaPlayerState.PAUSED]:
return MediaType.MUSIC
return None
@@ -212,18 +225,18 @@ class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity):
**kwargs: Any,
) -> None:
"""Play a piece of media."""
provider = media_type.value if isinstance(media_type, MediaType) else media_type
await self.async_call_alexa_music(media_id, provider)
await self.async_call_alexa_music(media_id, media_type)
@alexa_api_call
async def async_call_alexa_music(
self, search_phrase: str, provider_id: str
) -> None:
"""Call alexa music."""
async with alexa_api_call(self.coordinator):
await self.coordinator.api.call_alexa_music(
self.device, search_phrase, provider_id
)
await self.coordinator.api.call_alexa_music(
self.device, search_phrase, provider_id
)
@alexa_api_call
async def async_set_device_volume(self, volume: int) -> None:
"""Set the device volume."""
_LOGGER.debug(
@@ -231,8 +244,7 @@ class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity):
self.device.serial_number,
volume,
)
async with alexa_api_call(self.coordinator):
await self.coordinator.api.set_device_volume(self.device, volume)
await self.coordinator.api.set_device_volume(self.device, volume)
async def async_set_volume_level(self, volume: float) -> None:
"""Set the volume level (0.0 to 1.0)."""
@@ -247,27 +259,19 @@ class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity):
return
if mute:
self._prev_volume = self.volume_state.volume
await self.async_set_volume_level(0)
return
if self.volume_state.is_muted and self._prev_volume is None:
# is muted by Alexa which we can see but not control
# when muted this way, volume is still set
# changing volume will unmute
# if HA set volume to 0 then Alexa muted we just default to 30%
self._prev_volume = self.volume_state.volume or 30
if self._prev_volume is None:
return
target_volume = self._prev_volume
target_volume = 0
else:
if self._prev_volume is None:
return
target_volume = self._prev_volume
await self.async_set_volume_level(target_volume / 100)
self._prev_volume = None
@alexa_api_call
async def _send_media_command(self, command: AmazonMediaControls) -> None:
_LOGGER.debug(
"Sending media command '%s' to %s", command, self.device.serial_number
)
async with alexa_api_call(self.coordinator):
await self.coordinator.api.send_media_command(self.device, command)
await self.coordinator.api.send_media_command(self.device, command)
async def async_media_stop(self) -> None:
"""Send stop command."""
@@ -12,8 +12,9 @@ from homeassistant.components.notify import NotifyEntity, NotifyEntityDescriptio
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry, alexa_api_call
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
from .utils import alexa_api_call
PARALLEL_UPDATES = 1
@@ -79,11 +80,10 @@ class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
entity_description: AmazonNotifyEntityDescription
@alexa_api_call
async def async_send_message(
self, message: str, title: str | None = None, **kwargs: Any
) -> None:
"""Send a message."""
async with alexa_api_call(self.coordinator):
await self.entity_description.method(
self.coordinator.api, self.device, message
)
await self.entity_description.method(self.coordinator.api, self.device, message)
@@ -11,7 +11,7 @@ from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from .const import DOMAIN, INFO_SKILLS_MAPPING
from .coordinator import AmazonConfigEntry, alexa_api_call
from .coordinator import AmazonConfigEntry
ATTR_TEXT_COMMAND = "text_command"
ATTR_SOUND = "sound"
@@ -85,15 +85,13 @@ async def _async_execute_action(call: ServiceCall, attribute: str) -> None:
translation_key="invalid_sound_value",
translation_placeholders={"sound": value},
)
async with alexa_api_call():
await coordinator.api.call_alexa_sound(
coordinator.data[device.serial_number], value
)
await coordinator.api.call_alexa_sound(
coordinator.data[device.serial_number], value
)
elif attribute == ATTR_TEXT_COMMAND:
async with alexa_api_call():
await coordinator.api.call_alexa_text_command(
coordinator.data[device.serial_number], value
)
await coordinator.api.call_alexa_text_command(
coordinator.data[device.serial_number], value
)
elif attribute == ATTR_INFO_SKILL:
info_skill = INFO_SKILLS_MAPPING.get(value)
if info_skill not in ALEXA_INFO_SKILLS:
@@ -102,10 +100,9 @@ async def _async_execute_action(call: ServiceCall, attribute: str) -> None:
translation_key="invalid_info_skill_value",
translation_placeholders={"info_skill": value},
)
async with alexa_api_call():
await coordinator.api.call_alexa_info_skill(
coordinator.data[device.serial_number], info_skill
)
await coordinator.api.call_alexa_info_skill(
coordinator.data[device.serial_number], info_skill
)
async def async_send_sound_notification(call: ServiceCall) -> None:
@@ -125,9 +125,6 @@
},
"invalid_sound_value": {
"message": "Invalid sound {sound} specified"
},
"unknown_exception": {
"message": "Unknown error occurred: {error}"
}
},
"selector": {
@@ -14,9 +14,13 @@ from homeassistant.components.switch import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry, alexa_api_call
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
from .utils import async_remove_entity_from_virtual_group, async_update_unique_id
from .utils import (
alexa_api_call,
async_remove_dnd_from_virtual_group,
async_update_unique_id,
)
PARALLEL_UPDATES = 1
@@ -58,9 +62,7 @@ async def async_setup_entry(
new_key = "dnd"
# Remove old DND switch from virtual groups
await async_remove_entity_from_virtual_group(
hass, coordinator, SWITCH_DOMAIN, old_key
)
await async_remove_dnd_from_virtual_group(hass, coordinator, old_key)
# Replace unique id for DND switch
await async_update_unique_id(hass, coordinator, SWITCH_DOMAIN, old_key, new_key)
@@ -88,6 +90,7 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
entity_description: AmazonSwitchEntityDescription
@alexa_api_call
async def _switch_set_state(self, state: bool) -> None:
"""Set desired switch state."""
method = getattr(self.coordinator.api, self.entity_description.method)
@@ -95,8 +98,7 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
if TYPE_CHECKING:
assert method is not None
async with alexa_api_call(self.coordinator):
await method(self.device, state)
await method(self.device, state)
self.coordinator.data[self.device.serial_number].sensors[
self.entity_description.key
].value = state
@@ -1,18 +1,54 @@
"""Utils for Alexa Devices."""
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, Concatenate
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
from aioamazondevices.const.schedules import (
NOTIFICATION_ALARM,
NOTIFICATION_REMINDER,
NOTIFICATION_TIMER,
)
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.entity_registry as er
from .const import _LOGGER, DOMAIN
from .coordinator import AmazonDevicesCoordinator
from .entity import AmazonEntity
def alexa_api_call[_T: AmazonEntity, **_P](
func: Callable[Concatenate[_T, _P], Awaitable[None]],
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]:
"""Catch Alexa API call exceptions."""
@wraps(func)
async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
"""Wrap all command methods."""
try:
await func(self, *args, **kwargs)
except CannotConnect as err:
self.coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_connect_with_error",
translation_placeholders={"error": repr(err)},
) from err
except CannotRetrieveData as err:
self.coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(err)},
) from err
return cmd_wrapper
async def async_update_unique_id(
@@ -37,22 +73,23 @@ async def async_update_unique_id(
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
async def async_remove_entity_from_virtual_group(
async def async_remove_dnd_from_virtual_group(
hass: HomeAssistant,
coordinator: AmazonDevicesCoordinator,
platform: str,
key: str,
) -> None:
"""Remove entity from virtual group."""
"""Remove entity DND from virtual group."""
entity_registry = er.async_get(hass)
for serial_num in coordinator.data:
unique_id = f"{serial_num}-{key}"
entity_id = entity_registry.async_get_entity_id(DOMAIN, platform, unique_id)
entity_id = entity_registry.async_get_entity_id(
DOMAIN, SWITCH_DOMAIN, unique_id
)
is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY
if entity_id and is_group:
entity_registry.async_remove(entity_id)
_LOGGER.debug("Removed entity '%s' from virtual group", entity_id)
_LOGGER.debug("Removed DND switch from virtual group %s", entity_id)
async def async_remove_unsupported_notification_sensors(
@@ -34,13 +34,11 @@ def generate_site_selector_name(site: Site) -> str:
def filter_sites(sites: list[Site]) -> list[Site]:
"""Filter out closed sites and deduplicate the list of sites."""
"""Deduplicates the list of sites."""
filtered: list[Site] = []
filtered_nmi: set[str] = set()
for site in sorted(sites, key=lambda site: site.status):
if site.status == SiteStatus.CLOSED:
continue
if site.status == SiteStatus.ACTIVE or site.nmi not in filtered_nmi:
filtered.append(site)
filtered_nmi.add(site.nmi)
+16 -41
View File
@@ -5,12 +5,8 @@ from typing import Any
import voluptuous as vol
from homeassistant.components import labs, websocket_api
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.const import EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
@@ -53,7 +49,6 @@ 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"
@@ -62,39 +57,18 @@ 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)
try:
await analytics.load()
except HassioNotReadyError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="supervisor_not_ready",
) from err
# Load stored data
await analytics.load()
started = False
@@ -106,8 +80,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if started:
await analytics.async_schedule()
async def start_schedule(hass: HomeAssistant) -> None:
"""Start the send schedule once Home Assistant has started."""
async def start_schedule(_event: Event) -> None:
"""Start the send schedule after the started event."""
nonlocal started
started = True
await analytics.async_schedule()
@@ -115,7 +89,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
labs.async_subscribe_preview_feature(
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, _async_handle_labs_update
)
async_at_started(hass, start_schedule)
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)
hass.data[DATA_COMPONENT] = analytics
return True
@@ -130,9 +109,7 @@ def websocket_analytics(
msg: dict[str, Any],
) -> None:
"""Return analytics preferences."""
if (analytics := hass.data.get(DATA_COMPONENT)) is None:
connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, "Not loaded")
return
analytics = hass.data[DATA_COMPONENT]
connection.send_result(
msg["id"],
{ATTR_PREFERENCES: analytics.preferences, ATTR_ONBOARDED: analytics.onboarded},
@@ -153,10 +130,8 @@ 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,8 +299,12 @@ class Analytics:
self._data = AnalyticsData.from_dict(stored)
if self.supervisor and not self.onboarded:
# This may raise HassioNotReadyError if Supervisor was unreachable.
# The caller is responsible for handling this and triggering a retry.
# 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.
supervisor_info = hassio.get_supervisor_info(self._hass)
# User have not configured analytics, get this setting from the supervisor
@@ -345,10 +349,10 @@ class Analytics:
await self._save()
if self.supervisor:
# 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)
# 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)
with contextlib.suppress(hassio.HassioNotReadyError):
operating_system_info = hassio.get_os_info(hass)
with contextlib.suppress(hassio.HassioNotReadyError):
@@ -1,19 +0,0 @@
"""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,7 +3,6 @@
"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",
@@ -15,6 +14,5 @@
"report_issue_url": "https://github.com/OHF-Device-Database/device-database/issues/new"
}
},
"quality_scale": "internal",
"single_config_entry": true
"quality_scale": "internal"
}
@@ -1,9 +1,4 @@
{
"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,13 +38,11 @@ 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__)
@@ -128,6 +126,7 @@ 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()
@@ -353,41 +352,21 @@ class AppleTvMediaPlayer(
media_id = async_process_play_media_url(self.hass, play_item.url)
media_type = MediaType.MUSIC
use_stream_file = self._is_feature_available(FeatureName.StreamFile) and (
if self._is_feature_available(FeatureName.StreamFile) and (
media_type == MediaType.MUSIC or await is_streamable(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
):
_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,
)
@property
def media_image_hash(self) -> str | None:
@@ -481,7 +460,7 @@ class AppleTvMediaPlayer(
def _is_feature_available(self, feature: FeatureName) -> bool:
"""Return if a feature is available."""
if self.atv:
if self.atv and self._playing:
return self.atv.features.in_state(FeatureState.Available, feature)
return False
@@ -81,12 +81,6 @@
},
"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": {
@@ -1816,11 +1816,6 @@ class PipelineInput:
await self.run.text_to_speech(tts_input)
except PipelineError as err:
if self.run.tts_stream:
# Clean up TTS stream
self.run.tts_stream.delete()
self.run.tts_stream = None
self.run.process_event(
PipelineEvent(
PipelineEventType.ERROR,
@@ -1890,17 +1885,15 @@ class PipelineInput:
):
prepare_tasks.append(self.run.prepare_recognize_intent(self.session))
if prepare_tasks:
await asyncio.gather(*prepare_tasks)
# Do TTS prepare separately so we don't create a ResultStream if the
# pipeline is invalid.
if (
start_stage_index
<= PIPELINE_STAGE_ORDER.index(PipelineStage.TTS)
<= end_stage_index
):
await self.run.prepare_text_to_speech()
prepare_tasks.append(self.run.prepare_text_to_speech())
if prepare_tasks:
await asyncio.gather(*prepare_tasks)
class PipelinePreferred(CollectionError):
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.7.0"]
"requirements": ["hassil==3.5.0"]
}
+5 -19
View File
@@ -2,18 +2,12 @@
import avea
from homeassistant.components.bluetooth import (
BluetoothReachabilityIntent,
async_address_reachability_diagnostics,
async_ble_device_from_address,
)
from homeassistant.components.bluetooth import async_ble_device_from_address
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
type AveaConfigEntry = ConfigEntry[avea.Bulb]
PLATFORMS: list[Platform] = [Platform.LIGHT]
@@ -21,20 +15,12 @@ PLATFORMS: list[Platform] = [Platform.LIGHT]
async def async_setup_entry(hass: HomeAssistant, entry: AveaConfigEntry) -> bool:
"""Set up Avea from a config entry."""
address = entry.data[CONF_ADDRESS]
ble_device = async_ble_device_from_address(hass, address, connectable=True)
ble_device = async_ble_device_from_address(
hass, entry.data[CONF_ADDRESS], connectable=True
)
if not ble_device:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={
"address": address,
"reason": async_address_reachability_diagnostics(
hass,
address.upper(),
BluetoothReachabilityIntent.CONNECTION,
),
},
f"Could not find Avea device with address {entry.data[CONF_ADDRESS]}"
)
entry.runtime_data = avea.Bulb(ble_device)
+6 -13
View File
@@ -8,7 +8,6 @@ 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,
@@ -67,15 +66,6 @@ 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."""
@@ -160,7 +150,6 @@ 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 (
@@ -176,10 +165,11 @@ 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: _discovery_label(disc)}
{disc.address: label}
)
}
)
@@ -188,7 +178,10 @@ class AveaConfigFlow(ConfigFlow, domain=DOMAIN):
{
vol.Required(CONF_ADDRESS): vol.In(
{
service_info.address: _discovery_label(service_info)
service_info.address: (
f"{service_info.name or service_info.address}"
f" ({service_info.address})"
)
for service_info in self._discovered_devices.values()
}
),
@@ -22,11 +22,6 @@
}
}
},
"exceptions": {
"device_not_found": {
"message": "Could not find Avea device with address {address}: {reason}"
}
},
"issues": {
"deprecated_yaml": {
"description": "[%key:component::homeassistant::issues::deprecated_yaml::description%]",
@@ -26,12 +26,6 @@
"description": "The credentials for {username} need to be updated",
"title": "Re-authenticate Blink"
},
"reconfigure": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
}
},
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
@@ -27,7 +27,6 @@ from bluetooth_data_tools import monotonic_time_coarse as MONOTONIC_TIME
from habluetooth import (
BaseHaRemoteScanner,
BaseHaScanner,
BluetoothReachabilityIntent,
BluetoothScannerDevice,
BluetoothScanningMode,
HaBluetoothConnector,
@@ -56,7 +55,6 @@ 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,
@@ -110,14 +108,12 @@ __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",
+6 -21
View File
@@ -6,13 +6,11 @@ These APIs are the only documented way to interact with the bluetooth integratio
import asyncio
from asyncio import Future
from collections.abc import Callable, Iterable
from contextlib import ExitStack
from typing import TYPE_CHECKING, cast
from bleak import BleakScanner
from habluetooth import (
BaseHaScanner,
BluetoothReachabilityIntent,
BluetoothScannerDevice,
BluetoothScanningMode,
HaBleakScannerWrapper,
@@ -110,14 +108,6 @@ 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
@@ -179,20 +169,15 @@ async def async_process_advertisements(
if not done.done() and callback(service_info):
done.set_result(service_info)
manager = _get_manager(hass)
with ExitStack() as stack:
unload = manager.async_register_callback(
_async_discovered_device, match_dict, mode
)
stack.callback(unload)
if mode == BluetoothScanningMode.ACTIVE:
task = hass.async_create_task(manager.async_request_active_scan(timeout))
stack.callback(task.cancel)
unload = _get_manager(hass).async_register_callback(
_async_discovered_device, match_dict, mode, scan_duration=timeout
)
try:
async with asyncio.timeout(timeout):
return await done
finally:
unload()
@hass_callback
@@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.6.4",
"bluetooth-data-tools==1.29.18",
"dbus-fast==5.0.16",
"habluetooth==6.8.1"
"habluetooth==6.7.9"
]
}
@@ -3,7 +3,6 @@
"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_translation_key = "rf_transmitter"
_attr_name = None
def __init__(self, device: BroadlinkDevice) -> None:
"""Initialize the entity."""
@@ -54,11 +54,6 @@
"name": "IR emitter"
}
},
"radio_frequency": {
"rf_transmitter": {
"name": "RF transmitter"
}
},
"select": {
"day_of_week": {
"name": "Day of week",
@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==3.23.4"]
"requirements": ["bthome-ble==3.23.2"]
}
+1 -2
View File
@@ -3,7 +3,6 @@
import logging
import caldav
from caldav.lib.error import DAVError
from homeassistant.core import HomeAssistant
@@ -27,7 +26,7 @@ async def async_get_calendars(
for calendar in client.principal().calendars():
try:
supported_components = calendar.get_supported_components()
except KeyError, DAVError:
except KeyError:
needs_warning.append((str(calendar.url), calendar.name, component))
if component in ASSUMED_COMPONENTS:
@@ -8,6 +8,6 @@
"iot_class": "local_push",
"loggers": ["aiostreammagic"],
"quality_scale": "platinum",
"requirements": ["aiostreammagic==2.13.2"],
"requirements": ["aiostreammagic==2.13.1"],
"zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."]
}
@@ -66,10 +66,5 @@ 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)
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "platinum",
"requirements": ["aiocomelit==2.0.7"]
"requirements": ["aiocomelit==2.0.3"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.7.0", "home-assistant-intents==2026.6.1"]
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.5.5"]
}
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"requirements": ["pydaikin==2.18.1"],
"requirements": ["pydaikin==2.17.2"],
"zeroconf": ["_dkapi._tcp.local."]
}
@@ -20,8 +20,6 @@ from denonavr.const import (
from denonavr.exceptions import (
AvrCommandError,
AvrForbiddenError,
AvrIncompleteResponseError,
AvrInvalidResponseError,
AvrNetworkError,
AvrProcessingError,
AvrTimoutError,
@@ -193,17 +191,6 @@ 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(
@@ -12,19 +12,13 @@ from homeassistant.const import (
CONF_DOMAIN,
CONF_ENTITY_ID,
CONF_EVENT,
CONF_OPTIONS,
CONF_PLATFORM,
CONF_TYPE,
CONF_ZONE,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.trigger import (
TriggerActionType,
TriggerInfo,
# protected, but only used for legacy triggers
_async_attach_trigger_cls,
)
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
@@ -85,18 +79,16 @@ async def async_attach_trigger(
event = zone.EVENT_ENTER
else:
event = zone.EVENT_LEAVE
zone_config = await zone.LegacyZoneTrigger.async_validate_config(
hass,
{
CONF_OPTIONS: {
CONF_ENTITY_ID: [config[CONF_ENTITY_ID]],
CONF_ZONE: config[CONF_ZONE],
CONF_EVENT: event,
}
},
)
return await _async_attach_trigger_cls(
hass, zone.LegacyZoneTrigger, "device", zone_config, action, trigger_info
zone_config = {
CONF_PLATFORM: ZONE_DOMAIN,
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
CONF_ZONE: config[CONF_ZONE],
CONF_EVENT: event,
}
zone_config = await zone.async_validate_trigger_config(hass, zone_config)
return await zone.async_attach_trigger(
hass, zone_config, action, trigger_info, platform_type="device"
)
@@ -1,6 +1,7 @@
"""Provide functionality to keep track of devices."""
import asyncio
import logging
from typing import TYPE_CHECKING, Any, final
from propcache.api import cached_property
@@ -22,6 +23,7 @@ from homeassistant.core import (
EventStateChangedData,
HomeAssistant,
State,
async_get_hass_or_none,
callback,
)
from homeassistant.helpers import (
@@ -37,6 +39,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.loader import async_suggest_report_issue
from homeassistant.util.hass_dict import HassKey
from .const import (
@@ -52,6 +55,8 @@ from .const import (
SourceType,
)
_LOGGER = logging.getLogger(__name__)
DATA_KEY: HassKey[dict[str, tuple[str, str]]] = HassKey(f"{DOMAIN}_mac")
@@ -164,11 +169,35 @@ class BaseTrackerEntity(Entity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_source_type: SourceType
def __init_subclass__(cls, **kwargs: Any) -> None:
"""Post initialisation processing."""
super().__init_subclass__(**kwargs)
if "battery_level" in cls.__dict__:
if cls.__module__.startswith("homeassistant.components."):
# Don't ask users to report issue for built in integrations,
# they already have issues opened on them.
return
report_issue = async_suggest_report_issue(
async_get_hass_or_none(), module=cls.__module__
)
_LOGGER.warning(
(
"%s::%s is overriding the deprecated battery_level property on "
"a subclass of BaseTrackerEntity, this will be unsupported from "
"Home Assistant 2027.7, please %s"
),
cls.__module__,
cls.__name__,
report_issue,
)
@cached_property
def battery_level(self) -> int | None:
"""Return the battery level of the device.
Percentage from 0-100.
The property is deprecated and will be removed in Home Assistant 2027.7.
"""
return None
@@ -212,13 +241,38 @@ class TrackerEntity(
_attr_in_zones: list[str] | None = None
_attr_latitude: float | None = None
_attr_location_accuracy: float = 0
# _attr_location_name is deprecated and will be removed in Home Assistant 2027.7
_attr_location_name: str | None = None
_attr_longitude: float | None = None
_attr_source_type: SourceType = SourceType.GPS
__active_zone: State | None = None
# If we reported setting deprecated _attr_location_name
__deprecated_attr_location_name_reported = False
__in_zones: list[str] | None = None
def __init_subclass__(cls, **kwargs: Any) -> None:
"""Post initialisation processing."""
super().__init_subclass__(**kwargs)
if "location_name" in cls.__dict__:
if cls.__module__.startswith("homeassistant.components."):
# Don't ask users to report issue for built in integrations,
# they already have issues opened on them.
return
report_issue = async_suggest_report_issue(
async_get_hass_or_none(), module=cls.__module__
)
_LOGGER.warning(
(
"%s::%s is overriding the deprecated location_name property on "
"an instance of TrackerEntity, this will be unsupported from "
"Home Assistant 2027.7, please %s"
),
cls.__module__,
cls.__name__,
report_issue,
)
@cached_property
def should_poll(self) -> bool:
"""No polling for entities that have location pushed."""
@@ -249,7 +303,32 @@ class TrackerEntity(
@cached_property
def location_name(self) -> str | None:
"""Return a location name for the current location of the device."""
"""Return a location name for the current location of the device.
The property is deprecated and will be removed in Home Assistant 2027.7.
"""
if (location_name := self._attr_location_name) is not None:
if (
not self.__deprecated_attr_location_name_reported
and not self.__class__.__module__.startswith(
"homeassistant.components."
)
):
report_issue = async_suggest_report_issue(
self.hass, module=self.__class__.__module__
)
_LOGGER.warning(
(
"%s::%s is setting the deprecated _attr_location_name attribute "
"on an instance of TrackerEntity, this will be unsupported from "
"Home Assistant 2027.7, please %s"
),
self.__class__.__module__,
self.__class__.__name__,
report_issue,
)
self.__deprecated_attr_location_name_reported = True
return location_name
return self._attr_location_name
@cached_property
+2 -2
View File
@@ -16,7 +16,7 @@
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.7",
"aiodiscover==3.3.2",
"cached-ipaddress==1.1.2"
"aiodiscover==3.2.4",
"cached-ipaddress==1.1.1"
]
}
+1 -1
View File
@@ -8,5 +8,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["dsmr_parser"],
"requirements": ["dsmr-parser==1.9.0"]
"requirements": ["dsmr-parser==1.7.0"]
}
+2 -12
View File
@@ -86,6 +86,7 @@ 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,
@@ -99,18 +100,7 @@ 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=rssi_wifi,
rssi_wifi=lan_info.rssi_wifi,
)
+1 -1
View File
@@ -59,7 +59,7 @@
"name": "Target flow level"
},
"time_state_end": {
"name": "State end time"
"name": "Mode end time"
},
"ventilation_state": {
"name": "Ventilation state",
+1 -3
View File
@@ -30,9 +30,7 @@
},
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"description": "Please enter the API key obtained from ecobee.com."
}
+2 -2
View File
@@ -39,12 +39,12 @@ class EconetFanModeSelect(EcoNetEntity[Thermostat], SelectEntity):
@property
def options(self) -> list[str]:
"""Return available select options."""
return [e.name for e in self._econet.fan_modes]
return [e.value for e in self._econet.fan_modes]
@property
def current_option(self) -> str:
"""Return current select option."""
return self._econet.fan_mode.name
return self._econet.fan_mode.value
def select_option(self, option: str) -> None:
"""Set the selected option."""
@@ -294,9 +294,6 @@
"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"
},
+3 -2
View File
@@ -353,10 +353,11 @@ 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_not_supported",
translation_placeholders={"command": command, "name": name},
translation_key="vacuum_send_command_area_not_supported",
translation_placeholders={"name": name},
)
if command == "spot_area":
@@ -246,8 +246,8 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN):
)
if device is not None and device.mac_address:
await self.async_set_unique_id(dr.format_mac(device.mac_address))
if reconfigure_entry.unique_id is not None:
self._abort_if_unique_id_mismatch()
# aborts if user tried to switch devices
self._abort_if_unique_id_mismatch()
else:
# If we cannot confirm identity, keep existing
# behavior (don't block reconfigure)
@@ -255,7 +255,6 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_update_reload_and_abort(
reconfigure_entry,
unique_id=self.unique_id,
data_updates={
**reconfigure_entry.data,
CONF_HOST: info[CONF_HOST],
@@ -196,6 +196,4 @@ 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)
@@ -8,14 +8,13 @@ from eq3btsmart import Thermostat
from eq3btsmart.exceptions import Eq3Exception
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import BluetoothReachabilityIntent
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import DOMAIN, SIGNAL_THERMOSTAT_CONNECTED, SIGNAL_THERMOSTAT_DISCONNECTED
from .const import SIGNAL_THERMOSTAT_CONNECTED, SIGNAL_THERMOSTAT_DISCONNECTED
from .models import Eq3Config, Eq3ConfigEntryData
PLATFORMS = [
@@ -50,16 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: Eq3ConfigEntry) -> bool:
if device is None:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={
"mac_address": eq3_config.mac_address,
"reason": bluetooth.async_address_reachability_diagnostics(
hass,
mac_address.upper(),
BluetoothReachabilityIntent.CONNECTION,
),
},
f"[{eq3_config.mac_address}] Device could not be found"
)
thermostat = Thermostat(device)
@@ -61,10 +61,5 @@
"name": "Lock"
}
}
},
"exceptions": {
"device_not_found": {
"message": "[{mac_address}] Device could not be found: {reason}"
}
}
}
@@ -166,8 +166,6 @@ class RuntimeEntryData:
)
loaded_platforms: set[Platform] = field(default_factory=set)
platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
# Set once the first connection has finished scanner setup or teardown.
first_connect_done: asyncio.Event = field(default_factory=asyncio.Event)
_storage_contents: StoreData | None = None
_pending_storage: Callable[[], StoreData] | None = None
assist_pipeline_update_callbacks: list[CALLBACK_TYPE] = field(default_factory=list)
+1 -22
View File
@@ -1,12 +1,11 @@
"""Manager for esphome devices."""
import asyncio
import base64
from functools import partial
import logging
import secrets
import struct
from typing import TYPE_CHECKING, Any, Final, NamedTuple
from typing import TYPE_CHECKING, Any, NamedTuple
from aioesphomeapi import (
APIClient,
@@ -107,9 +106,6 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
# Max time to wait at startup for a BLE proxy to register its scanner.
STARTUP_SCANNER_WAIT: Final = 3.0
LOG_LEVEL_TO_LOGGER = {
LogLevel.LOG_LEVEL_NONE: logging.DEBUG,
LogLevel.LOG_LEVEL_ERROR: logging.ERROR,
@@ -681,8 +677,6 @@ class ESPHomeManager:
hass, device_info.bluetooth_mac_address or device_info.mac_address
)
entry_data.first_connect_done.set()
if device_info.voice_assistant_feature_flags_compat(api_version) and (
Platform.ASSIST_SATELLITE not in entry_data.loaded_platforms
):
@@ -994,21 +988,6 @@ class ESPHomeManager:
await reconnect_logic.start()
# Wait for a cached BLE proxy to register its scanner before finishing setup.
if (
device_info := entry_data.device_info
) is not None and device_info.bluetooth_proxy_feature_flags_compat(
entry_data.api_version
):
try:
async with asyncio.timeout(STARTUP_SCANNER_WAIT):
await entry_data.first_connect_done.wait()
except TimeoutError:
_LOGGER.debug(
"%s: Timed out waiting for Bluetooth scanner to register",
self.host,
)
@callback
def _async_setup_device_registry(
@@ -284,19 +284,6 @@ 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:
@@ -23,18 +23,14 @@ from homeassistant.helpers.selector import (
TextSelectorType,
)
from .const import CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES, DOMAIN, USER_AGENT
from .const import CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES, DOMAIN
LOGGER = logging.getLogger(__name__)
async def async_fetch_feed(hass: HomeAssistant, url: str) -> feedparser.FeedParserDict:
"""Fetch the feed."""
def _parse_feed() -> feedparser.FeedParserDict:
return feedparser.parse(url, agent=USER_AGENT)
return await hass.async_add_executor_job(_parse_feed)
return await hass.async_add_executor_job(feedparser.parse, url)
class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
@@ -3,8 +3,6 @@
from datetime import timedelta
from typing import Final
from homeassistant.const import APPLICATION_NAME, __version__ as ha_version
DOMAIN: Final[str] = "feedreader"
CONF_MAX_ENTRIES: Final[str] = "max_entries"
@@ -12,5 +10,3 @@ DEFAULT_MAX_ENTRIES: Final[int] = 20
DEFAULT_SCAN_INTERVAL: Final[timedelta] = timedelta(hours=1)
EVENT_FEEDREADER: Final[str] = "feedreader"
USER_AGENT: Final[str] = f"{APPLICATION_NAME}/{ha_version}"
@@ -18,13 +18,7 @@ from homeassistant.helpers.storage import Store
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import (
CONF_MAX_ENTRIES,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
EVENT_FEEDREADER,
USER_AGENT,
)
from .const import CONF_MAX_ENTRIES, DEFAULT_SCAN_INTERVAL, DOMAIN, EVENT_FEEDREADER
DELAY_SAVE = 30
STORAGE_VERSION = 1
@@ -80,7 +74,6 @@ class FeedReaderCoordinator(
self.url,
etag=None if not self._feed else self._feed.get("etag"),
modified=None if not self._feed else self._feed.get("modified"),
agent=USER_AGENT,
)
feed = await self.hass.async_add_executor_job(_parse_feed)
@@ -2,12 +2,10 @@
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,
@@ -30,9 +28,7 @@ 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)
@@ -13,11 +13,6 @@
"discovery_confirm": {
"description": "Do you want to set up {model} {id} ({ipaddr})?"
},
"pick_device": {
"data": {
"device": "[%key:common::config_flow::data::device%]"
}
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
@@ -938,15 +938,3 @@ 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,11 +24,9 @@ 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.7"]
"requirements": ["home-assistant-frontend==20260527.0"]
}
@@ -320,7 +320,7 @@ class AFSAPIDevice(MediaPlayerEntity):
@fs_command_exception_wrap
async def async_media_play(self) -> None:
"""Send play command."""
if (await self.fs_device.get_play_status()) == PlayState.STOPPED:
if (await self.fs_device.get_play_state()) == PlayState.STOPPED:
# The 'play' command only seems to work when the current stream is paused.
# We need to send a 'stop' command instead to resume a stopped stream.
await self.fs_device.stop()
@@ -1,13 +1,11 @@
"""The Gardena Bluetooth integration."""
from contextlib import suppress
import logging
from bleak.backends.device import BLEDevice
from gardena_bluetooth.client import CachedConnection, Client
from gardena_bluetooth.const import ScanService
from gardena_bluetooth.parse import ManufacturerData, ProductType
from habluetooth import BluetoothServiceInfoBleak
from gardena_bluetooth.const import ProductType
from gardena_bluetooth.scan import async_get_manufacturer_data
from homeassistant.components import bluetooth
from homeassistant.const import CONF_ADDRESS, Platform
@@ -32,64 +30,6 @@ PLATFORMS: list[Platform] = [
]
LOGGER = logging.getLogger(__name__)
DISCONNECT_DELAY = 5
PRODUCTS_SCAN_TIMEOUT = 10
PRODUCT_TYPE_TIMEOUT = 30
async def async_get_product_type(hass: HomeAssistant, address: str) -> ProductType:
"""Get a product type for the given address."""
data = ManufacturerData()
def _data_callback(info: BluetoothServiceInfoBleak) -> bool:
LOGGER.debug("Processing advertisement from %s: %s", info.address, info)
if info.device.address != address:
return False
data.update(info.manufacturer_data.get(ManufacturerData.company, b""))
return data.product_type is not ProductType.UNKNOWN
with suppress(TimeoutError):
await bluetooth.async_process_advertisements(
hass,
_data_callback,
bluetooth.BluetoothCallbackMatcher(
address=address, manufacturer_id=ManufacturerData.company
),
mode=bluetooth.BluetoothScanningMode.ACTIVE,
timeout=PRODUCT_TYPE_TIMEOUT,
)
return data.product_type
async def async_get_products(hass: HomeAssistant) -> dict[str, ManufacturerData]:
"""Get all products that are currently advertising."""
products: dict[str, ManufacturerData] = {}
def _data_callback(info: BluetoothServiceInfoBleak) -> bool:
LOGGER.debug("Processing advertisement from %s: %s", info.address, info)
if ScanService not in info.service_uuids:
return False
raw = info.manufacturer_data.get(ManufacturerData.company, b"")
if (data := products.get(info.device.address)) is None:
data = ManufacturerData()
products[info.device.address] = data
data.update(raw)
return False
with suppress(TimeoutError):
await bluetooth.async_process_advertisements(
hass,
_data_callback,
bluetooth.BluetoothCallbackMatcher(
manufacturer_id=ManufacturerData.company
),
mode=bluetooth.BluetoothScanningMode.ACTIVE,
timeout=PRODUCTS_SCAN_TIMEOUT,
)
return products
def get_connection(hass: HomeAssistant, address: str) -> CachedConnection:
@@ -113,7 +53,12 @@ async def async_setup_entry(
address = entry.data[CONF_ADDRESS]
product_type = await async_get_product_type(hass, address)
try:
mfg_data = await async_get_manufacturer_data({address})
except TimeoutError as exc:
raise ConfigEntryNotReady("Unable to find product type") from exc
product_type = mfg_data[address].product_type
if product_type is ProductType.UNKNOWN:
raise ConfigEntryNotReady("Unable to find product type")
@@ -4,17 +4,21 @@ import logging
from typing import Any
from gardena_bluetooth.client import Client
from gardena_bluetooth.const import PRODUCT_NAMES, DeviceInformation
from gardena_bluetooth.const import PRODUCT_NAMES, DeviceInformation, ScanService
from gardena_bluetooth.exceptions import CharacteristicNotFound, CommunicationFailure
from gardena_bluetooth.parse import ProductType
from gardena_bluetooth.parse import ManufacturerData, ProductType
from gardena_bluetooth.scan import async_get_manufacturer_data
import voluptuous as vol
from homeassistant.components.bluetooth import BluetoothServiceInfo
from homeassistant.components.bluetooth import (
BluetoothServiceInfo,
async_discovered_service_info,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ADDRESS
from homeassistant.data_entry_flow import AbortFlow
from . import async_get_product_type, async_get_products, get_connection
from . import get_connection
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -29,6 +33,17 @@ _SUPPORTED_PRODUCT_TYPES = {
}
def _is_supported(discovery_info: BluetoothServiceInfo):
"""Check if device is supported."""
if ScanService not in discovery_info.service_uuids:
return False
if discovery_info.manufacturer_data.get(ManufacturerData.company) is None:
_LOGGER.debug("Missing manufacturer data: %s", discovery_info)
return False
return True
class GardenaBluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Gardena Bluetooth."""
@@ -60,7 +75,8 @@ class GardenaBluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle the bluetooth discovery step."""
_LOGGER.debug("Discovered device: %s", discovery_info)
product_type = await async_get_product_type(self.hass, discovery_info.address)
data = await async_get_manufacturer_data({discovery_info.address})
product_type = data[discovery_info.address].product_type
if product_type not in _SUPPORTED_PRODUCT_TYPES:
return self.async_abort(reason="no_devices_found")
@@ -101,16 +117,22 @@ class GardenaBluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
return await self.async_step_confirm()
current = self._async_current_ids(include_ignore=False)
devices = await async_get_products(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
candidates = set()
for discovery_info in async_discovered_service_info(self.hass):
address = discovery_info.address
if address in current_addresses or not _is_supported(discovery_info):
continue
candidates.add(address)
data = await async_get_manufacturer_data(candidates)
for address, mfg_data in data.items():
if mfg_data.product_type not in _SUPPORTED_PRODUCT_TYPES:
continue
self.devices[address] = PRODUCT_NAMES[mfg_data.product_type]
# Keep selection sorted by address to ensure stable tests
self.devices = {
address: PRODUCT_NAMES[data.product_type]
for address in sorted(devices)
if address not in current
and (data := devices[address]).product_type in _SUPPORTED_PRODUCT_TYPES
}
self.devices = dict(sorted(self.devices.items(), key=lambda x: x[0]))
if not self.devices:
return self.async_abort(reason="no_devices_found")
@@ -1,12 +1,11 @@
{
"domain": "gentex_homelink",
"name": "HomeLink",
"codeowners": ["@Gentex-Corporation/Homelink", "@rjones-gentex"],
"codeowners": ["@niaexa", "@ryanjones-gentex"],
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/gentex_homelink",
"integration_type": "hub",
"iot_class": "cloud_push",
"quality_scale": "bronze",
"requirements": ["homelink-integration-api==0.0.5"]
"requirements": ["homelink-integration-api==0.0.1"]
}
@@ -11,7 +11,6 @@
"step": {
"user": {
"data": {
"device": "[%key:common::config_flow::data::device%]",
"ip_address": "[%key:common::config_flow::data::ip%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
@@ -8,7 +8,7 @@
"integration_type": "service",
"iot_class": "cloud_push",
"requirements": [
"google-cloud-texttospeech==2.25.1",
"google-cloud-speech==2.31.1"
"google-cloud-texttospeech==2.36.0",
"google-cloud-speech==2.38.0"
]
}
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/google_pubsub",
"iot_class": "cloud_push",
"quality_scale": "legacy",
"requirements": ["google-cloud-pubsub==2.29.0"]
"requirements": ["google-cloud-pubsub==2.38.0"]
}
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["google", "homeassistant.helpers.location"],
"requirements": ["google-maps-routing==0.6.15"]
"requirements": ["google-maps-routing==0.10.0"]
}
@@ -34,11 +34,7 @@ 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,
ConfigEntryNotReady,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
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
@@ -59,7 +55,6 @@ 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
@@ -269,10 +264,6 @@ 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})"
@@ -189,8 +189,7 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
) from err
total_info["todayEnergy"] = total_info["today_energy"]
total_info["totalEnergy"] = total_info["total_energy"]
# V1 API returns current_power in kW, convert to W
total_info["invTodayPpv"] = total_info["current_power"] * 1000
total_info["invTodayPpv"] = total_info["current_power"]
else:
# Classic API: use plant_info as before.
# Copy the response to avoid mutating the dict returned by the library
+1 -24
View File
@@ -8,7 +8,7 @@ import os
import struct
from typing import Any
from aiohasupervisor import SupervisorBadRequestError, SupervisorError
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import (
GreenOptions,
HomeAssistantOptions,
@@ -25,7 +25,6 @@ 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,
@@ -302,28 +301,6 @@ 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,9 +55,6 @@
},
"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": {
@@ -15,7 +15,7 @@
},
"title": "[%key:component::here_travel_time::config::step::destination_menu::title%]"
},
"destination_entity": {
"destination_entity_id": {
"data": {
"destination_entity_id": "Destination using an entity"
},
@@ -34,7 +34,7 @@
},
"title": "[%key:component::here_travel_time::config::step::origin_menu::title%]"
},
"origin_entity": {
"origin_entity_id": {
"data": {
"origin_entity_id": "Origin using an entity"
},
@@ -12,7 +12,8 @@
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"host": "The hostname or IP address of your Hi-Link HLK-SW-16 device."
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.98", "babel==2.15.0"]
"requirements": ["holidays==0.97", "babel==2.15.0"]
}
@@ -135,24 +135,7 @@ async def async_migrate_entry(
)
if canonical.entry_id != config_entry.entry_id:
if canonical.minor_version < 2:
# The canonical entry has not been migrated yet and its
# migration will remove this duplicate.
return False
# The canonical entry is already fully migrated and will not run
# a migration that removes this duplicate, so remove it here. The
# entry can't remove itself while its setup lock is held, so
# schedule the removal instead.
_LOGGER.debug(
"Removing duplicate config entry %s for serial %s in favor of %s",
config_entry.entry_id,
serial_number,
canonical.entry_id,
)
hass.async_create_task(
hass.config_entries.async_remove(config_entry.entry_id)
)
# The canonical entry's migration will remove this duplicate.
return False
for duplicate in duplicates:
@@ -239,24 +239,7 @@ async def async_migrate_entry(
)
if canonical.entry_id != config_entry.entry_id:
if canonical.minor_version < 5:
# The canonical entry has not been migrated yet and its
# migration will remove this duplicate.
return False
# The canonical entry is already fully migrated and will not run
# a migration that removes this duplicate, so remove it here. The
# entry can't remove itself while its setup lock is held, so
# schedule the removal instead.
_LOGGER.warning(
"Removing duplicate config entry %s for serial %s in favor of %s",
config_entry.entry_id,
serial_number,
canonical.entry_id,
)
hass.async_create_task(
hass.config_entries.async_remove(config_entry.entry_id)
)
# The canonical entry's migration will remove this duplicate.
return False
for duplicate in duplicates:
@@ -10,8 +10,7 @@
"step": {
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
"password": "[%key:common::config_flow::data::password%]"
},
"description": "The Honeywell integration needs to re-authenticate your account",
"title": "[%key:common::config_flow::title::reauth%]"
+2 -7
View File
@@ -9,12 +9,10 @@ import time
from typing import TYPE_CHECKING, Any, cast
from urllib.parse import urlparse
import uuid
import warnings
from aiohttp import ClientError, ClientResponse, ClientSession, web
from aiohttp.hdrs import AUTHORIZATION
import jwt
from jwt.warnings import InsecureKeyLengthWarning
from py_vapid import Vapid
from pywebpush import WebPusher, WebPushException, webpush_async
import voluptuous as vol
@@ -327,8 +325,7 @@ class HTML5PushCallbackView(HomeAssistantView):
if target_check.get(ATTR_TARGET) in self.registrations:
possible_target = self.registrations[target_check[ATTR_TARGET]]
key = possible_target["subscription"]["keys"]["auth"]
with suppress(jwt.exceptions.DecodeError), warnings.catch_warnings():
warnings.simplefilter("ignore", InsecureKeyLengthWarning)
with suppress(jwt.exceptions.DecodeError):
return jwt.decode(token, key, algorithms=["ES256", "HS256"])
return self.json_message(
@@ -588,9 +585,7 @@ def add_jwt(timestamp: int, target: str, tag: str, jwt_secret: str) -> str:
ATTR_TARGET: target,
ATTR_TAG: tag,
}
with warnings.catch_warnings():
warnings.simplefilter("ignore", InsecureKeyLengthWarning)
return jwt.encode(jwt_claims, jwt_secret)
return jwt.encode(jwt_claims, jwt_secret)
async def async_setup_entry(
-6
View File
@@ -1,12 +1,6 @@
{
"entity": {
"light": {
"hue_grouped_light": {
"default": "mdi:lightbulb-group",
"state": {
"off": "mdi:lightbulb-group-off"
}
},
"hue_light": {
"state_attributes": {
"effect": {
+1 -2
View File
@@ -18,8 +18,7 @@
"step": {
"init": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"id": "Hue bridge"
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of your Hue bridge."
+1 -1
View File
@@ -85,7 +85,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
entity_description = LightEntityDescription(
key="hue_grouped_light",
translation_key="hue_grouped_light",
icon="mdi:lightbulb-group",
has_entity_name=True,
name=None,
)
+6 -10
View File
@@ -178,21 +178,17 @@ 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) and (
mirek_max := color_temp.mirek_schema.mirek_maximum
):
return mirek_max
# return a fallback value if the light doesn't provide valid limits
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
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) and (
mirek_min := color_temp.mirek_schema.mirek_minimum
):
return mirek_min
# return a fallback value if the light doesn't provide valid limits
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
return FALLBACK_MIN_MIREDS
@property
+1 -3
View File
@@ -166,10 +166,8 @@ class HueLightLevelSensor(HueSensorBase):
)
@property
def native_value(self) -> int | None:
def native_value(self) -> int:
"""Return the value reported by the sensor."""
if self.resource.light.value is None:
return None
# Light level in 10000 log10 (lux) +1 measured by sensor. Logarithm
# scale used because the human eye adjusts to light levels and small
# changes at low lux levels are more noticeable than at high lux
@@ -9,5 +9,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==2.7.6"]
"requirements": ["aioautomower==2.7.5"]
}

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