Compare commits

..

6 Commits

Author SHA1 Message Date
epenet cf437bc23b Replace mqtt with huawei_lte 2026-05-29 14:29:06 +00:00
epenet 904449356c Merge branch 'dev' into epenet/20260529-1252 2026-05-29 14:18:48 +02:00
epenet 534ef4695b Adjust 2026-05-29 11:29:46 +00:00
epenet 616a1c25a2 Do not use StrEnum 2026-05-29 11:23:11 +00:00
epenet ffe76224a8 Use in some components 2026-05-29 11:03:54 +00:00
epenet 8fadb72509 Add new StrEnum to set device info attributes 2026-05-29 10:53:17 +00:00
178 changed files with 1067 additions and 2967 deletions
+2 -2
View File
@@ -380,7 +380,7 @@ jobs:
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: ${{ matrix.registry }}/home-assistant
sep-tags: ","
@@ -394,7 +394,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v3.7.1
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v3.7.1
- name: Copy architecture images to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
+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,
)
)
@@ -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
@@ -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,4 +52,4 @@ class AmazonRoutineButton(AmazonServiceEntity, ButtonEntity):
async def async_press(self) -> None:
"""Handle button press action."""
await self.coordinator.api.call_routine(self._routine)
await self._coordinator.api.call_routine(self._routine)
+17 -17
View File
@@ -7,17 +7,13 @@ from typing import Any, Concatenate
from androidtv.exceptions import LockNotAcquiredException
from homeassistant.const import (
ATTR_CONNECTIONS,
ATTR_IDENTIFIERS,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_SW_VERSION,
CONF_HOST,
CONF_NAME,
)
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
DeviceInfoAttribute,
)
from homeassistant.helpers.entity import Entity
from . import (
@@ -129,19 +125,23 @@ class AndroidTVEntity(Entity):
CONF_NAME, f"{device_type} {entry.data[CONF_HOST]}"
)
info = self.aftv.device_properties
model = info.get(ATTR_MODEL)
model = info.get("model")
self._attr_device_info = DeviceInfo(
model=f"{model} ({device_type})" if model else device_type,
name=device_name,
)
if self.unique_id:
self._attr_device_info[ATTR_IDENTIFIERS] = {(DOMAIN, self.unique_id)}
if manufacturer := info.get(ATTR_MANUFACTURER):
self._attr_device_info[ATTR_MANUFACTURER] = manufacturer
if sw_version := info.get(ATTR_SW_VERSION):
self._attr_device_info[ATTR_SW_VERSION] = sw_version
self._attr_device_info[DeviceInfoAttribute.IDENTIFIERS] = {
(DOMAIN, self.unique_id)
}
if manufacturer := info.get("manufacturer"):
self._attr_device_info[DeviceInfoAttribute.MANUFACTURER] = manufacturer
if sw_version := info.get("sw_version"):
self._attr_device_info[DeviceInfoAttribute.SW_VERSION] = sw_version
if mac := get_androidtv_mac(info):
self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)}
self._attr_device_info[DeviceInfoAttribute.CONNECTIONS] = {
(CONNECTION_NETWORK_MAC, mac)
}
# ADB exceptions to catch
if not self.aftv.adb_server_ip:
+1 -1
View File
@@ -372,7 +372,7 @@ def _convert_content( # noqa: C901
)
if (
content.native.container is not None
and content.native.container.expires_at > datetime.now(UTC) # pylint: disable=home-assistant-enforce-utcnow
and content.native.container.expires_at > datetime.now(UTC)
):
container_id = content.native.container.id
+10 -15
View File
@@ -14,13 +14,6 @@ from pyatv.interface import AppleTV as AppleTVInterface, DeviceListener
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_CONNECTIONS,
ATTR_IDENTIFIERS,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_NAME,
ATTR_SUGGESTED_AREA,
ATTR_SW_VERSION,
CONF_ADDRESS,
CONF_NAME,
EVENT_HOMEASSISTANT_STOP,
@@ -354,12 +347,12 @@ class AppleTVManager(DeviceListener):
@callback
def _async_setup_device_registry(self) -> None:
attrs = {
ATTR_IDENTIFIERS: {(DOMAIN, self.config_entry.unique_id)},
ATTR_MANUFACTURER: "Apple",
ATTR_NAME: self.config_entry.data[CONF_NAME],
dr.DeviceInfoAttribute.IDENTIFIERS: {(DOMAIN, self.config_entry.unique_id)},
dr.DeviceInfoAttribute.MANUFACTURER: "Apple",
dr.DeviceInfoAttribute.NAME: self.config_entry.data[CONF_NAME],
}
attrs[ATTR_SUGGESTED_AREA] = (
attrs[ATTR_NAME]
attrs[dr.DeviceInfoAttribute.SUGGESTED_AREA] = (
attrs[dr.DeviceInfoAttribute.NAME]
.removesuffix(f" {DEFAULT_NAME_TV}")
.removesuffix(f" {DEFAULT_NAME_HP}")
)
@@ -367,15 +360,17 @@ class AppleTVManager(DeviceListener):
if self.atv:
dev_info = self.atv.device_info
attrs[ATTR_MODEL] = (
attrs[dr.DeviceInfoAttribute.MODEL] = (
dev_info.raw_model
if dev_info.model is DeviceModel.Unknown and dev_info.raw_model
else model_str(dev_info.model)
)
attrs[ATTR_SW_VERSION] = dev_info.version
attrs[dr.DeviceInfoAttribute.SW_VERSION] = dev_info.version
if dev_info.mac:
attrs[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, dev_info.mac)}
attrs[dr.DeviceInfoAttribute.CONNECTIONS] = {
(dr.CONNECTION_NETWORK_MAC, dev_info.mac)
}
device_registry = dr.async_get(self.hass)
device_registry.async_get_or_create(
@@ -30,5 +30,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.7", "yalexs-ble==3.3.0"]
"requirements": ["yalexs==9.2.1", "yalexs-ble==3.3.0"]
}
+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()
}
),
@@ -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",
@@ -11,7 +11,6 @@ from typing import TYPE_CHECKING, cast
from bleak import BleakScanner
from habluetooth import (
BaseHaScanner,
BluetoothReachabilityIntent,
BluetoothScannerDevice,
BluetoothScanningMode,
HaBleakScannerWrapper,
@@ -109,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
@@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.6.4",
"bluetooth-data-tools==1.29.18",
"dbus-fast==5.0.16",
"habluetooth==6.8.0"
"habluetooth==6.7.9"
]
}
@@ -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)
@@ -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(
@@ -100,7 +100,7 @@ class DwdWeatherWarningsSensor(
if warnings is None:
return []
now = datetime.now(UTC) # pylint: disable=home-assistant-enforce-utcnow
now = datetime.now(UTC)
return [warning for warning in warnings if warning[API_ATTR_WARNING_END] > now]
@property
@@ -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":
@@ -221,7 +221,6 @@ def update_listeners(hass: HomeAssistant, entry: EnergyIDConfigEntry) -> None:
):
try:
value = float(current_state.state)
# pylint: disable-next=home-assistant-enforce-utcnow
timestamp = current_state.last_updated or dt.datetime.now(dt.UTC)
client.get_or_create_sensor(energyid_key).update(value, timestamp)
except ValueError, TypeError:
@@ -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)
@@ -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:
+1 -1
View File
@@ -161,7 +161,7 @@ class EvoChild(EvoEntity):
or self._schedule is None
or (
(until := self._setpoints.get("next_sp_from")) is not None
and until < datetime.now(UTC) # pylint: disable=home-assistant-enforce-utcnow
and until < datetime.now(UTC)
)
): # must use self._setpoints, not self.setpoints
await get_schedule()
@@ -91,7 +91,6 @@ class TokenManager(AbstractTokenManager, AbstractSessionManager):
session_id_expires = session.get(SZ_SESSION_ID_EXPIRES)
if session_id_expires is None:
# pylint: disable-next=home-assistant-enforce-utcnow
self._session_id_expires = datetime.now(tz=UTC) + timedelta(minutes=15)
else:
self._session_id_expires = datetime.fromisoformat(session_id_expires)
@@ -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)
@@ -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,
@@ -279,7 +279,7 @@ class GardenaBluetoothRemainSensor(GardenaBluetoothEntity, SensorEntity):
super()._handle_coordinator_update()
return
time = datetime.now(UTC) + timedelta(seconds=value) # pylint: disable=home-assistant-enforce-utcnow
time = datetime.now(UTC) + timedelta(seconds=value)
if not self._attr_native_value:
self._attr_native_value = time
super()._handle_coordinator_update()
@@ -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})"
+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": {
@@ -23,9 +23,6 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_HW_VERSION,
ATTR_MODEL,
ATTR_SW_VERSION,
CONF_MAC,
CONF_NAME,
CONF_PASSWORD,
@@ -383,15 +380,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: HuaweiLteConfigEntry) ->
hw_version = router_info.get("HardwareVersion")
sw_version = router_info.get("SoftwareVersion")
if router_info.get("DeviceName"):
device_info[ATTR_MODEL] = router_info["DeviceName"]
device_info[dr.DeviceInfoAttribute.MODEL] = router_info["DeviceName"]
if not sw_version and router.data.get(KEY_DEVICE_BASIC_INFORMATION):
sw_version = router.data[KEY_DEVICE_BASIC_INFORMATION].get(
"SoftwareVersion"
)
if hw_version:
device_info[ATTR_HW_VERSION] = hw_version
device_info[dr.DeviceInfoAttribute.HW_VERSION] = hw_version
if sw_version:
device_info[ATTR_SW_VERSION] = sw_version
device_info[dr.DeviceInfoAttribute.SW_VERSION] = sw_version
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
@@ -4,7 +4,7 @@ from logging import getLogger
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
from aioimmich.assets.models import ImmichAsset
from aioimmich.exceptions import ImmichError, ImmichForbiddenError
from aioimmich.exceptions import ImmichError
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_player import BrowseError, MediaClass
@@ -79,7 +79,7 @@ class ImmichMediaSource(MediaSource):
],
)
async def _async_build_immich( # noqa: C901
async def _async_build_immich(
self, item: MediaSourceItem, entries: list[ConfigEntry]
) -> list[BrowseMediaSource]:
"""Handle browsing different immich instances."""
@@ -137,12 +137,6 @@ class ImmichMediaSource(MediaSource):
LOGGER.debug("Render all albums for %s", entry.title)
try:
albums = await immich_api.albums.async_get_all_albums()
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="missing_api_permission",
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
@@ -164,12 +158,6 @@ class ImmichMediaSource(MediaSource):
LOGGER.debug("Render all tags for %s", entry.title)
try:
tags = await immich_api.tags.async_get_all_tags()
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="missing_api_permission",
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
@@ -190,12 +178,6 @@ class ImmichMediaSource(MediaSource):
LOGGER.debug("Render all people for %s", entry.title)
try:
people = await immich_api.people.async_get_all_people()
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="missing_api_permission",
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
@@ -229,12 +211,6 @@ class ImmichMediaSource(MediaSource):
identifier.collection_id
)
assets = album_info.assets
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="missing_api_permission",
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
@@ -247,12 +223,6 @@ class ImmichMediaSource(MediaSource):
assets = await immich_api.search.async_get_all_by_tag_ids(
[identifier.collection_id]
)
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="missing_api_permission",
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
@@ -265,24 +235,12 @@ class ImmichMediaSource(MediaSource):
assets = await immich_api.search.async_get_all_by_person_ids(
[identifier.collection_id]
)
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="missing_api_permission",
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
elif identifier.collection == "favorites":
LOGGER.debug("Render all assets for favorites collection")
try:
assets = await immich_api.search.async_get_all_favorites()
except ImmichForbiddenError as err:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="missing_api_permission",
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
@@ -102,9 +102,6 @@
"identifier_unresolvable": {
"message": "Could not parse identifier: {identifier}"
},
"missing_api_permission": {
"message": "Missing API permission ({msg})."
},
"not_configured": {
"message": "Immich is not configured."
},
+1 -26
View File
@@ -1,10 +1,7 @@
"""Home Assistant integration for indevolt device."""
from typing import Any
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
@@ -23,28 +20,6 @@ PLATFORMS: list[Platform] = [
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_migrate_entry(hass: HomeAssistant, entry: IndevoltConfigEntry) -> bool:
"""Migrate old entry."""
if entry.version == 1 and entry.minor_version < 2:
# 1.1 -> 1.2: indevolt-api 1.8.3 changed IndevoltBattery.MAIN_HEATING_STATE
# from 9079 to 9080, so migrate affected unique IDs.
@callback
def migrate_unique_id(
entity_entry: er.RegistryEntry,
) -> dict[str, Any] | None:
if entity_entry.unique_id.endswith("_9079"):
return {
"new_unique_id": entity_entry.unique_id.removesuffix("_9079")
+ "_9080"
}
return None
await er.async_migrate_entries(hass, entry.entry_id, migrate_unique_id)
hass.config_entries.async_update_entry(entry, version=1, minor_version=2)
return True
async def async_setup_entry(hass: HomeAssistant, entry: IndevoltConfigEntry) -> bool:
"""Set up indevolt integration entry using given configuration."""
coordinator = IndevoltCoordinator(hass, entry)
@@ -46,8 +46,6 @@ BINARY_SENSORS: Final = (
key=IndevoltSystem.HEATING_STATE,
generation=(1,),
translation_key="electric_heating_state",
on_value=1000,
off_value=1001,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -22,7 +22,6 @@ class IndevoltConfigFlow(ConfigFlow, domain=DOMAIN):
"""Configuration flow for Indevolt integration."""
VERSION = 1
MINOR_VERSION = 2
def __init__(self) -> None:
"""Initialize the config flow."""
@@ -8,6 +8,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["indevolt-api==1.8.3"],
"requirements": ["indevolt-api==1.8.2"],
"zeroconf": [{ "name": "igen_fw*", "type": "_http._tcp.local." }]
}
+1 -13
View File
@@ -939,24 +939,12 @@ class IndevoltSensorEntity(IndevoltEntity, SensorEntity):
@property
def available(self) -> bool:
"""Return False for sensors in a non-applicable state."""
# Check whether device is not in the required energy mode
"""Return False when the device is not in the required energy mode."""
if self.entity_description.energy_mode is not None:
energy_mode = self.coordinator.data.get(IndevoltConfig.READ_ENERGY_MODE)
if energy_mode != self.entity_description.energy_mode:
return False
# Check whether inverter is reporting 0 degrees with heater not active (thus reporting to indicate "idle")
# Pending fix by Indevolt: https://discord.com/channels/1417471269942591571/1510277757689659522
if self.entity_description.key == IndevoltBattery.GEN_1_INVERTER_TEMPERATURE:
inverter_temp = self.coordinator.data.get(
IndevoltBattery.GEN_1_INVERTER_TEMPERATURE
)
heating_state = self.coordinator.data.get(IndevoltSystem.HEATING_STATE)
if inverter_temp == 0 and heating_state != 1000:
return False
return super().available
@property
@@ -339,7 +339,6 @@ class IntegrationSensor(RestoreSensor):
else max_sub_interval
)
self._max_sub_interval_exceeded_callback: CALLBACK_TYPE = lambda *args: None
# pylint: disable-next=home-assistant-enforce-utcnow
self._last_integration_time: datetime = datetime.now(tz=UTC)
self._last_integration_trigger = _IntegrationTrigger.StateEvent
self._attr_suggested_display_precision = round_digits or 2
@@ -499,7 +498,6 @@ class IntegrationSensor(RestoreSensor):
old_timestamp, new_timestamp, old_state, new_state
)
self._last_integration_trigger = _IntegrationTrigger.StateEvent
# pylint: disable-next=home-assistant-enforce-utcnow
self._last_integration_time = datetime.now(tz=UTC)
finally:
# When max_sub_interval exceeds without state change the source is assumed
@@ -608,7 +606,6 @@ class IntegrationSensor(RestoreSensor):
self._update_integral(area)
self.async_write_ha_state()
# pylint: disable-next=home-assistant-enforce-utcnow
self._last_integration_time = datetime.now(tz=UTC)
self._last_integration_trigger = _IntegrationTrigger.TimeElapsed
+4 -4
View File
@@ -9,14 +9,14 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import DATA_CONFIG, DOMAIN
from .const import DATA_CONFIG, IZONE
from .discovery import async_start_discovery_service, async_stop_discovery_service
PLATFORMS = [Platform.CLIMATE]
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
IZONE: vol.Schema(
{
vol.Optional(CONF_EXCLUDE, default=[]): vol.All(
cv.ensure_list, [cv.string]
@@ -32,13 +32,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Register the iZone component config."""
# Check for manually added config, this may exclude some devices
if conf := config.get(DOMAIN):
if conf := config.get(IZONE):
hass.data[DATA_CONFIG] = conf
# Explicitly added in the config file, create a config entry.
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
IZONE, context={"source": config_entries.SOURCE_IMPORT}
)
)
+4 -4
View File
@@ -43,7 +43,7 @@ from .const import (
DISPATCH_CONTROLLER_RECONNECTED,
DISPATCH_CONTROLLER_UPDATE,
DISPATCH_ZONE_UPDATE,
DOMAIN,
IZONE,
)
type _FuncType[_T, **_P, _R] = Callable[Concatenate[_T, _P], _R]
@@ -188,7 +188,7 @@ class ControllerDevice(ClimateEntity):
self._attr_unique_id = controller.device_uid
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, controller.device_uid)},
identifiers={(IZONE, controller.device_uid)},
manufacturer="IZone",
model=controller.sys_type,
name=f"iZone Controller {controller.device_uid}",
@@ -484,12 +484,12 @@ class ZoneDevice(ClimateEntity):
assert controller.unique_id
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, controller.unique_id, zone.index) # type:ignore[arg-type]
(IZONE, controller.unique_id, zone.index) # type:ignore[arg-type]
},
manufacturer="IZone",
model=zone.type.name.title(),
name=zone.name.title(),
via_device=(DOMAIN, controller.unique_id),
via_device=(IZONE, controller.unique_id),
)
async def async_added_to_hass(self) -> None:
@@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_entry_flow
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import DISPATCH_CONTROLLER_DISCOVERED, DOMAIN, TIMEOUT_DISCOVERY
from .const import DISPATCH_CONTROLLER_DISCOVERED, IZONE, TIMEOUT_DISCOVERY
from .discovery import async_start_discovery_service, async_stop_discovery_service
_LOGGER = logging.getLogger(__name__)
@@ -39,4 +39,4 @@ async def _async_has_devices(hass: HomeAssistant) -> bool:
return True
config_entry_flow.register_discovery_flow(DOMAIN, "iZone Aircon", _async_has_devices)
config_entry_flow.register_discovery_flow(IZONE, "iZone Aircon", _async_has_devices)
+1 -1
View File
@@ -1,6 +1,6 @@
"""Constants used by the izone component."""
DOMAIN = "izone"
IZONE = "izone"
DATA_DISCOVERY_SERVICE = "izone_discovery"
DATA_CONFIG = "izone_config"
@@ -52,7 +52,11 @@ _LOGGER = logging.getLogger(__name__)
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
"""Set up Jellyfin media source."""
return JellyfinSource(hass)
# Currently only a single Jellyfin server is supported
entry: JellyfinConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
coordinator = entry.runtime_data
return JellyfinSource(hass, coordinator.api_client, entry)
class JellyfinSource(MediaSource):
@@ -60,28 +64,21 @@ class JellyfinSource(MediaSource):
name: str = "Jellyfin"
def __init__(self, hass: HomeAssistant) -> None:
def __init__(
self, hass: HomeAssistant, client: JellyfinClient, entry: JellyfinConfigEntry
) -> None:
"""Initialize the Jellyfin media source."""
super().__init__(DOMAIN)
self.hass = hass
self.entry: JellyfinConfigEntry
self.client: JellyfinClient
self.api: Any
self.url: str
def _ensure_loaded(self) -> None:
"""Ensure the Jellyfin integration is loaded and set up instance state."""
if not (entries := self.hass.config_entries.async_loaded_entries(DOMAIN)):
raise BrowseError("Jellyfin integration not loaded")
entry: JellyfinConfigEntry = entries[0]
self.hass = hass
self.entry = entry
self.client = entry.runtime_data.api_client
self.api = self.client.jellyfin
self.url = jellyfin_url(self.client, "")
self.client = client
self.api = client.jellyfin
self.url = jellyfin_url(client, "")
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Return a streamable URL and associated mime type."""
self._ensure_loaded()
media_item = await self.hass.async_add_executor_job(
self.api.get_item, item.identifier
)
@@ -97,7 +94,6 @@ class JellyfinSource(MediaSource):
async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource:
"""Return a browsable Jellyfin media source."""
self._ensure_loaded()
if not item.identifier:
return await self._build_libraries()
@@ -70,7 +70,7 @@ class LGTVRS232ConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
port = user_input[CONF_DEVICE]
set_id = int(user_input[CONF_SET_ID])
set_id = user_input[CONF_SET_ID]
self._async_abort_entries_match({CONF_DEVICE: port, CONF_SET_ID: set_id})
error = await _async_attempt_connect(port, set_id)
+2 -2
View File
@@ -53,8 +53,8 @@
"iot_class": "local_polling",
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
"requirements": [
"aiolifx==1.2.2",
"aiolifx==1.2.1",
"aiolifx-effects==0.3.2",
"aiolifx-themes==1.0.4"
"aiolifx-themes==1.0.2"
]
}
+6 -13
View File
@@ -4,7 +4,6 @@ import asyncio
import logging
from typing import TypedDict
import aiohttp
from aiohttp.web import Request
from loqedAPI import loqed
@@ -161,20 +160,14 @@ class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]):
_LOGGER.debug("Webhook URL: %s", webhook_url)
try:
webhooks = await self.lock.getWebhooks()
webhooks = await self.lock.getWebhooks()
webhook_index = next(
(x["id"] for x in webhooks if x["url"] == webhook_url), None
)
webhook_index = next(
(x["id"] for x in webhooks if x["url"] == webhook_url), None
)
if webhook_index:
await self.lock.deleteWebhook(webhook_index)
except (TimeoutError, aiohttp.ClientError) as err:
_LOGGER.warning(
"Could not remove webhook from LOQED bridge; the bridge may be offline. Continuing to unload the entry anyway: %s",
err,
)
if webhook_index:
await self.lock.deleteWebhook(webhook_index)
async def async_cloudhook_generate_url(
@@ -97,11 +97,6 @@ class LunatoneConfigFlow(ConfigFlow, domain=DOMAIN):
self._data[CONF_URL] = url
self.context["title_placeholders"] = {
"model": discovery_info.properties["device"],
"name": discovery_info.name.rsplit(" ", maxsplit=1)[0],
}
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
@@ -12,7 +12,6 @@
"invalid_url": "Failed to connect. Check the URL and if the device is connected to power",
"missing_device_info": "Failed to read device information. Check the network connection of the device"
},
"flow_title": "{name} ({model})",
"step": {
"discovery_confirm": {
"description": "Do you want to set up the Lunatone device at {url}?"
+20 -43
View File
@@ -14,7 +14,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_DISCOVERY,
CONF_PLATFORM,
CONF_PORT,
CONF_PROTOCOL,
SERVICE_RELOAD,
)
@@ -51,7 +50,6 @@ from .client import (
async_subscribe_internal,
publish,
subscribe,
try_connection,
)
from .config import MQTT_BASE_SCHEMA, MQTT_RO_SCHEMA, MQTT_RW_SCHEMA
from .config_integration import CONFIG_SCHEMA_BASE
@@ -81,15 +79,14 @@ from .const import (
CONFIG_ENTRY_VERSION,
DEFAULT_DISCOVERY,
DEFAULT_ENCODING,
DEFAULT_PORT,
DEFAULT_PREFIX,
DEFAULT_PROTOCOL,
DEFAULT_QOS,
DEFAULT_RETAIN,
DOMAIN,
ENTITY_PLATFORMS,
ENTRY_OPTION_FIELDS,
MQTT_CONNECTION_STATE,
PROTOCOL_5,
PROTOCOL_311,
TEMPLATE_ERRORS,
Platform,
@@ -499,45 +496,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Load a config entry."""
mqtt_data: MqttData
if (protocol := entry.data.get(CONF_PROTOCOL, PROTOCOL_311)) != PROTOCOL_5:
# Automatically migrate the broker protocol to v5 if possible
# Can be removed with HA Core 2027.1
new_entry_data = entry.data.copy()
new_entry_data[CONF_PROTOCOL] = PROTOCOL_5
# Try the connection with protocol version 5
# And update the protocol if successful
if await hass.async_add_executor_job(
try_connection,
{CONF_PORT: DEFAULT_PORT} | new_entry_data,
):
hass.config_entries.async_update_entry(
entry,
data=new_entry_data,
)
ir.async_delete_issue(hass, DOMAIN, "protocol_5_migration")
_LOGGER.info(
"The MQTT protocol version was successfully updated to version 5"
)
else:
broker: str = entry.data[CONF_BROKER]
async_create_issue(
hass,
DOMAIN,
"protocol_5_migration",
issue_domain=DOMAIN,
is_fixable=False,
breaks_in_ha_version="2027.1.0",
severity=IssueSeverity.WARNING,
learn_more_url="https://www.home-assistant.io/integrations/mqtt/"
"#mqtt-protocol",
data={
"entry_id": entry.entry_id,
"broker": broker,
"protocol": protocol,
},
translation_placeholders={"broker": broker, "protocol": protocol},
translation_key="protocol_5_migration",
)
if (protocol := entry.data.get(CONF_PROTOCOL, PROTOCOL_311)) != DEFAULT_PROTOCOL:
broker: str = entry.data[CONF_BROKER]
async_create_issue(
hass,
DOMAIN,
"protocol_5_migration",
issue_domain=DOMAIN,
is_fixable=True,
breaks_in_ha_version="2027.1.0",
severity=IssueSeverity.WARNING,
learn_more_url="https://www.home-assistant.io/integrations/mqtt/#mqtt-protocol",
data={
"entry_id": entry.entry_id,
"broker": broker,
"protocol": protocol,
},
translation_placeholders={"broker": broker, "protocol": protocol},
translation_key="protocol_5_migration",
)
async def _setup_client() -> tuple[MqttData, dict[str, Any]]:
"""Set up the MQTT client."""
-37
View File
@@ -9,7 +9,6 @@ from functools import lru_cache, partial
from itertools import chain, groupby
import logging
from operator import attrgetter
import queue
import socket
import ssl
import time
@@ -93,8 +92,6 @@ from .util import EnsureJobAfterCooldown, get_file_path, mqtt_config_entry_enabl
_LOGGER = logging.getLogger(__name__)
MQTT_TIMEOUT = 5
MIN_BUFFER_SIZE = 131072 # Minimum buffer size to use if preferred size fails
PREFERRED_BUFFER_SIZE = 8 * 1024 * 1024 # Set receive buffer size to 8MiB
@@ -436,40 +433,6 @@ class MqttClientSetup:
return self._client
def try_connection(
user_input: dict[str, Any],
) -> bool:
"""Test if we can connect to an MQTT broker."""
mqtt_client_setup = MqttClientSetup(user_input)
mqtt_client_setup.setup()
client = mqtt_client_setup.client
result: queue.Queue[bool] = queue.Queue(maxsize=1)
def on_connect(
_mqttc: mqtt.Client,
_userdata: None,
_connect_flags: mqtt.ConnectFlags,
reason_code: mqtt.ReasonCode,
_properties: mqtt.Properties | None = None,
) -> None:
"""Handle connection result."""
result.put(not reason_code.is_failure)
client.on_connect = on_connect
client.connect_async(user_input[CONF_BROKER], user_input[CONF_PORT])
client.loop_start()
try:
return result.get(timeout=MQTT_TIMEOUT)
except queue.Empty:
return False
finally:
client.disconnect()
client.loop_stop()
class MQTT:
"""Home Assistant MQTT client."""
+39 -1
View File
@@ -8,6 +8,7 @@ from dataclasses import dataclass
from enum import IntEnum
import json
import logging
import queue
from ssl import PROTOCOL_TLS_CLIENT, SSLContext, SSLError
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, cast
@@ -21,6 +22,7 @@ from cryptography.hazmat.primitives.serialization import (
load_pem_private_key,
)
from cryptography.x509 import load_der_x509_certificate, load_pem_x509_certificate
import paho.mqtt.client as mqtt
import voluptuous as vol
import yaml
@@ -141,7 +143,7 @@ from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
from homeassistant.util.unit_conversion import TemperatureConverter
from .addon import get_addon_manager
from .client import try_connection
from .client import MqttClientSetup
from .const import (
ALARM_CONTROL_PANEL_SUPPORTED_FEATURES,
ATTR_PAYLOAD,
@@ -442,6 +444,8 @@ ADDON_SETUP_TIMEOUT_ROUNDS = 5
CONF_CLIENT_KEY_PASSWORD = "client_key_password"
MQTT_TIMEOUT = 5
ADVANCED_OPTIONS = "advanced_options"
SET_CA_CERT = "set_ca_cert"
SET_CLIENT_CERT = "set_client_cert"
@@ -5577,6 +5581,40 @@ async def async_get_broker_settings(
return False
def try_connection(
user_input: dict[str, Any],
) -> bool:
"""Test if we can connect to an MQTT broker."""
mqtt_client_setup = MqttClientSetup(user_input)
mqtt_client_setup.setup()
client = mqtt_client_setup.client
result: queue.Queue[bool] = queue.Queue(maxsize=1)
def on_connect(
_mqttc: mqtt.Client,
_userdata: None,
_connect_flags: mqtt.ConnectFlags,
reason_code: mqtt.ReasonCode,
_properties: mqtt.Properties | None = None,
) -> None:
"""Handle connection result."""
result.put(not reason_code.is_failure)
client.on_connect = on_connect
client.connect_async(user_input[CONF_BROKER], user_input[CONF_PORT])
client.loop_start()
try:
return result.get(timeout=MQTT_TIMEOUT)
except queue.Empty:
return False
finally:
client.disconnect()
client.loop_stop()
def check_certicate_chain() -> str | None:
"""Check the MQTT certificates."""
if client_certificate := get_file_path(CONF_CLIENT_CERT):
@@ -175,10 +175,10 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
self._attr_latitude = None
self._attr_longitude = None
_LOGGER.warning(
"Extra state attributes received at %s and template %s "
"Extra state attributes received at % and template %s "
"contain invalid or incomplete location info. Got %s",
self._config.get(CONF_JSON_ATTRS_TOPIC),
self._config.get(CONF_JSON_ATTRS_TEMPLATE),
self._config.get(CONF_JSON_ATTRS_TOPIC),
extra_state_attributes,
)
@@ -190,11 +190,11 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
self._attr_location_accuracy = gps_accuracy
else:
_LOGGER.warning(
"Extra state attributes received at %s and template %s "
"Extra state attributes received at % and template %s "
"contain invalid GPS accuracy setting, "
"gps_accuracy was set to 0 as the default. Got %s",
self._config.get(CONF_JSON_ATTRS_TOPIC),
self._config.get(CONF_JSON_ATTRS_TEMPLATE),
self._config.get(CONF_JSON_ATTRS_TOPIC),
extra_state_attributes,
)
self._attr_location_accuracy = 0
+1 -1
View File
@@ -530,7 +530,7 @@ class MqttAttributesMixin(Entity):
self._attributes_message_received,
{
"_attr_extra_state_attributes",
"_attr_location_accuracy",
"_attr_gps_accuracy",
"_attr_latitude",
"_attr_location_name",
"_attr_longitude",
+56 -1
View File
@@ -5,10 +5,12 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.repairs import RepairsFlow, RepairsFlowResult
from homeassistant.const import CONF_PORT, CONF_PROTOCOL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from .const import DOMAIN
from .config_flow import try_connection
from .const import DEFAULT_PORT, DOMAIN, PROTOCOL_5
URL_MQTT_BROKER_CONFIGURATION = (
"https://www.home-assistant.io/integrations/mqtt/#broker-configuration"
@@ -53,6 +55,55 @@ class MQTTDeviceEntryMigration(RepairsFlow):
)
class MQTTProtocolV5Migration(RepairsFlow):
"""Handler to migrate to MQTT protocol version 5."""
def __init__(self, entry_id: str, broker: str, protocol: str) -> None:
"""Initialize the flow."""
self.entry_id = entry_id
self.broker = broker
self.protocol = protocol
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> RepairsFlowResult:
"""Handle the first step of a fix flow."""
return await self.async_step_confirm()
async def async_step_confirm(
self, user_input: dict[str, str] | None = None
) -> RepairsFlowResult:
"""Handle the confirm step of a fix flow."""
if user_input is not None:
entry = self.hass.config_entries.async_get_entry(self.entry_id)
if TYPE_CHECKING:
assert entry is not None
new_entry_data = entry.data.copy()
new_entry_data[CONF_PROTOCOL] = PROTOCOL_5
# Try the connection with protocol version 5
if await self.hass.async_add_executor_job(
try_connection,
{CONF_PORT: DEFAULT_PORT} | new_entry_data,
):
self.hass.config_entries.async_update_entry(entry, data=new_entry_data)
return self.async_create_entry(data={})
return self.async_abort(
reason="mqtt_broker_migration_to_v5_failed",
description_placeholders={
"broker": self.broker,
"protocol": self.protocol,
"url_mqtt_broker_configuration": URL_MQTT_BROKER_CONFIGURATION,
},
)
return self.async_show_form(
step_id="confirm",
data_schema=vol.Schema({}),
description_placeholders={"broker": self.broker, "protocol": self.protocol},
)
async def async_create_fix_flow(
hass: HomeAssistant,
issue_id: str,
@@ -62,6 +113,10 @@ async def async_create_fix_flow(
if TYPE_CHECKING:
assert data is not None
entry_id: str = data["entry_id"] # type: ignore[assignment]
if issue_id == "protocol_5_migration":
broker: str = data["broker"] # type: ignore[assignment]
protocol: str = data["protocol"] # type: ignore[assignment]
return MQTTProtocolV5Migration(entry_id, broker, protocol)
subentry_id: str = data["subentry_id"] # type: ignore[assignment]
name: str = data["name"] # type: ignore[assignment]
return MQTTDeviceEntryMigration(
+13 -3
View File
@@ -56,7 +56,7 @@
"keepalive": "A value less than 90 seconds is advised.",
"password": "The password to log in to your MQTT broker.",
"port": "The port your MQTT broker listens to. For example 1883.",
"protocol": "The MQTT protocol version that is used. Note that Home Assistant will silently change to version 5 if your broker supports it.",
"protocol": "The MQTT protocol your broker operates at. For example 3.1.1.",
"set_ca_cert": "Select **Auto** for automatic CA validation, or **Custom** and select **Next** to set a custom CA certificate, to allow validating your MQTT broker's certificate.",
"set_client_cert": "Enable and select **Next** to set a client certificate and private key to authenticate against your MQTT broker.",
"tls_insecure": "Option to ignore validation of your MQTT broker's certificate.",
@@ -1134,8 +1134,18 @@
"title": "Invalid config found for MQTT {domain} item"
},
"protocol_5_migration": {
"description": "The automatic migration to MQTT protocol version 5 failed. The currently configured protocol version for MQTT broker {broker} is {protocol}, but this protocol version is deprecated, and support for it will be removed.\n\nMake sure your broker supports protocol version 5. Update your MQTT broker's connection settings, and restart Home Assistant to fix this issue.",
"title": "MQTT protocol migration failed"
"fix_flow": {
"abort": {
"mqtt_broker_migration_to_v5_failed": "Migrating the broker ({broker}) protocol version from {protocol} to 5 failed, and the migration has been aborted.\n\nYour broker may not support MQTT protocol version 5.\n\nPlease [reconfigure your MQTT broker settings]({url_mqtt_broker_configuration}) or upgrade your broker to support MQTT protocol version 5 to fix this issue."
},
"step": {
"confirm": {
"description": "Home Assistant needs to migrate to MQTT protocol version 5. The currently configured protocol version for broker {broker} is {protocol}. This protocol version is deprecated, and support for it will be removed.\n\nSubmitting this form will attempt to migrate your MQTT broker configuration to use protocol version 5 to fix this issue. If the broker cannot be reached using MQTT protocol version 5, for example because it does not support it, the migration will be aborted.",
"title": "MQTT protocol change required"
}
}
},
"title": "Deprecated MQTT protocol {protocol} in use"
},
"subentry_migration_discovery": {
"fix_flow": {
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["pynintendoauth", "pynintendoparental"],
"quality_scale": "bronze",
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.4.0"]
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.4"]
}
@@ -96,7 +96,7 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [
# DomesticHotWaterProduction/WaterHeatingSystem
OverkizBinarySensorDescription(
key=OverkizState.IO_OPERATING_MODE_CAPABILITIES,
name="Energy demand status",
name="Energy Demand Status",
device_class=BinarySensorDeviceClass.HEAT,
value_fn=lambda state: (
cast(dict, state).get(OverkizCommandParam.ENERGY_DEMAND_STATUS) == 1
@@ -854,9 +854,6 @@ class OverkizCover(OverkizDescriptiveEntity, CoverEntity):
if current_value is None or target_value is None:
return None
if current_value in (_POSITION_MY, _POSITION_UNKNOWN):
return None
return current_value - target_value
@@ -90,5 +90,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["PSNAWP==3.0.3", "pyrate-limiter==4.2.0"]
"requirements": ["PSNAWP==3.0.3", "pyrate-limiter==4.1.0"]
}
@@ -41,7 +41,6 @@ from .const import (
CONF_VMS,
DEFAULT_PORT,
DEFAULT_REALM,
DEFAULT_TIMEOUT,
DEFAULT_VERIFY_SSL,
DOMAIN,
NODE_ONLINE,
@@ -80,14 +79,14 @@ TOKEN_SCHEMA = vol.Schema(
def _get_nodes_data(data: dict[str, Any]) -> list[dict[str, Any]]:
"""Validate the user input and fetch data (sync, for executor)."""
auth_kwargs = (
{
auth_kwargs = {
"password": data.get(CONF_PASSWORD),
}
if data.get(CONF_TOKEN):
auth_kwargs = {
"token_name": data[CONF_TOKEN_ID],
"token_value": data[CONF_TOKEN_SECRET],
}
if data.get(CONF_TOKEN)
else {"password": data.get(CONF_PASSWORD)}
)
data = sanitize_config_entry(data)
try:
client = ProxmoxAPI(
@@ -95,7 +94,6 @@ def _get_nodes_data(data: dict[str, Any]) -> list[dict[str, Any]]:
port=data[CONF_PORT],
user=data[CONF_USERNAME],
verify_ssl=data.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
timeout=DEFAULT_TIMEOUT,
**auth_kwargs,
)
except AuthenticationError as err:
@@ -124,9 +122,6 @@ def _get_nodes_data(data: dict[str, Any]) -> list[dict[str, Any]]:
except requests.exceptions.ConnectionError as err:
raise ProxmoxConnectionError from err
if not nodes:
raise ProxmoxNoNodesFound("No nodes found")
nodes_data: list[dict[str, Any]] = []
for node in nodes:
if node.get("status") != NODE_ONLINE:
@@ -29,7 +29,6 @@ AUTH_METHODS = [AUTH_PAM, AUTH_PVE, AUTH_OTHER]
DEFAULT_PORT = 8006
DEFAULT_REALM = AUTH_PAM
DEFAULT_TIMEOUT = 30
DEFAULT_VERIFY_SSL = True
TYPE_VM = 0
TYPE_CONTAINER = 1
@@ -29,7 +29,6 @@ from .const import (
CONF_NODE,
CONF_TOKEN_ID,
CONF_TOKEN_SECRET,
DEFAULT_TIMEOUT,
DEFAULT_VERIFY_SSL,
DOMAIN,
NODE_ONLINE,
@@ -218,7 +217,6 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
port=data[CONF_PORT],
user=data[CONF_USERNAME],
verify_ssl=data.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
timeout=DEFAULT_TIMEOUT,
**auth_kwargs,
)
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/radio_frequency",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["rf-protocols==4.0.1"]
"requirements": ["rf-protocols==4.0.0"]
}
@@ -22,12 +22,14 @@ _LOGGER = logging.getLogger(__name__)
USER_SCHEMA = vol.Schema(
{
vol.Required(RenaultConfigurationKeys.LOCALE): vol.In(AVAILABLE_LOCALES.keys()),
vol.Required(RenaultConfigurationKeys.USERNAME): str,
vol.Required(RenaultConfigurationKeys.PASSWORD): str,
vol.Required(RenaultConfigurationKeys.LOCALE.value): vol.In(
AVAILABLE_LOCALES.keys()
),
vol.Required(RenaultConfigurationKeys.USERNAME.value): str,
vol.Required(RenaultConfigurationKeys.PASSWORD.value): str,
}
)
REAUTH_SCHEMA = vol.Schema({vol.Required(RenaultConfigurationKeys.PASSWORD): str})
REAUTH_SCHEMA = vol.Schema({vol.Required(RenaultConfigurationKeys.PASSWORD.value): str})
class RenaultFlowHandler(ConfigFlow, domain=DOMAIN):
@@ -120,9 +122,9 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN):
step_id="kamereon",
data_schema=vol.Schema(
{
vol.Required(RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID): vol.In(
accounts
)
vol.Required(
RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID.value
): vol.In(accounts)
}
),
)
+7 -7
View File
@@ -1,20 +1,20 @@
"""Constants for the Renault component."""
from typing import Final
from enum import StrEnum
from homeassistant.const import Platform
DOMAIN = "renault"
class RenaultConfigurationKeys:
class RenaultConfigurationKeys(StrEnum):
"""Configuration keys."""
KAMEREON_ACCOUNT_ID: Final = "kamereon_account_id"
LOCALE: Final = "locale"
LOGIN_TOKEN: Final = "login_token"
PASSWORD: Final = "password"
USERNAME: Final = "username"
LOCALE = "locale"
KAMEREON_ACCOUNT_ID = "kamereon_account_id"
LOGIN_TOKEN = "login_token"
USERNAME = "username"
PASSWORD = "password"
# normal number of allowed calls per hour to the API
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
"quality_scale": "silver",
"requirements": ["renault-api==0.5.11"]
"requirements": ["renault-api==0.5.10"]
}
+1 -1
View File
@@ -168,7 +168,7 @@ async def async_setup_entry(
hass.config_entries.async_update_entry(config_entry, data=data)
# If camera WAN blocked, firmware check fails and takes long, do not prevent setup
now = datetime.now(UTC) # pylint: disable=home-assistant-enforce-utcnow
now = datetime.now(UTC)
check_time = timedelta(seconds=check_time_sec)
delta_midnight = now - now.replace(hour=0, minute=0, second=0, microsecond=0)
firmware_check_delay = check_time - delta_midnight
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/risco",
"iot_class": "local_push",
"loggers": ["pyrisco"],
"requirements": ["pyrisco==0.8.0"]
"requirements": ["pyrisco==0.7.0"]
}
+1 -5
View File
@@ -42,8 +42,4 @@ class SchlageEntity(CoordinatorEntity[SchlageDataUpdateCoordinator]):
@property
def available(self) -> bool:
"""Return if entity is available."""
return (
super().available
and self.device_id in self.coordinator.data.locks
and self._lock.connected
)
return super().available and self.device_id in self.coordinator.data.locks
@@ -165,5 +165,5 @@ class SensoterraEntity(CoordinatorEntity[SensoterraCoordinator], SensorEntity):
return False
# Expire sensor if no update within the last few days.
expiration = datetime.now(UTC) - timedelta(days=SENSOR_EXPIRATION_DAYS) # pylint: disable=home-assistant-enforce-utcnow
expiration = datetime.now(UTC) - timedelta(days=SENSOR_EXPIRATION_DAYS)
return sensor.timestamp >= expiration
+5 -8
View File
@@ -244,8 +244,7 @@ def async_restore_rpc_attribute_entities(
sensor_class: Callable,
) -> None:
"""Restore RPC attributes entities."""
entities: list[Entity] = []
sleep_period = config_entry.data[CONF_SLEEP_PERIOD]
entities = []
ent_reg = er.async_get(hass)
entries = er.async_entries_for_config_entry(ent_reg, config_entry.entry_id)
@@ -260,13 +259,11 @@ def async_restore_rpc_attribute_entities(
attribute = entry.unique_id.split("-")[-1]
if description := sensors.get(attribute):
entity_class = get_entity_class(sensor_class, description)
if sleep_period:
entities.append(
entity_class(coordinator, key, attribute, description, entry)
entities.append(
get_entity_class(sensor_class, description)(
coordinator, key, attribute, description, entry
)
else:
entities.append(entity_class(coordinator, key, attribute, description))
)
if not entities:
return
@@ -63,7 +63,6 @@ def _utc_minutes_to_time(utc_minutes: int, timezone: tzinfo) -> time:
hour -= 24
minute = utc_minutes % 60
try:
# pylint: disable-next=home-assistant-enforce-utcnow
utc = datetime.now(UTC).replace(
hour=hour, minute=minute, second=0, microsecond=0
)
+1 -17
View File
@@ -6,7 +6,6 @@ from typing import Any
import switchbot
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import BluetoothReachabilityIntent
from homeassistant.components.sensor import ConfigType
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -311,11 +310,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: SwitchbotConfigEntry) ->
translation_placeholders={
"sensor_type": entry.data[CONF_SENSOR_TYPE],
"address": entry.data[CONF_ADDRESS],
"reason": bluetooth.async_address_reachability_diagnostics(
hass,
entry.data[CONF_ADDRESS].upper(),
BluetoothReachabilityIntent.CONNECTION,
),
},
)
@@ -337,17 +331,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SwitchbotConfigEntry) ->
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_not_found_error",
translation_placeholders={
"sensor_type": sensor_type,
"address": address,
"reason": bluetooth.async_address_reachability_diagnostics(
hass,
address.upper(),
BluetoothReachabilityIntent.CONNECTION
if connectable
else BluetoothReachabilityIntent.PASSIVE_ADVERTISEMENT,
),
},
translation_placeholders={"sensor_type": sensor_type, "address": address},
)
cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice)
@@ -384,7 +384,7 @@
"message": "The device ID {device_id} does not belong to SwitchBot integration."
},
"device_not_found_error": {
"message": "Could not find Switchbot {sensor_type} with address {address}: {reason}"
"message": "Could not find Switchbot {sensor_type} with address {address}"
},
"device_without_config_entry": {
"message": "The device ID {device_id} is not associated with a config entry."
@@ -31,7 +31,6 @@ class TedeeBinarySensorEntityDescription(
is_on_fn: Callable[[TedeeLock], bool | None]
supported_fn: Callable[[TedeeLock], bool] = lambda _: True
available_fn: Callable[[TedeeLock], bool] = lambda _: True
always_available: bool = False
ENTITIES: tuple[TedeeBinarySensorEntityDescription, ...] = (
@@ -76,13 +75,6 @@ ENTITIES: tuple[TedeeBinarySensorEntityDescription, ...] = (
not in [TedeeDoorState.UNCALIBRATED, TedeeDoorState.DISCONNECTED]
),
),
TedeeBinarySensorEntityDescription(
key="connectivity",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
is_on_fn=lambda lock: lock.is_connected,
entity_category=EntityCategory.DIAGNOSTIC,
always_available=True,
),
)
@@ -119,6 +111,4 @@ class TedeeBinarySensorEntity(TedeeDescriptionEntity, BinarySensorEntity):
@property
def available(self) -> bool:
"""Return true if the binary sensor is available."""
if self.entity_description.always_available:
return True
return self.entity_description.available_fn(self._lock) and super().available
-5
View File
@@ -36,11 +36,6 @@ class TedeeEntity(CoordinatorEntity[TedeeApiCoordinator]):
via_device=(DOMAIN, coordinator.bridge.serial),
)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self._lock.is_connected
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
+5 -1
View File
@@ -89,7 +89,11 @@ class TedeeLockEntity(TedeeEntity, LockEntity):
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self._lock.state != TedeeLockState.UNCALIBRATED
return (
super().available
and self._lock.is_connected
and self._lock.state != TedeeLockState.UNCALIBRATED
)
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the door."""
@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/thread",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.10.0", "pyroute2==0.9.6"],
"requirements": ["python-otbr-api==2.10.0", "pyroute2==0.7.5"],
"single_config_entry": true,
"zeroconf": ["_meshcop._udp.local."]
}
+2
View File
@@ -102,12 +102,14 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity):
"""Return the current state of the burner."""
return {"heating_type": self.coordinator.data.agreement.heating_type}
# pylint: disable-next=home-assistant-action-swallowed-exception
@toon_exception_handler
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Change the setpoint of the thermostat."""
temperature = kwargs.get(ATTR_TEMPERATURE)
await self.coordinator.toon.set_current_setpoint(temperature)
# pylint: disable-next=home-assistant-action-swallowed-exception
@toon_exception_handler
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
+8 -12
View File
@@ -1,15 +1,15 @@
"""Helpers for Toon."""
from collections.abc import Callable, Coroutine
import logging
from typing import Any, Concatenate
from toonapi import ToonConnectionError, ToonError
from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN
from .entity import ToonEntity
_LOGGER = logging.getLogger(__name__)
def toon_exception_handler[_ToonEntityT: ToonEntity, **_P](
func: Callable[Concatenate[_ToonEntityT, _P], Coroutine[Any, Any, None]],
@@ -17,24 +17,20 @@ def toon_exception_handler[_ToonEntityT: ToonEntity, **_P](
"""Decorate Toon calls to handle Toon exceptions.
A decorator that wraps the passed in function, catches Toon errors,
and raises a translated ``HomeAssistantError``.
and handles the availability of the device in the data coordinator.
"""
async def handler(self: _ToonEntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:
try:
await func(self, *args, **kwargs)
self.coordinator.async_update_listeners()
except ToonConnectionError as error:
_LOGGER.error("Error communicating with API: %s", error)
self.coordinator.last_update_success = False
self.coordinator.async_update_listeners()
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="communication_error",
) from error
except ToonError as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="invalid_response",
) from error
_LOGGER.error("Invalid response from API: %s", error)
return handler
@@ -34,12 +34,6 @@
}
},
"exceptions": {
"communication_error": {
"message": "An error occurred while communicating with the Toon device."
},
"invalid_response": {
"message": "Received an invalid response from the Toon device."
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
+4
View File
@@ -60,6 +60,7 @@ class ToonSwitch(ToonEntity, SwitchEntity):
class ToonProgramSwitch(ToonSwitch, ToonDisplayDeviceEntity):
"""Defines a Toon program switch."""
# pylint: disable-next=home-assistant-action-swallowed-exception
@toon_exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the Toon program switch."""
@@ -67,6 +68,7 @@ class ToonProgramSwitch(ToonSwitch, ToonDisplayDeviceEntity):
ACTIVE_STATE_AWAY, PROGRAM_STATE_OFF
)
# pylint: disable-next=home-assistant-action-swallowed-exception
@toon_exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the Toon program switch."""
@@ -78,6 +80,7 @@ class ToonProgramSwitch(ToonSwitch, ToonDisplayDeviceEntity):
class ToonHolidayModeSwitch(ToonSwitch, ToonDisplayDeviceEntity):
"""Defines a Toon Holiday mode switch."""
# pylint: disable-next=home-assistant-action-swallowed-exception
@toon_exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the Toon holiday mode switch."""
@@ -85,6 +88,7 @@ class ToonHolidayModeSwitch(ToonSwitch, ToonDisplayDeviceEntity):
ACTIVE_STATE_AWAY, PROGRAM_STATE_ON
)
# pylint: disable-next=home-assistant-action-swallowed-exception
@toon_exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the Toon holiday mode switch."""
+1 -2
View File
@@ -3,8 +3,7 @@
from pathlib import Path
from typing import Any
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
from tuya_device_handlers.devices import register_tuya_quirks
from tuya_device_handlers.devices import TUYA_QUIRKS_REGISTRY, register_tuya_quirks
from tuya_sharing import (
CustomerDevice,
Manager,
+1 -1
View File
@@ -44,7 +44,7 @@
"iot_class": "cloud_push",
"loggers": ["tuya_sharing"],
"requirements": [
"tuya-device-handlers==0.0.22",
"tuya-device-handlers==0.0.21",
"tuya-device-sharing-sdk==0.2.8"
]
}
-5
View File
@@ -112,8 +112,3 @@ class WLEDUpdateEntity(WLEDEntity, UpdateEntity):
version = cast(str, self.latest_version)
await self.coordinator.wled.upgrade(version=version)
await self.coordinator.async_refresh()
async def async_update(self) -> None:
"""Update the entity."""
await super().async_update()
await self.releases_coordinator.async_request_refresh()
+1 -1
View File
@@ -14,5 +14,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["socketio", "engineio", "yalexs"],
"requirements": ["yalexs==9.2.7", "yalexs-ble==3.3.0"]
"requirements": ["yalexs==9.2.1", "yalexs-ble==3.3.0"]
}
@@ -44,7 +44,7 @@
"name": "Rain delay"
},
"water_hammer_duration": {
"name": "Water hammer duration"
"name": "Water hammer reduction"
},
"zone_delay": {
"name": "Zone delay"
@@ -72,7 +72,6 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]):
device_reporttime = device_state_resp.data.get("reportAt")
if device_reporttime is not None:
rpt_time_delta = (
# pylint: disable-next=home-assistant-enforce-utcnow
datetime.now(tz=UTC).replace(tzinfo=None)
- datetime.strptime(device_reporttime, "%Y-%m-%dT%H:%M:%S.%fZ")
).total_seconds()
+1 -1
View File
@@ -23,7 +23,7 @@
"universal_silabs_flasher",
"serialx"
],
"requirements": ["zha==1.4.1"],
"requirements": ["zha==1.4.0"],
"usb": [
{
"description": "*2652*",
+18 -1
View File
@@ -8,7 +8,7 @@ from enum import StrEnum
from functools import lru_cache
import logging
import time
from typing import TYPE_CHECKING, Any, Literal, TypedDict, Unpack
from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict, Unpack
import attr
from yarl import URL
@@ -80,6 +80,23 @@ class DeviceEntryDisabler(StrEnum):
USER = "user"
class DeviceInfoAttribute:
"""Device info attributes."""
CONFIGURATION_URL: Final = "configuration_url"
CONNECTIONS: Final = "connections"
IDENTIFIERS: Final = "identifiers"
HW_VERSION: Final = "hw_version"
MANUFACTURER: Final = "manufacturer"
MODEL: Final = "model"
MODEL_ID: Final = "model_id"
NAME: Final = "name"
SERIAL_NUMBER: Final = "serial_number"
SUGGESTED_AREA: Final = "suggested_area"
SW_VERSION: Final = "sw_version"
VIA_DEVICE: Final = "via_device"
class DeviceInfo(TypedDict, total=False):
"""Entity device information for device registry."""
+1 -1
View File
@@ -35,7 +35,7 @@ file-read-backwards==2.0.0
fnv-hash-fast==2.0.3
go2rtc-client==0.4.0
ha-ffmpeg==3.2.2
habluetooth==6.8.0
habluetooth==6.7.9
hass-nabucasa==2.2.0
hassil==3.5.0
home-assistant-bluetooth==2.0.0
-14
View File
@@ -103,7 +103,6 @@ Every check has a code following the
| `W7420` | [`home-assistant-tests-direct-platform-async-setup-entry`](#w7420-home-assistant-tests-direct-platform-async-setup-entry) | Tests should not call a platform's `async_setup_entry` directly |
| `W7421` | [`home-assistant-tests-direct-async-migrate-entry`](#w7421-home-assistant-tests-direct-async-migrate-entry) | Tests should not call an integration's `async_migrate_entry` directly |
| `W7422` | [`home-assistant-tests-direct-async-setup`](#w7422-home-assistant-tests-direct-async-setup) | Tests should not call an integration's `async_setup` directly |
| `C7414` | [`home-assistant-enforce-utcnow`](#c7414-home-assistant-enforce-utcnow) | Use `homeassistant.util.dt.utcnow` instead of `datetime.now(UTC)` |
## `home_assistant_logger` checker
@@ -400,16 +399,3 @@ the setup through the normal pipeline:
`homeassistant.setup`.
See [epic #79](https://github.com/home-assistant/epics/issues/79).
## `home_assistant_enforce_utcnow` checker
Ensures the Home Assistant helper is used to get the current UTC time.
### `C7414`: `home-assistant-enforce-utcnow`
Use `homeassistant.util.dt.utcnow()` instead of `datetime.datetime.now(UTC)`.
The helper is implemented as
`functools.partial(datetime.datetime.now, UTC)` and avoids the global
lookup of `UTC` on every call, while keeping the codebase consistent in
how the current UTC time is obtained.
@@ -1,123 +0,0 @@
"""Checker that enforces ``homeassistant.util.dt.utcnow`` over ``datetime.now(UTC)``.
Home Assistant exposes ``homeassistant.util.dt.utcnow`` -- a thin wrapper around
``datetime.datetime.now(UTC)`` implemented as a ``functools.partial``. Using the
helper avoids the per-call global lookup of ``UTC`` and keeps the codebase
consistent in how the current UTC time is obtained.
"""
from astroid import nodes
from pylint.checkers import BaseChecker
from pylint.lint import PyLinter
# ``homeassistant.util.dt`` defines ``utcnow`` itself, so it must call
# ``datetime.datetime.now(UTC)`` directly.
_SKIP_MODULES = frozenset({"homeassistant.util.dt"})
def _attribute_path(node: nodes.NodeNG) -> tuple[str, ...] | None:
"""Return the dotted-name path of an Attribute/Name chain, or ``None``."""
parts: list[str] = []
while isinstance(node, nodes.Attribute):
parts.append(node.attrname)
node = node.expr
if not isinstance(node, nodes.Name):
return None
parts.append(node.name)
return tuple(reversed(parts))
def _is_zoneinfo_utc(node: nodes.NodeNG) -> bool:
"""Return True if *node* is ``ZoneInfo("UTC")`` or ``*.ZoneInfo("UTC")``."""
match node:
case nodes.Call(
func=nodes.Name(name="ZoneInfo") | nodes.Attribute(attrname="ZoneInfo"),
args=[nodes.Const(value="UTC")],
keywords=[],
):
return True
return False
class HassEnforceUtcnowChecker(BaseChecker):
"""Checker that flags ``datetime.now(UTC)`` calls."""
name = "home_assistant_enforce_utcnow"
priority = -1
msgs = {
"C7414": (
"Use `homeassistant.util.dt.utcnow()` instead of `datetime.now(UTC)`",
"home-assistant-enforce-utcnow",
"Used when ``datetime.datetime.now(UTC)`` is called. Use the "
"``homeassistant.util.dt.utcnow`` helper instead -- it is "
"implemented as ``functools.partial(datetime.datetime.now, UTC)`` "
"and avoids the global lookup of ``UTC`` on every call.",
),
}
options = ()
_enabled: bool
_datetime_class_paths: set[tuple[str, ...]]
_utc_paths: set[tuple[str, ...]]
def visit_module(self, node: nodes.Module) -> None:
"""Collect ``datetime`` bindings introduced by module-level imports."""
self._datetime_class_paths = set()
self._utc_paths = set()
self._enabled = node.name not in _SKIP_MODULES
if not self._enabled:
return
for stmt in node.body:
match stmt:
case nodes.ImportFrom(modname="datetime", names=names):
for name, alias in names:
local = alias or name
match name:
case "datetime":
self._datetime_class_paths.add((local,))
case "UTC":
self._utc_paths.add((local,))
case "timezone":
self._utc_paths.add((local, "utc"))
case nodes.Import(names=names):
for name, alias in names:
if name != "datetime":
continue
local = alias or name
self._datetime_class_paths.add((local, "datetime"))
self._utc_paths.add((local, "UTC"))
self._utc_paths.add((local, "timezone", "utc"))
def visit_call(self, node: nodes.Call) -> None:
"""Check for ``datetime.now(UTC)`` calls."""
if not self._enabled:
return
match node:
case nodes.Call(
func=nodes.Attribute(attrname="now", expr=expr),
args=[arg],
keywords=[],
):
pass
case nodes.Call(
func=nodes.Attribute(attrname="now", expr=expr),
args=[],
keywords=[nodes.Keyword(arg="tz", value=arg)],
):
pass
case _:
return
if _attribute_path(expr) not in self._datetime_class_paths:
return
if _attribute_path(arg) not in self._utc_paths and not _is_zoneinfo_utc(arg):
return
self.add_message("home-assistant-enforce-utcnow", node=node)
def register(linter: PyLinter) -> None:
"""Register the checker."""
linter.register_checker(HassEnforceUtcnowChecker(linter))
+64 -8
View File
@@ -654,7 +654,42 @@ required-version = ">=0.15.14"
select = [
"A001", # Variable {name} is shadowing a Python builtin
"ASYNC", # flake8-async
"B", # flake8-bugbear
"B002", # Python does not support the unary prefix increment
"B003", # Assigning to os.environ doesn't clear the environment
"B004", # Using hasattr(x, "__call__") instead of callable(x)
"B005", # Using .strip() with multi-character strings is misleading
"B006", # Do not use mutable data structures for argument defaults
"B007", # Loop control variable {name} not used within loop body
"B009", # Do not call getattr with a constant attribute value. It is not any safer than normal property access.
"B010", # Do not call setattr with a constant attribute value. It is not any safer than normal property access.
"B011", # Do not call assert False since python -O removes these calls
"B012", # Use of break/continue/return inside a finally block
"B013", # A length-one tuple literal is redundant in exception handlers
"B014", # Exception handler with duplicate exception
"B015", # Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it.
"B016", # Cannot raise a literal. Did you intend to return it or raise an exception?
"B017", # pytest.raises(BaseException) should be considered evil
"B018", # Found useless attribute access. Either assign it to a variable or remove it.
"B020", # Loop control variable overrides iterable it iterates
"B021", # f-string used as docstring
"B022", # No arguments passed to contextlib.suppress
"B023", # Function definition does not bind loop variable {name}
"B024", # `{name}` is an abstract base class, but it has no abstract methods or properties
"B025", # try-except* block with duplicate exception {name}
"B026", # Star-arg unpacking after a keyword argument is strongly discouraged
"B027", # Empty method in abstract base class without abstract decorator
"B028", # No explicit stacklevel keyword argument found in warnings.warn
"B029", # Using except (): with an empty tuple is a no-op
"B030", # Except handlers should only be exception classes
"B031", # Using the result of the groupby() generator
"B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)?
"B033", # Set should not contain duplicate items
"B034", # re.sub/subn/split should pass count/maxsplit as keyword arguments
"B035", # Dictionary comprehension uses static key
"B039", # ContextVar with mutable literal or function call as default
"B904", # Use raise from to specify exception cause
"B905", # zip() without an explicit strict= parameter
"B911", # itertools.batched() without explicit strict= parameter
"BLE",
"C", # complexity
"COM818", # Trailing comma on bare tuple prohibited
@@ -686,7 +721,34 @@ select = [
"PYI", # flake8-pyi
"RET", # flake8-return
"RSE", # flake8-raise
"RUF", # Ruff-specific rules (see `ignore` for exclusions)
"RUF005", # Consider iterable unpacking instead of concatenation
"RUF006", # Store a reference to the return value of asyncio.create_task
"RUF007", # Prefer itertools.pairwise() over zip() when iterating over successive pairs
"RUF008", # Do not use mutable default values for dataclass attributes
"RUF010", # Use explicit conversion flag
"RUF013", # PEP 484 prohibits implicit Optional
"RUF016", # Slice in indexed access to type {value_type} uses type {index_type} instead of an integer
"RUF017", # Avoid quadratic list summation
"RUF018", # Avoid assignment expressions in assert statements
"RUF019", # Unnecessary key check before dictionary access
"RUF020", # {never_like} | T is equivalent to T
"RUF021", # Parenthesize a and b expressions when chaining and and or together, to make the precedence clear
"RUF022", # Sort __all__
"RUF023", # Sort __slots__
"RUF024", # Do not pass mutable objects as values to dict.fromkeys
"RUF026", # default_factory is a positional-only argument to defaultdict
"RUF030", # print() call in assert statement is likely unintentional
"RUF032", # Decimal() called with float literal argument
"RUF033", # __post_init__ method with argument defaults
"RUF034", # Useless if-else condition
"RUF037", # Unnecessary empty iterable within deque call
"RUF046", # Unnecessary cast to int
"RUF051", # Use dict.pop(key, None) instead of if-key-in-dict-del
"RUF057", # Unnecessary round call
"RUF059", # unused-unpacked-variable
"RUF100", # Unused `noqa` directive
"RUF101", # noqa directives that use redirected rule codes
"RUF200", # Failed to parse pyproject.toml: {message}
"S107", # Possible hardcoded password assigned to function default
"S102", # Use of exec detected
"S103", # bad-file-permissions
@@ -724,8 +786,6 @@ ignore = [
"ASYNC109", # Async function definition with a `timeout` parameter Use `asyncio.timeout` instead
"ASYNC110", # Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` loop
"ASYNC240", # Use an async function for entering the file system
"B008", # Do not perform function call in argument defaults; commonly used in Home Assistant (e.g. cv.* validators)
"B019", # Use of functools.lru_cache or functools.cache on methods can lead to memory leaks
"D202", # No blank lines allowed after function docstring
"D203", # 1 blank line required before class docstring
"D213", # Multi-line docstring summary should start at the second line
@@ -748,11 +808,7 @@ ignore = [
"RUF001", # String contains ambiguous unicode character.
"RUF002", # Docstring contains ambiguous unicode character.
"RUF003", # Comment contains ambiguous unicode character.
"RUF009", # Do not perform function call in dataclass defaults
"RUF012", # Mutable class attributes should be annotated with typing.ClassVar
"RUF015", # Prefer next(...) over single element slice
"RUF043", # Pattern passed to match= contains metacharacters but is neither escaped nor raw
"RUF061", # Use context-manager form of pytest.raises()
"SIM102", # Use a single if statement instead of nested if statements
"SIM103", # Return the condition {condition} directly
"SIM108", # Use ternary operator {contents} instead of if-else-block
+1 -1
View File
@@ -47,7 +47,7 @@ python-slugify==8.0.4
PyTurboJPEG==1.8.3
PyYAML==6.0.3
requests==2.34.2
rf-protocols==4.0.1
rf-protocols==4.0.0
securetar==2026.4.1
SQLAlchemy==2.0.49
standard-aifc==3.13.0
+13 -13
View File
@@ -318,10 +318,10 @@ aiolichess==1.3.0
aiolifx-effects==0.3.2
# homeassistant.components.lifx
aiolifx-themes==1.0.4
aiolifx-themes==1.0.2
# homeassistant.components.lifx
aiolifx==1.2.2
aiolifx==1.2.1
# homeassistant.components.lookin
aiolookin==1.0.0
@@ -1213,7 +1213,7 @@ ha-xthings-cloud==1.0.5
habiticalib==0.4.7
# homeassistant.components.bluetooth
habluetooth==6.8.0
habluetooth==6.7.9
# homeassistant.components.hanna
hanna-cloud==0.0.7
@@ -1350,7 +1350,7 @@ imgw_pib==2.2.0
incomfort-client==0.7.0
# homeassistant.components.indevolt
indevolt-api==1.8.3
indevolt-api==1.8.2
# homeassistant.components.influxdb
influxdb-client==1.50.0
@@ -2367,7 +2367,7 @@ pynina==1.0.2
pynintendoauth==1.0.2
# homeassistant.components.nintendo_parental_controls
pynintendoparental==2.4.0
pynintendoparental==2.3.4
# homeassistant.components.nobo_hub
pynobo==1.9.0
@@ -2477,7 +2477,7 @@ pyrail==0.4.1
pyrainbird==6.3.0
# homeassistant.components.playstation_network
pyrate-limiter==4.2.0
pyrate-limiter==4.1.0
# homeassistant.components.recswitch
pyrecswitch==1.0.2
@@ -2486,13 +2486,13 @@ pyrecswitch==1.0.2
pyrepetierng==0.1.0
# homeassistant.components.risco
pyrisco==0.8.0
pyrisco==0.7.0
# homeassistant.components.rituals_perfume_genie
pyrituals==0.0.7
# homeassistant.components.thread
pyroute2==0.9.6
pyroute2==0.7.5
# homeassistant.components.rympro
pyrympro==0.0.9
@@ -2872,7 +2872,7 @@ refoss-ha==1.2.5
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.5.11
renault-api==0.5.10
# homeassistant.components.renson
renson-endura-delta==1.7.2
@@ -2881,7 +2881,7 @@ renson-endura-delta==1.7.2
reolink-aio==0.20.0
# homeassistant.components.radio_frequency
rf-protocols==4.0.1
rf-protocols==4.0.0
# homeassistant.components.idteck_prox
rfk101py==0.0.1
@@ -3207,7 +3207,7 @@ ttls==1.8.3
ttn_client==1.3.0
# homeassistant.components.tuya
tuya-device-handlers==0.0.22
tuya-device-handlers==0.0.21
# homeassistant.components.tuya
tuya-device-sharing-sdk==0.2.8
@@ -3403,7 +3403,7 @@ yalexs-ble==3.3.0
# homeassistant.components.august
# homeassistant.components.yale
yalexs==9.2.7
yalexs==9.2.1
# homeassistant.components.yeelight
yeelight==0.7.16
@@ -3442,7 +3442,7 @@ zeroconf==0.149.16
zeversolar==0.3.2
# homeassistant.components.zha
zha==1.4.1
zha==1.4.0
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13
+1 -1
View File
@@ -27,7 +27,7 @@ pytest-aiohttp==1.1.0
pytest-cov==7.1.0
pytest-freezer==0.4.9
pytest-github-actions-annotate-failures==0.4.0
pytest-socket==0.8.0
pytest-socket==0.7.0
pytest-sugar==1.1.1
pytest-timeout==2.4.0
pytest-unordered==0.7.0
+2 -2
View File
@@ -492,7 +492,7 @@ def async_fire_time_changed_exact(
approach, as this is only for testing.
"""
if datetime_ is None:
utc_datetime = datetime.now(UTC) # pylint: disable=home-assistant-enforce-utcnow
utc_datetime = datetime.now(UTC)
else:
utc_datetime = dt_util.as_utc(datetime_)
@@ -515,7 +515,7 @@ def async_fire_time_changed(
for an exact microsecond, use async_fire_time_changed_exact.
"""
if datetime_ is None:
utc_datetime = datetime.now(UTC) # pylint: disable=home-assistant-enforce-utcnow
utc_datetime = datetime.now(UTC)
else:
utc_datetime = dt_util.as_utc(datetime_)
+69 -12
View File
@@ -286,20 +286,37 @@ async def test_generate_data_content_type(
mock_ai_task_entity: MockAITaskEntity,
) -> None:
"""Test that user-provided content type of an attachment is respected."""
with patch(
"homeassistant.components.media_source.async_resolve_media",
return_value=media_source.PlayMedia(
url="http://example.com/test.png", # jpeg image saved as png
mime_type="image/png",
path=Path("/media/test.png"),
),
) as mock_resolve_media:
with (
patch( # Intentionally broken content type
"homeassistant.components.camera.async_get_image",
return_value=Image(content_type="image/png", content=b"fake_camera_jpeg"),
) as mock_get_camera_image,
patch( # Same
"homeassistant.components.image.async_get_image",
return_value=Image(content_type="image/png", content=b"fake_image_jpeg"),
) as mock_get_image_image,
patch(
"homeassistant.components.media_source.async_resolve_media",
return_value=media_source.PlayMedia(
url="http://example.com/test.png", # jpeg image saved as png
mime_type="image/png",
path=Path("/media/test.png"),
),
) as mock_resolve_media,
):
await async_generate_data(
hass,
task_name="Test Task",
entity_id=TEST_ENTITY_ID,
instructions="Describe this image",
instructions="Describe these images",
attachments=[
{ # supply corrected content type from the user input
"media_content_id": "media-source://camera/camera.front_door",
"media_content_type": "image/jpeg",
},
{ # User did not provide content type, fallback to the integration
"media_content_id": "media-source://image/image.floorplan",
},
{
"media_content_id": "media-source://media_player/test.png",
"media_content_type": "image/jpeg",
@@ -307,7 +324,9 @@ async def test_generate_data_content_type(
],
)
# Verify the method was called
# Verify both methods were called
mock_get_camera_image.assert_called_once_with(hass, "camera.front_door")
mock_get_image_image.assert_called_once_with(hass, "image.floorplan")
mock_resolve_media.assert_called_once_with(
hass, "media-source://media_player/test.png", None
)
@@ -316,9 +335,47 @@ async def test_generate_data_content_type(
assert len(mock_ai_task_entity.mock_generate_data_tasks) == 1
task = mock_ai_task_entity.mock_generate_data_tasks[0]
assert task.attachments is not None
assert len(task.attachments) == 1
assert len(task.attachments) == 3
media_attachment = task.attachments[0]
# Check camera attachment
camera_attachment = task.attachments[0]
assert (
camera_attachment.media_content_id == "media-source://camera/camera.front_door"
)
assert camera_attachment.mime_type == "image/jpeg"
assert isinstance(camera_attachment.path, Path)
assert camera_attachment.path.suffix == ".png" # This is fine
# Verify camera snapshot content
assert camera_attachment.path.exists()
content = await hass.async_add_executor_job(camera_attachment.path.read_bytes)
assert content == b"fake_camera_jpeg"
# Check image attachment
image_attachment = task.attachments[1]
assert image_attachment.media_content_id == "media-source://image/image.floorplan"
assert image_attachment.mime_type == "image/png"
assert isinstance(image_attachment.path, Path)
assert image_attachment.path.suffix == ".png"
# Verify image snapshot content
assert image_attachment.path.exists()
content = await hass.async_add_executor_job(image_attachment.path.read_bytes)
assert content == b"fake_image_jpeg"
# Trigger clean up
async_fire_time_changed(
hass,
dt_util.utcnow() + chat_session.CONVERSATION_TIMEOUT + timedelta(seconds=1),
)
await hass.async_block_till_done(wait_background_tasks=True)
# Verify the temporary file cleaned up
assert not camera_attachment.path.exists()
assert not image_attachment.path.exists()
# Check regular media attachment
media_attachment = task.attachments[2]
assert media_attachment.media_content_id == "media-source://media_player/test.png"
assert media_attachment.mime_type == "image/jpeg"
assert media_attachment.path == Path("/media/test.png")
+3 -9
View File
@@ -65,15 +65,9 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]:
client.on_history_event = MagicMock()
client.on_volume_state_event = MagicMock()
client.on_media_state_event = MagicMock()
async def _start_http2_processing(*_args, **_kwargs) -> asyncio.Task[None]:
async def _completed_task() -> None:
return
return asyncio.create_task(_completed_task())
client.start_http2_processing = AsyncMock(side_effect=_start_http2_processing)
client.stop_http2_processing = AsyncMock()
http2_task = asyncio.Future()
http2_task.set_result(None)
client.start_http2_processing = AsyncMock(return_value=http2_task)
client.send_sound_notification = AsyncMock()
yield client

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