mirror of
https://github.com/home-assistant/core.git
synced 2026-06-01 13:39:35 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf437bc23b | |||
| 904449356c | |||
| 534ef4695b | |||
| 616a1c25a2 | |||
| ffe76224a8 | |||
| 8fadb72509 |
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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})"
|
||||
|
||||
@@ -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,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." }]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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}?"
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."]
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"universal_silabs_flasher",
|
||||
"serialx"
|
||||
],
|
||||
"requirements": ["zha==1.4.1"],
|
||||
"requirements": ["zha==1.4.0"],
|
||||
"usb": [
|
||||
{
|
||||
"description": "*2652*",
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Generated
+1
-1
@@ -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
|
||||
|
||||
Generated
+13
-13
@@ -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
|
||||
|
||||
@@ -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
@@ -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_)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user